~emersion/soju-dev

Added SSL fingerprint pinning v1 SUPERSEDED

rj1: 1
 Added SSL fingerprint pinning

 8 files changed, 54 insertions(+), 21 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/37166/mbox | git am -3
Learn more about email & git

[PATCH] Added SSL fingerprint pinning Export this patch

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.
---
 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
	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),
	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),
	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,
			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),
	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
			}
		}

		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,