rj1: 1 Added SSL fingerprint pinning 6 files changed, 75 insertions(+), 18 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~emersion/soju-dev/patches/37444/mbox | git am -3Learn more about email & git
Hi, I added support for both SHA256 and SHA512 hashes to be used as fingerprints. When a user adds a fingerprint, it checks the string length and prepends sha-256: or sha-512: to it's entry in the database, respectively. I fixed the database migration stuff as indicated. I've only tested it using sqlite. It will now check against every certificate the server offers until it finds a match. --- database/database.go | 1 + database/postgres.go | 22 +++++++++++++--------- database/sqlite.go | 16 ++++++++++------ doc/soju.1.scd | 5 +++++ service.go | 16 +++++++++++++--- upstream.go | 33 +++++++++++++++++++++++++++++++++ 6 files changed, 75 insertions(+), 18 deletions(-) diff --git a/database/database.go b/database/database.go index b4a6ac8..061d529 100644 --- a/database/database.go +++ b/database/database.go @@ -130,6 +130,7 @@ type Network struct { Realname string Pass string ConnectCommands []string + CertFP string SASL SASL AutoAway bool Enabled bool diff --git a/database/postgres.go b/database/postgres.go index 9af5c4a..3ff9503 100644 --- a/database/postgres.go +++ b/database/postgres.go @@ -44,6 +44,7 @@ CREATE TABLE "Network" ( nick VARCHAR(255), username VARCHAR(255), realname VARCHAR(255), + certfp VARCHAR(255), pass VARCHAR(255), connect_commands VARCHAR(1023), sasl_mechanism sasl_mechanism, @@ -165,6 +166,7 @@ var postgresMigrations = []string{ SET NOT NULL; `, `ALTER TABLE "Network" ADD COLUMN auto_away BOOLEAN NOT NULL DEFAULT TRUE`, + `ALTER TABLE "Network" ADD COLUMN certfp VARCHAR(255)`, } type PostgresDB struct { @@ -380,7 +382,7 @@ func (db *PostgresDB) ListNetworks(ctx context.Context, userID int64) ([]Network defer cancel() rows, err := db.db.QueryContext(ctx, ` - SELECT id, name, addr, nick, username, realname, pass, connect_commands, sasl_mechanism, + SELECT id, name, addr, nick, username, realname, certfp, pass, connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password, sasl_external_cert, sasl_external_key, auto_away, enabled FROM "Network" WHERE "user" = $1`, userID) @@ -392,7 +394,7 @@ func (db *PostgresDB) ListNetworks(ctx context.Context, userID int64) ([]Network var networks []Network for rows.Next() { var net Network - var name, nick, username, realname, pass, connectCommands sql.NullString + var name, nick, username, realname, certfp, pass, connectCommands sql.NullString var saslMechanism, saslPlainUsername, saslPlainPassword sql.NullString err := rows.Scan(&net.ID, &name, &net.Addr, &nick, &username, &realname, &pass, &connectCommands, &saslMechanism, &saslPlainUsername, &saslPlainPassword, @@ -404,6 +406,7 @@ func (db *PostgresDB) ListNetworks(ctx context.Context, userID int64) ([]Network net.Nick = nick.String net.Username = username.String net.Realname = realname.String + net.CertFP = certfp.String net.Pass = pass.String if connectCommands.Valid { net.ConnectCommands = strings.Split(connectCommands.String, "\r\n") @@ -428,6 +431,7 @@ func (db *PostgresDB) StoreNetwork(ctx context.Context, userID int64, network *N nick := toNullString(network.Nick) netUsername := toNullString(network.Username) realname := toNullString(network.Realname) + certfp := toNullString(network.CertFP) pass := toNullString(network.Pass) connectCommands := toNullString(strings.Join(network.ConnectCommands, "\r\n")) @@ -450,23 +454,23 @@ func (db *PostgresDB) StoreNetwork(ctx context.Context, userID int64, network *N var err error if network.ID == 0 { err = db.db.QueryRowContext(ctx, ` - INSERT INTO "Network" ("user", name, addr, nick, username, realname, pass, connect_commands, + INSERT INTO "Network" ("user", name, addr, nick, username, realname, certfp, pass, connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password, sasl_external_cert, sasl_external_key, auto_away, enabled) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16) RETURNING id`, - userID, netName, network.Addr, nick, netUsername, realname, pass, connectCommands, + userID, netName, network.Addr, nick, netUsername, realname, certfp, pass, connectCommands, saslMechanism, saslPlainUsername, saslPlainPassword, network.SASL.External.CertBlob, network.SASL.External.PrivKeyBlob, network.AutoAway, network.Enabled).Scan(&network.ID) } else { _, err = db.db.ExecContext(ctx, ` UPDATE "Network" - SET name = $2, addr = $3, nick = $4, username = $5, realname = $6, pass = $7, - connect_commands = $8, sasl_mechanism = $9, sasl_plain_username = $10, - sasl_plain_password = $11, sasl_external_cert = $12, sasl_external_key = $13, + SET name = $2, addr = $3, nick = $4, username = $5, realname = $6, certfp = $7, pass = $8, + connect_commands = $9, sasl_mechanism = $10, sasl_plain_username = $11, + sasl_plain_password = $12, sasl_external_cert = $13, sasl_external_key = $14, auto_away = $14, enabled = $15 WHERE id = $1`, - network.ID, netName, network.Addr, nick, netUsername, realname, pass, connectCommands, + network.ID, netName, network.Addr, nick, netUsername, realname, certfp, pass, connectCommands, saslMechanism, saslPlainUsername, saslPlainPassword, network.SASL.External.CertBlob, network.SASL.External.PrivKeyBlob, network.AutoAway, network.Enabled) } diff --git a/database/sqlite.go b/database/sqlite.go index 90b05b1..11e83ee 100644 --- a/database/sqlite.go +++ b/database/sqlite.go @@ -42,6 +42,7 @@ CREATE TABLE Network ( nick TEXT, username TEXT, realname TEXT, + certfp TEXT, pass TEXT, connect_commands TEXT, sasl_mechanism TEXT, @@ -250,6 +251,7 @@ var sqliteMigrations = []string{ `, "ALTER TABLE User ADD COLUMN nick TEXT;", "ALTER TABLE Network ADD COLUMN auto_away INTEGER NOT NULL DEFAULT 1;", + "ALTER TABLE Network ADD COLUMN certfp TEXT;", } type SqliteDB struct { @@ -488,7 +490,7 @@ func (db *SqliteDB) ListNetworks(ctx context.Context, userID int64) ([]Network, defer cancel() rows, err := db.db.QueryContext(ctx, ` - SELECT id, name, addr, nick, username, realname, pass, + SELECT id, name, addr, nick, username, realname, certfp, pass, connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password, sasl_external_cert, sasl_external_key, auto_away, enabled FROM Network @@ -502,9 +504,9 @@ func (db *SqliteDB) ListNetworks(ctx context.Context, userID int64) ([]Network, var networks []Network for rows.Next() { var net Network - var name, nick, username, realname, pass, connectCommands sql.NullString + var name, nick, username, realname, certfp, pass, connectCommands sql.NullString var saslMechanism, saslPlainUsername, saslPlainPassword sql.NullString - err := rows.Scan(&net.ID, &name, &net.Addr, &nick, &username, &realname, + err := rows.Scan(&net.ID, &name, &net.Addr, &nick, &username, &realname, &certfp, &pass, &connectCommands, &saslMechanism, &saslPlainUsername, &saslPlainPassword, &net.SASL.External.CertBlob, &net.SASL.External.PrivKeyBlob, &net.AutoAway, &net.Enabled) if err != nil { @@ -514,6 +516,7 @@ func (db *SqliteDB) ListNetworks(ctx context.Context, userID int64) ([]Network, net.Nick = nick.String net.Username = username.String net.Realname = realname.String + net.CertFP = certfp.String net.Pass = pass.String if connectCommands.Valid { net.ConnectCommands = strings.Split(connectCommands.String, "\r\n") @@ -556,6 +559,7 @@ func (db *SqliteDB) StoreNetwork(ctx context.Context, userID int64, network *Net sql.Named("nick", toNullString(network.Nick)), sql.Named("username", toNullString(network.Username)), sql.Named("realname", toNullString(network.Realname)), + sql.Named("certfp", toNullString(network.CertFP)), sql.Named("pass", toNullString(network.Pass)), sql.Named("connect_commands", toNullString(strings.Join(network.ConnectCommands, "\r\n"))), sql.Named("sasl_mechanism", saslMechanism), @@ -575,7 +579,7 @@ func (db *SqliteDB) StoreNetwork(ctx context.Context, userID int64, network *Net _, err = db.db.ExecContext(ctx, ` UPDATE Network SET name = :name, addr = :addr, nick = :nick, username = :username, - realname = :realname, pass = :pass, connect_commands = :connect_commands, + realname = :realname, certfp = :certfp, pass = :pass, connect_commands = :connect_commands, sasl_mechanism = :sasl_mechanism, sasl_plain_username = :sasl_plain_username, sasl_plain_password = :sasl_plain_password, sasl_external_cert = :sasl_external_cert, sasl_external_key = :sasl_external_key, auto_away = :auto_away, enabled = :enabled @@ -583,10 +587,10 @@ func (db *SqliteDB) StoreNetwork(ctx context.Context, userID int64, network *Net } else { var res sql.Result res, err = db.db.ExecContext(ctx, ` - INSERT INTO Network(user, name, addr, nick, username, realname, pass, + INSERT INTO Network(user, name, addr, nick, username, realname, certfp, pass, connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password, sasl_external_cert, sasl_external_key, auto_away, enabled) - VALUES (:user, :name, :addr, :nick, :username, :realname, :pass, + VALUES (:user, :name, :addr, :nick, :username, :realname, :certfp, :pass, :connect_commands, :sasl_mechanism, :sasl_plain_username, :sasl_plain_password, :sasl_external_cert, :sasl_external_key, :auto_away, :enabled)`, args...) diff --git a/doc/soju.1.scd b/doc/soju.1.scd index b5e398e..352cbce 100644 --- a/doc/soju.1.scd +++ b/doc/soju.1.scd @@ -213,6 +213,11 @@ abbreviated form, for instance *network* can be abbreviated as *net* or just Connect with the specified real name. By default, the account's realname is used if set, otherwise the network's nickname is used. + *-fingerprint* <fingerprint> + Connect to the server using an SSL fingerprint. You can set this option to + connect to an SSL enabled server using a self-signed certificate. The + fingerprint format is SHA256. + *-nick* <nickname> Connect with the specified nickname. By default, the account's username is used. diff --git a/service.go b/service.go index 7e6521c..2f8696d 100644 --- a/service.go +++ b/service.go @@ -201,7 +201,7 @@ func init() { "network": { children: serviceCommandSet{ "create": { - usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-auto-away auto-away] [-enabled enabled] [-connect-command command]...", + usage: "-addr <addr> [-name name] [-username username] [-pass pass] [-realname realname] [-fingerprint fingerprint] [-nick nick] [-auto-away auto-away] [-enabled enabled] [-connect-command command]...", desc: "add a new network", handle: handleServiceNetworkCreate, }, @@ -210,7 +210,7 @@ func init() { handle: handleServiceNetworkStatus, }, "update": { - usage: "[name] [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-nick nick] [-auto-away auto-away] [-enabled enabled] [-connect-command command]...", + usage: "[name] [-addr addr] [-name name] [-username username] [-pass pass] [-realname realname] [-fingerprint fingerprint] [-nick nick] [-auto-away auto-away] [-enabled enabled] [-connect-command command]...", desc: "update a network", handle: handleServiceNetworkUpdate, }, @@ -430,7 +430,7 @@ func getNetworkFromArg(dc *downstreamConn, params []string) (*network, []string, type networkFlagSet struct { *flag.FlagSet - Addr, Name, Nick, Username, Pass, Realname *string + Addr, Name, Nick, Username, Pass, Realname, CertFP *string AutoAway, Enabled *bool ConnectCommands []string } @@ -443,6 +443,7 @@ func newNetworkFlagSet() *networkFlagSet { fs.Var(stringPtrFlag{&fs.Username}, "username", "") fs.Var(stringPtrFlag{&fs.Pass}, "pass", "") fs.Var(stringPtrFlag{&fs.Realname}, "realname", "") + fs.Var(stringPtrFlag{&fs.CertFP}, "fingerprint", "") fs.Var(boolPtrFlag{&fs.AutoAway}, "auto-away", "") fs.Var(boolPtrFlag{&fs.Enabled}, "enabled", "") fs.Var((*stringSliceFlag)(&fs.ConnectCommands), "connect-command", "") @@ -479,6 +480,15 @@ func (fs *networkFlagSet) update(network *database.Network) error { if fs.Realname != nil { network.Realname = *fs.Realname } + if fs.CertFP != nil { + if len(*fs.CertFP) == 64 { + network.CertFP = "sha-256:" + *fs.CertFP + } else if len(*fs.CertFP) == 128 { + network.CertFP = "sha-512:" + *fs.CertFP + } else { + return fmt.Errorf("the certificate fingerprint must be a sha256 or sha512 hash") + } + } if fs.AutoAway != nil { network.AutoAway = *fs.AutoAway } diff --git a/upstream.go b/upstream.go index ddc1fb9..8ba7cd6 100644 --- a/upstream.go +++ b/upstream.go @@ -4,6 +4,7 @@ import ( "context" "crypto" "crypto/sha256" + "crypto/sha512" "crypto/tls" "crypto/x509" "encoding/base64" @@ -14,6 +15,7 @@ import ( "strconv" "strings" "time" + "encoding/hex" "github.com/emersion/go-sasl" "gopkg.in/irc.v4" @@ -215,6 +217,37 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er logger.Printf("using TLS client certificate %x", sha256.Sum256(network.SASL.External.CertBlob)) } + if network.CertFP != "" { + tlsConfig.InsecureSkipVerify = true + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + + parts := strings.Split(network.CertFP, ":") + CertFPType := parts[0] + LocalCertFP := parts[1] + + for _, rawCert := range rawCerts { + var RemoteCertFP string + if CertFPType == "sha-512" { + sha512Sum := sha512.Sum512(rawCert) + RemoteCertFP = hex.EncodeToString(sha512Sum[:]) + } else if CertFPType == "sha-256" { + sha256Sum := sha256.Sum256(rawCert) + RemoteCertFP = hex.EncodeToString(sha256Sum[:]) + } + + if RemoteCertFP == LocalCertFP { + // fingerprints match - let's connect + return nil + } + } + + // fingerprints don't match, let's give the user a fingerprint they can use to connect + sha512Sum := sha512.Sum512(rawCerts[0]) + RemoteCertFP := hex.EncodeToString(sha512Sum[:]) + return fmt.Errorf("your fingerprint doesn't match that of the servers - %s", RemoteCertFP) + } + } + netConn, err = dialer.DialContext(ctx, "tcp", addr) if err != nil { return nil, fmt.Errorf("failed to dial %q: %v", addr, err) -- 2.38.1
Pushed with some minor edits, thanks!