wraith/internal/ssh/hostkey.go
Vantz Stockwell cab286b4a6 feat: SSH host key verification + OSC 7 CWD tracker
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 06:52:59 -04:00

90 lines
2.8 KiB
Go

package ssh
import (
"database/sql"
"fmt"
)
// HostKeyResult represents the result of a host key verification.
type HostKeyResult int
const (
HostKeyNew HostKeyResult = iota // never seen this host before
HostKeyMatch // fingerprint matches stored
HostKeyChanged // fingerprint CHANGED — possible MITM
)
// HostKeyStore stores and verifies SSH host key fingerprints in the host_keys SQLite table.
type HostKeyStore struct {
db *sql.DB
}
// NewHostKeyStore creates a new HostKeyStore backed by the given database.
func NewHostKeyStore(db *sql.DB) *HostKeyStore {
return &HostKeyStore{db: db}
}
// Verify checks whether the given fingerprint matches any stored host key for the
// specified hostname, port, and key type. It returns HostKeyNew if no key is stored,
// HostKeyMatch if the fingerprint matches, or HostKeyChanged if it differs.
func (s *HostKeyStore) Verify(hostname string, port int, keyType string, fingerprint string) (HostKeyResult, error) {
var storedFingerprint string
err := s.db.QueryRow(
"SELECT fingerprint FROM host_keys WHERE hostname = ? AND port = ? AND key_type = ?",
hostname, port, keyType,
).Scan(&storedFingerprint)
if err == sql.ErrNoRows {
return HostKeyNew, nil
}
if err != nil {
return 0, fmt.Errorf("query host key: %w", err)
}
if storedFingerprint == fingerprint {
return HostKeyMatch, nil
}
return HostKeyChanged, nil
}
// Store inserts or replaces a host key fingerprint for the given hostname, port, and key type.
func (s *HostKeyStore) Store(hostname string, port int, keyType string, fingerprint string, rawKey string) error {
_, err := s.db.Exec(
`INSERT INTO host_keys (hostname, port, key_type, fingerprint, raw_key)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT (hostname, port, key_type)
DO UPDATE SET fingerprint = excluded.fingerprint, raw_key = excluded.raw_key`,
hostname, port, keyType, fingerprint, rawKey,
)
if err != nil {
return fmt.Errorf("store host key: %w", err)
}
return nil
}
// Delete removes all stored host keys for the given hostname and port.
func (s *HostKeyStore) Delete(hostname string, port int) error {
_, err := s.db.Exec(
"DELETE FROM host_keys WHERE hostname = ? AND port = ?",
hostname, port,
)
if err != nil {
return fmt.Errorf("delete host key: %w", err)
}
return nil
}
// GetFingerprint returns the stored fingerprint for the given hostname and port.
// It returns an empty string and sql.ErrNoRows if no key is stored.
func (s *HostKeyStore) GetFingerprint(hostname string, port int) (string, error) {
var fingerprint string
err := s.db.QueryRow(
"SELECT fingerprint FROM host_keys WHERE hostname = ? AND port = ?",
hostname, port,
).Scan(&fingerprint)
if err != nil {
return "", fmt.Errorf("get fingerprint: %w", err)
}
return fingerprint, nil
}