From c956e49d44545ebc0d8455ff8903cc3e40d7547a Mon Sep 17 00:00:00 2001 From: Sam Anthony Date: Mon, 14 Apr 2025 19:13:50 -0400 Subject: store signature verification key in known hosts file --- hosts/hosts.go | 101 +++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 36 deletions(-) (limited to 'hosts') 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) +} -- cgit v1.2.3