~emersion/soju-dev

Added SSL fingerprint pinning v2 APPLIED

rj1: 1
 Added SSL fingerprint pinning

 6 files changed, 75 insertions(+), 18 deletions(-)
Export patchset (mbox)
How do I use this?

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 -3
Learn more about email & git

[PATCH v2] Added SSL fingerprint pinning Export this patch

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!