Changeset 307 in code for trunk


Ignore:
Timestamp:
Jun 2, 2020, 9:24:22 AM (5 years ago)
Author:
fox.cpp
Message:

Implement upstream SASL EXTERNAL support

Closes: https://todo.sr.ht/~emersion/soju/47

Location:
trunk
Files:
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/db.go

    r284 r307  
    2121                Username string
    2222                Password string
     23        }
     24
     25        // TLS client certificate authentication.
     26        External struct {
     27                // X.509 certificate in DER form.
     28                CertBlob []byte
     29                // PKCS#8 private key in DER form.
     30                PrivKeyBlob []byte
    2331        }
    2432}
     
    6977        sasl_plain_username VARCHAR(255),
    7078        sasl_plain_password VARCHAR(255),
     79        sasl_external_cert BLOB DEFAULT NULL,
     80        sasl_external_key BLOB DEFAULT NULL,
    7181        FOREIGN KEY(user) REFERENCES User(username),
    7282        UNIQUE(user, addr, nick)
     
    8898        "ALTER TABLE Network ADD COLUMN connect_commands VARCHAR(1023)",
    8999        "ALTER TABLE Channel ADD COLUMN detached INTEGER NOT NULL DEFAULT 0",
     100        "ALTER TABLE Network ADD COLUMN sasl_external_cert BLOB DEFAULT NULL",
     101        "ALTER TABLE Network ADD COLUMN sasl_external_key BLOB DEFAULT NULL",
    90102}
    91103
     
    239251
    240252        rows, err := db.db.Query(`SELECT id, name, addr, nick, username, realname, pass,
    241                         connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password
     253                        connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password,
     254                        sasl_external_cert, sasl_external_key
    242255                FROM Network
    243256                WHERE user = ?`,
     
    254267                var saslMechanism, saslPlainUsername, saslPlainPassword *string
    255268                err := rows.Scan(&net.ID, &name, &net.Addr, &net.Nick, &username, &realname,
    256                         &pass, &connectCommands, &saslMechanism, &saslPlainUsername, &saslPlainPassword)
     269                        &pass, &connectCommands, &saslMechanism, &saslPlainUsername, &saslPlainPassword,
     270                        &net.SASL.External.CertBlob, &net.SASL.External.PrivKeyBlob)
    257271                if err != nil {
    258272                        return nil, err
     
    294308                        saslPlainUsername = toStringPtr(network.SASL.Plain.Username)
    295309                        saslPlainPassword = toStringPtr(network.SASL.Plain.Password)
     310                        network.SASL.External.CertBlob = nil
     311                        network.SASL.External.PrivKeyBlob = nil
     312                case "EXTERNAL":
     313                        // keep saslPlain* nil
    296314                default:
    297315                        return fmt.Errorf("soju: cannot store network: unsupported SASL mechanism %q", network.SASL.Mechanism)
     
    303321                _, err = db.db.Exec(`UPDATE Network
    304322                        SET name = ?, addr = ?, nick = ?, username = ?, realname = ?, pass = ?, connect_commands = ?,
    305                                 sasl_mechanism = ?, sasl_plain_username = ?, sasl_plain_password = ?
     323                                sasl_mechanism = ?, sasl_plain_username = ?, sasl_plain_password = ?,
     324                                sasl_external_cert = ?, sasl_external_key = ?
    306325                        WHERE id = ?`,
    307326                        netName, network.Addr, network.Nick, netUsername, realname, pass, connectCommands,
    308                         saslMechanism, saslPlainUsername, saslPlainPassword, network.ID)
     327                        saslMechanism, saslPlainUsername, saslPlainPassword,
     328                        network.SASL.External.CertBlob, network.SASL.External.PrivKeyBlob,
     329                        network.ID)
    309330        } else {
    310331                var res sql.Result
    311332                res, err = db.db.Exec(`INSERT INTO Network(user, name, addr, nick, username,
    312333                                realname, pass, connect_commands, sasl_mechanism, sasl_plain_username,
    313                                 sasl_plain_password)
    314                         VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
     334                                sasl_plain_password, sasl_external_cert, sasl_external_key)
     335                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
    315336                        username, netName, network.Addr, network.Nick, netUsername, realname, pass, connectCommands,
    316                         saslMechanism, saslPlainUsername, saslPlainPassword)
     337                        saslMechanism, saslPlainUsername, saslPlainPassword, network.SASL.External.CertBlob,
     338                        network.SASL.External.PrivKeyBlob)
    317339                if err != nil {
    318340                        return err
  • trunk/doc/soju.1.scd

    r284 r307  
    120120                is used.
    121121
     122*certfp generate* *[options...]* <network name>
     123        Generate self-signed certificate and use it for authentication.
     124
     125        Generates RSA-3072 private key by default.
     126
     127        Options are:
     128
     129        *-key-type* <type>
     130                Private key algoritm to use. Valid values are: rsa, ecdsa, ed25519.
     131                ecdsa uses NIST P-521 curve.
     132
     133        *-bits* <bits>
     134                Size of RSA key to generate. Ignored for other key types.
     135
     136*certfp fingerprint* <network name>
     137        Show SHA-1 and SHA-256 fingerprints for the certificate
     138        currently used with the network.
     139
     140*certfp reset* <network name>
     141        Disable SASL EXTERNAL authentication and remove stored certificate.
     142
    122143*network delete* <name>
    123144        Disconnect and delete a network.
  • trunk/downstream.go

    r306 r307  
    15241524        }
    15251525
     1526        // User may have e.g. EXTERNAL mechanism configured. We do not want to
     1527        // automatically erase the key pair or any other credentials.
     1528        if uc.network.SASL.Mechanism != "" && uc.network.SASL.Mechanism != "PLAIN" {
     1529                return
     1530        }
     1531
    15261532        dc.logger.Printf("auto-saving NickServ credentials with username %q", username)
    15271533        n := uc.network
  • trunk/service.go

    r279 r307  
    22
    33import (
     4        "crypto"
     5        "crypto/ecdsa"
     6        "crypto/ed25519"
     7        "crypto/elliptic"
     8        "crypto/rand"
     9        "crypto/rsa"
     10        "crypto/sha1"
     11        "crypto/sha256"
     12        "crypto/x509"
     13        "crypto/x509/pkix"
     14        "encoding/hex"
     15        "errors"
    416        "flag"
    517        "fmt"
    618        "io/ioutil"
     19        "math/big"
    720        "strings"
     21        "time"
    822
    923        "github.com/google/shlex"
     
    120134                        },
    121135                },
     136                "certfp": {
     137                        children: serviceCommandSet{
     138                                "generate": {
     139                                        usage:  "[-key-type rsa|ecdsa|ed25519] [-bits N] <network name>",
     140                                        desc:   "generate a new self-signed certificate, defaults to using RSA-3072 key",
     141                                        handle: handleServiceCertfpGenerate,
     142                                },
     143                                "fingerprint": {
     144                                        usage:  "<network name>",
     145                                        desc:   "show fingerprints of certificate associated with the network",
     146                                        handle: handleServiceCertfpFingerprints,
     147                                },
     148                                "reset": {
     149                                        usage:  "<network name>",
     150                                        desc:   "disable SASL EXTERNAL authentication and remove stored certificate",
     151                                        handle: handleServiceCertfpReset,
     152                                },
     153                        },
     154                },
    122155                "change-password": {
    123156                        usage:  "<new password>",
     
    126159                },
    127160        }
     161}
     162
     163func handleServiceCertfpGenerate(dc *downstreamConn, params []string) error {
     164        fs := newFlagSet()
     165        keyType := fs.String("key-type", "rsa", "key type to generate (rsa, ecdsa, ed25519)")
     166        bits := fs.Int("bits", 3072, "size of key to generate, meaningful only for RSA")
     167
     168        if err := fs.Parse(params); err != nil {
     169                return err
     170        }
     171
     172        if len(fs.Args()) != 1 {
     173                return errors.New("exactly one argument is required")
     174        }
     175
     176        net := dc.user.getNetwork(fs.Arg(0))
     177        if net == nil {
     178                return fmt.Errorf("unknown network %q", fs.Arg(0))
     179        }
     180
     181        var (
     182                privKey crypto.PrivateKey
     183                pubKey  crypto.PublicKey
     184        )
     185        switch *keyType {
     186        case "rsa":
     187                key, err := rsa.GenerateKey(rand.Reader, *bits)
     188                if err != nil {
     189                        return err
     190                }
     191                privKey = key
     192                pubKey = key.Public()
     193        case "ecdsa":
     194                key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
     195                if err != nil {
     196                        return err
     197                }
     198                privKey = key
     199                pubKey = key.Public()
     200        case "ed25519":
     201                var err error
     202                pubKey, privKey, err = ed25519.GenerateKey(rand.Reader)
     203                if err != nil {
     204                        return err
     205                }
     206        }
     207
     208        // Using PKCS#8 allows easier extension for new key types.
     209        privKeyBytes, err := x509.MarshalPKCS8PrivateKey(privKey)
     210        if err != nil {
     211                return err
     212        }
     213
     214        notBefore := time.Now()
     215        // Lets make a fair assumption nobody will use the same cert for more than 20 years...
     216        notAfter := notBefore.Add(24 * time.Hour * 365 * 20)
     217        serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
     218        serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
     219        if err != nil {
     220                return err
     221        }
     222        cert := &x509.Certificate{
     223                SerialNumber: serialNumber,
     224                Subject:      pkix.Name{CommonName: "soju auto-generated certificate"},
     225                NotBefore:    notBefore,
     226                NotAfter:     notAfter,
     227                KeyUsage:     x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
     228                ExtKeyUsage:  []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
     229        }
     230        derBytes, err := x509.CreateCertificate(rand.Reader, cert, cert, pubKey, privKey)
     231        if err != nil {
     232                return err
     233        }
     234
     235        net.SASL.External.CertBlob = derBytes
     236        net.SASL.External.PrivKeyBlob = privKeyBytes
     237        net.SASL.Mechanism = "EXTERNAL"
     238
     239        if err := dc.srv.db.StoreNetwork(net.Username, &net.Network); err != nil {
     240                return err
     241        }
     242
     243        sendServicePRIVMSG(dc, "certificate generated")
     244
     245        sha1Sum := sha1.Sum(derBytes)
     246        sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
     247        sha256Sum := sha256.Sum256(derBytes)
     248        sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
     249
     250        return nil
     251}
     252
     253func handleServiceCertfpFingerprints(dc *downstreamConn, params []string) error {
     254        if len(params) != 1 {
     255                return fmt.Errorf("expected exactly one argument")
     256        }
     257
     258        net := dc.user.getNetwork(params[0])
     259        if net == nil {
     260                return fmt.Errorf("unknown network %q", params[0])
     261        }
     262
     263        sha1Sum := sha1.Sum(net.SASL.External.CertBlob)
     264        sendServicePRIVMSG(dc, "SHA-1 fingerprint: "+hex.EncodeToString(sha1Sum[:]))
     265        sha256Sum := sha256.Sum256(net.SASL.External.CertBlob)
     266        sendServicePRIVMSG(dc, "SHA-256 fingerprint: "+hex.EncodeToString(sha256Sum[:]))
     267        return nil
     268}
     269
     270func handleServiceCertfpReset(dc *downstreamConn, params []string) error {
     271        if len(params) != 1 {
     272                return fmt.Errorf("expected exactly one argument")
     273        }
     274
     275        net := dc.user.getNetwork(params[0])
     276        if net == nil {
     277                return fmt.Errorf("unknown network %q", params[0])
     278        }
     279
     280        net.SASL.External.CertBlob = nil
     281        net.SASL.External.PrivKeyBlob = nil
     282
     283        if net.SASL.Mechanism == "EXTERNAL" {
     284                net.SASL.Mechanism = ""
     285        }
     286        if err := dc.srv.db.StoreNetwork(dc.user.Username, &net.Network); err != nil {
     287                return err
     288        }
     289
     290        sendServicePRIVMSG(dc, "certificate reset")
     291        return nil
    128292}
    129293
  • trunk/upstream.go

    r305 r307  
    22
    33import (
     4        "crypto"
     5        "crypto/sha256"
    46        "crypto/tls"
     7        "crypto/x509"
    58        "encoding/base64"
    69        "errors"
     
    101104
    102105                logger.Printf("connecting to TLS server at address %q", addr)
    103                 netConn, err = tls.DialWithDialer(&dialer, "tcp", addr, nil)
     106
     107                var cfg *tls.Config
     108                if network.SASL.Mechanism == "EXTERNAL" {
     109                        if network.SASL.External.CertBlob == nil {
     110                                return nil, fmt.Errorf("missing certificate for authentication")
     111                        }
     112                        if network.SASL.External.PrivKeyBlob == nil {
     113                                return nil, fmt.Errorf("missing private key for authentication")
     114                        }
     115                        key, err := x509.ParsePKCS8PrivateKey(network.SASL.External.PrivKeyBlob)
     116                        if err != nil {
     117                                return nil, fmt.Errorf("failed to parse private key: %v", err)
     118                        }
     119                        cfg = &tls.Config{
     120                                Certificates: []tls.Certificate{
     121                                        {
     122                                                Certificate: [][]byte{network.SASL.External.CertBlob},
     123                                                PrivateKey:  key.(crypto.PrivateKey),
     124                                        },
     125                                },
     126                        }
     127                        logger.Printf("using TLS client certificate %x", sha256.Sum256(network.SASL.External.CertBlob))
     128                }
     129
     130                netConn, err = tls.DialWithDialer(&dialer, "tcp", addr, cfg)
    104131        case "irc+insecure":
    105132                if !strings.ContainsRune(addr, ':') {
     
    14001427                        uc.logger.Printf("starting SASL PLAIN authentication with username %q", auth.Plain.Username)
    14011428                        uc.saslClient = sasl.NewPlainClient("", auth.Plain.Username, auth.Plain.Password)
     1429                case "EXTERNAL":
     1430                        uc.logger.Printf("starting SASL EXTERNAL authentication")
     1431                        uc.saslClient = sasl.NewExternalClient("")
    14021432                default:
    14031433                        return fmt.Errorf("unsupported SASL mechanism %q", name)
Note: See TracChangeset for help on using the changeset viewer.