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,
> 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