aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSam Anthony <sam@samanthony.xyz>2025-04-14 19:13:50 -0400
committerSam Anthony <sam@samanthony.xyz>2025-04-14 19:13:50 -0400
commitc956e49d44545ebc0d8455ff8903cc3e40d7547a (patch)
tree88a87de4264e441ad4260cbe7d5bd812d53d17b4
parent02ace55f4cd39b7b9c97a361393a09fcaa1bd9a3 (diff)
downloadhose-c956e49d44545ebc0d8455ff8903cc3e40d7547a.zip
store signature verification key in known hosts file
-rw-r--r--hosts/hosts.go101
-rw-r--r--key/key.go15
2 files changed, 80 insertions, 36 deletions
diff --git a/hosts/hosts.go b/hosts/hosts.go
index 23883e2..3dc3f33 100644
--- a/hosts/hosts.go
+++ b/hosts/hosts.go
@@ -6,41 +6,46 @@ import (
"errors"
"fmt"
"github.com/adrg/xdg"
- "net"
"net/netip"
"os"
"path/filepath"
+ "slices"
"strings"
+
+ "git.samanthony.xyz/hose/key"
+ "git.samanthony.xyz/hose/util"
)
var knownHostsFile = filepath.Join(xdg.DataHome, "hose", "known_hosts")
-// Set sets the public key of a remote host.
-// It replaces or creates an entry in the known hosts file.
-func Set(hostport net.Addr, pubkey [32]byte) error {
- host, _, err := net.SplitHostPort(hostport.String())
- if err != nil {
- return err
- }
- addr, err := netip.ParseAddr(host)
- if err != nil {
- return err
- }
+type Host struct {
+ netip.Addr // address.
+ key.BoxPublicKey // public encryption key.
+ key.SigPublicKey // public signature verification key.
+}
+// Add adds or replaces an entry in the known hosts file.
+func Add(host Host) error {
hosts, err := Load()
if err != nil {
return err
}
- hosts[addr] = pubkey
+ i, ok := slices.BinarySearchFunc(hosts, host, cmpHost)
+ if ok {
+ util.Logf("replacing host %q in known hosts file")
+ hosts[i] = host
+ } else {
+ hosts = slices.Insert(hosts, i, host)
+ }
return Store(hosts)
}
-// Load loads the set of known hosts and their associated public keys
-// from disc.
-func Load() (map[netip.Addr][32]byte, error) {
- hosts := make(map[netip.Addr][32]byte)
+// Load loads the set of known hosts from disc.
+// The returned list is sorted.
+func Load() ([]Host, error) {
+ hosts := make([]Host, 0)
f, err := os.Open(knownHostsFile)
if errors.Is(err, os.ErrNotExist) {
@@ -52,53 +57,77 @@ func Load() (map[netip.Addr][32]byte, error) {
scanner := bufio.NewScanner(f)
for line := 1; scanner.Scan(); line++ {
- host, pubkey, err := parseHostKeyPair(scanner.Text())
+ host, err := parseHost(scanner.Text())
if err != nil {
return hosts, fmt.Errorf("error parsing known hosts file: %s:%d: %v", knownHostsFile, line, err)
}
- if _, ok := hosts[host]; ok {
+ i, ok := slices.BinarySearchFunc(hosts, host, cmpHost)
+ if ok {
return hosts, fmt.Errorf("duplicate entry in known hosts file: %s", host)
}
- hosts[host] = pubkey
+ hosts = slices.Insert(hosts, i, host)
}
return hosts, scanner.Err()
}
-// parseHostKeyPair parses a line of the known hosts file.
-func parseHostKeyPair(s string) (netip.Addr, [32]byte, error) {
+// parseHost parses a line of the known hosts file.
+func parseHost(s string) (Host, error) {
fields := strings.Fields(s)
- if len(fields) != 2 {
- return netip.Addr{}, [32]byte{}, fmt.Errorf("expected 2 fields; got %d", len(fields))
+ if len(fields) != 3 {
+ return Host{}, fmt.Errorf("expected 3 fields; got %d", len(fields))
}
addr, err := netip.ParseAddr(fields[0])
if err != nil {
- return netip.Addr{}, [32]byte{}, err
+ return Host{}, err
}
- var key [32]byte
- if hex.DecodedLen(len(fields[1])) != len(key) {
- return netip.Addr{}, [32]byte{}, fmt.Errorf("malformed key: %s", fields[1])
+ var boxPubKey key.BoxPublicKey
+ if hex.DecodedLen(len(fields[1])) != len(boxPubKey) {
+ return Host{}, fmt.Errorf("malformed box public key: %s", fields[1])
}
- if _, err := hex.Decode(key[:], []byte(fields[1])); err != nil {
- return netip.Addr{}, [32]byte{}, err
+ if _, err := hex.Decode(boxPubKey[:], []byte(fields[1])); err != nil {
+ return Host{}, err
}
- return addr, key, nil
+ var sigPubKey key.SigPublicKey
+ if hex.DecodedLen(len(fields[2])) != len(sigPubKey) {
+ return Host{}, fmt.Errorf("malformed signature public key: %s", fields[2])
+ }
+ if _, err := hex.Decode(sigPubKey[:], []byte(fields[2])); err != nil {
+ return Host{}, err
+ }
+
+ return Host{addr, boxPubKey, sigPubKey}, nil
}
-// Store stores the set of known hosts and their associated public keys
-// to disc. It overwrites the entire file.
-func Store(hosts map[netip.Addr][32]byte) error {
+// Store stores the set of known hosts to disc. It overwrites the entire file.
+func Store(hosts []Host) error {
+ slices.SortFunc(hosts, cmpHost)
+
f, err := os.Create(knownHostsFile)
if err != nil {
return err
}
defer f.Close()
- for host, key := range hosts {
- fmt.Fprintf(f, "%s %x\n", host, key)
+ for _, host := range hosts {
+ fmt.Fprintf(f, "%s\n", host)
}
return nil
}
+
+func cmpHost(a, b Host) int {
+ if x := a.Addr.Compare(b.Addr); x != 0 {
+ return x
+ }
+ if x := a.BoxPublicKey.Compare(b.BoxPublicKey); x != 0 {
+ return x
+ }
+ return a.SigPublicKey.Compare(b.SigPublicKey)
+}
+
+func (h Host) String() string {
+ return fmt.Sprintf("%s %x %x", h.Addr, h.BoxPublicKey, h.SigPublicKey)
+}
diff --git a/key/key.go b/key/key.go
index 38b4bad..b3dd393 100644
--- a/key/key.go
+++ b/key/key.go
@@ -1,6 +1,7 @@
package key
import (
+ "bytes"
"encoding/hex"
"fmt"
"io"
@@ -13,6 +14,12 @@ type BoxPublicKey [32]byte
// BoxPrivateKey is a private NaCl box key.
type BoxPrivateKey [32]byte
+// SigPublicKey is a public NaCl signature verification key.
+type SigPublicKey [32]byte
+
+// SigPrivateKey is a private NaCl signing key.
+type SigPrivateKey [64]byte
+
// LoadBoxKeypair reads the public and private NaCl box keys from disc,
// or generates a new keypair if it does not already exist.
// These keys can be used for NaCl box (encryption/decryption) operations.
@@ -67,3 +74,11 @@ func loadBoxKey(filename string) ([32]byte, error) {
return key, nil
}
+
+func (bpk1 BoxPublicKey) Compare(bpk2 BoxPublicKey) int {
+ return bytes.Compare(bpk1[:], bpk2[:])
+}
+
+func (spk1 SigPublicKey) Compare(spk2 SigPublicKey) int {
+ return bytes.Compare(spk1[:], spk2[:])
+}