~emersion/soju-dev

This thread contains a patchset. You're looking at the original emails, but you may wish to use the patch review UI. Review patch
1

[PATCH] Added SSL fingerprint pinning

Details
Message ID
<20221127050754.60478-1-rj1@riseup.net>
DKIM signature
missing
Download raw message
Patch: +54 -21
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
Details
Message ID
<PVg7X9wieBDmTkPGpNgiM9-HcuEJOKVpR0yWXLKLOCHtBtDdXf3FlMAZCJLcNFRYh9bPDnD3BEiSOWj6gNCwGZ6ildAyoA6AEMt8ahHzXEI=@emersion.fr>
In-Reply-To
<20221127050754.60478-1-rj1@riseup.net> (view parent)
DKIM signature
missing
Download raw message
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
Reply to thread Export thread (mbox)