rj1: 1 Added SSL fingerprint pinning 8 files changed, 54 insertions(+), 21 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/37166/mbox | git am -3Learn more about email & git
Hi, I added SSL fingerprint pinning functionality to soju. I need this feature in order connect to some IRC servers whose admins prefer to use self-signed certificates.
Thanks for the patch! This is a feature which has been requested multiple times, would be great to get merged! Here are a few comments.
--- database/database.go | 1 + database/postgres.go | 21 ++++++++++++--------- database/postgres_test.go | 1 + database/sqlite.go | 22 +++++++++++++--------- database/sqlite_test.go | 1 + doc/soju.1.scd | 5 +++++ service.go | 10 +++++++--- upstream.go | 14 ++++++++++++++ 8 files changed, 54 insertions(+), 21 deletions(-) diff --git a/database/database.go b/database/database.go index b4a6ac8..1150e57 100644 --- a/database/database.go +++ b/database/database.go @@ -130,6 +130,7 @@ type Network struct { Realname string Pass string ConnectCommands []string + Fingerprint string
Can we rename this to e.g. CertFingerprint or CertFP? To avoid mixing it up with the public key fingerprint. Also, can we include the hash algorithm name too? I'm worried about a future change to add support for more hash algorithms, e.g. SHA512. We can either prepend "Sha256" to the name, or put "sha-256:<hash>" in the field values, or maybe you have a better idea? See also: - The WebIRC extension deals with this kind of thing: https://ircv3.net/specs/extensions/webirc - The IANA registry for hash algos: https://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xhtml
SASL SASL AutoAway bool Enabled bool diff --git a/database/postgres.go b/database/postgres.go index 9af5c4a..55a3d81 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), + fingerprint VARCHAR(255),
This updates the schema, which is good, but we also need to append a migration.
pass VARCHAR(255), connect_commands VARCHAR(1023), sasl_mechanism sasl_mechanism, @@ -380,7 +381,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, fingerprint, 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 +393,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, fingerprint, 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 +405,7 @@ func (db *PostgresDB) ListNetworks(ctx context.Context, userID int64) ([]Network net.Nick = nick.String net.Username = username.String net.Realname = realname.String + net.Fingerprint = fingerprint.String net.Pass = pass.String if connectCommands.Valid { net.ConnectCommands = strings.Split(connectCommands.String, "\r\n") @@ -428,6 +430,7 @@ func (db *PostgresDB) StoreNetwork(ctx context.Context, userID int64, network *N nick := toNullString(network.Nick) netUsername := toNullString(network.Username) realname := toNullString(network.Realname) + fingerprint := toNullString(network.Fingerprint) pass := toNullString(network.Pass) connectCommands := toNullString(strings.Join(network.ConnectCommands, "\r\n")) @@ -450,23 +453,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, fingerprint, 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, fingerprint, 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, fingerprint = $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, fingerprint, pass, connectCommands, saslMechanism, saslPlainUsername, saslPlainPassword, network.SASL.External.CertBlob, network.SASL.External.PrivKeyBlob, network.AutoAway, network.Enabled) } diff --git a/database/postgres_test.go b/database/postgres_test.go index 4df736b..d92787d 100644 --- a/database/postgres_test.go +++ b/database/postgres_test.go @@ -31,6 +31,7 @@ CREATE TABLE "Network" ( nick VARCHAR(255) NOT NULL, username VARCHAR(255), realname VARCHAR(255), + fingerprint VARCHAR(255),
This is the initial v0 schema, which should never be changed.
pass VARCHAR(255), connect_commands VARCHAR(1023), sasl_mechanism VARCHAR(255), diff --git a/database/sqlite.go b/database/sqlite.go index 90b05b1..29b96be 100644 --- a/database/sqlite.go +++ b/database/sqlite.go @@ -42,6 +42,7 @@ CREATE TABLE Network ( nick TEXT, username TEXT, realname TEXT, + fingerprint TEXT, pass TEXT, connect_commands TEXT, sasl_mechanism TEXT, @@ -154,7 +155,7 @@ var sqliteMigrations = []string{ ); INSERT INTO NetworkNew SELECT Network.id, name, User.id as user, addr, nick, - Network.username, realname, pass, connect_commands, + Network.username, realname, fingerprint, pass, connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password, sasl_external_cert, sasl_external_key FROM Network @@ -191,6 +192,7 @@ var sqliteMigrations = []string{ nick TEXT, username TEXT, realname TEXT, + fingerprint TEXT,
This (and the line changed above) update an old migration. We cannot do this. Instead, we need to append a new migration which adds the column to the existing table.
pass TEXT, connect_commands TEXT, sasl_mechanism TEXT, @@ -204,8 +206,8 @@ var sqliteMigrations = []string{ UNIQUE(user, name) ); INSERT INTO NetworkNew - SELECT id, name, user, addr, nick, username, realname, pass, - connect_commands, sasl_mechanism, sasl_plain_username, + SELECT id, name, user, addr, nick, username, realname, fingerprint, + pass, connect_commands, sasl_mechanism, sasl_plain_username, sasl_plain_password, sasl_external_cert, sasl_external_key, enabled FROM Network; @@ -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, fingerprint, 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, fingerprint, 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, &fingerprint, &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.Fingerprint = fingerprint.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("fingerprint", toNullString(network.Fingerprint)), 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, fingerprint = :fingerprint, 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, fingerprint, 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, :fingerprint, :pass, :connect_commands, :sasl_mechanism, :sasl_plain_username, :sasl_plain_password, :sasl_external_cert, :sasl_external_key, :auto_away, :enabled)`, args...) diff --git a/database/sqlite_test.go b/database/sqlite_test.go index b1376cb..03e6a7e 100644 --- a/database/sqlite_test.go +++ b/database/sqlite_test.go @@ -22,6 +22,7 @@ CREATE TABLE Network ( nick VARCHAR(255) NOT NULL, username VARCHAR(255), realname VARCHAR(255), + fingerprint VARCHAR(255),
Ditto.
pass VARCHAR(255), sasl_mechanism VARCHAR(255), sasl_plain_username VARCHAR(255), 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..20d8c04 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, Fingerprint *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.Fingerprint}, "fingerprint", "") fs.Var(boolPtrFlag{&fs.AutoAway}, "auto-away", "") fs.Var(boolPtrFlag{&fs.Enabled}, "enabled", "") fs.Var((*stringSliceFlag)(&fs.ConnectCommands), "connect-command", "") @@ -479,6 +480,9 @@ func (fs *networkFlagSet) update(network *database.Network) error { if fs.Realname != nil { network.Realname = *fs.Realname } + if fs.Fingerprint != nil { + network.Fingerprint = *fs.Fingerprint + } if fs.AutoAway != nil { network.AutoAway = *fs.AutoAway } diff --git a/upstream.go b/upstream.go index ddc1fb9..5941caa 100644 --- a/upstream.go +++ b/upstream.go @@ -14,6 +14,7 @@ import ( "strconv" "strings" "time" + "encoding/hex" "github.com/emersion/go-sasl" "gopkg.in/irc.v4" @@ -215,6 +216,19 @@ func connectToUpstream(ctx context.Context, network *network) (*upstreamConn, er logger.Printf("using TLS client certificate %x", sha256.Sum256(network.SASL.External.CertBlob)) } + if network.Fingerprint != "" { + tlsConfig.InsecureSkipVerify = true + tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + cert := rawCerts[0] + sha256Sum := sha256.Sum256(cert) + remoteFingerprint := hex.EncodeToString(sha256Sum[:]) + if network.Fingerprint != remoteFingerprint { + return fmt.Errorf("Your fingerprint doesn't match that of the servers - %s", remoteFingerprint) + } + return nil + } + }
The indentation here is off. We use only tabs. Should we look at each certificate in the list? It seems like it's possible for a server to present multiple of these.
+ netConn, err = dialer.DialContext(ctx, "tcp", addr) if err != nil { return nil, fmt.Errorf("failed to dial %q: %v", addr, err) -- 2.38.1
Hi,