99 lines
3.0 KiB
Go
99 lines
3.0 KiB
Go
// Package knownhosts manages SSH known host entries, providing verification,
|
|
// storage, and management of host key fingerprints.
|
|
package knownhosts
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/andydixon/nexus/internal/models"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// KnownHostStorage is the persistence interface used by the Store.
|
|
type KnownHostStorage interface {
|
|
GetKnownHost(host string, port int) (*models.KnownHost, error)
|
|
SaveKnownHost(kh *models.KnownHost) error
|
|
ListKnownHosts() ([]*models.KnownHost, error)
|
|
DeleteKnownHost(id string) error
|
|
}
|
|
|
|
// Store manages SSH known host entries. It provides verification against
|
|
// previously accepted host keys and allows new keys to be trusted.
|
|
type Store struct {
|
|
storage KnownHostStorage
|
|
}
|
|
|
|
// NewStore creates a Store backed by the given storage implementation.
|
|
func NewStore(storage KnownHostStorage) *Store {
|
|
return &Store{storage: storage}
|
|
}
|
|
|
|
// Verify checks whether the presented host key matches a previously accepted
|
|
// entry. Returns true if the fingerprint matches, false if there is no record,
|
|
// and an error if the key has changed (possible MITM).
|
|
func (s *Store) Verify(host string, port int, keyType string, fingerprint string, publicKey string) (bool, error) {
|
|
existing, err := s.storage.GetKnownHost(host, port)
|
|
if err != nil {
|
|
return false, fmt.Errorf("knownhosts: lookup failed: %w", err)
|
|
}
|
|
if existing == nil {
|
|
// No prior record — the caller should prompt the user.
|
|
return false, nil
|
|
}
|
|
|
|
if existing.Fingerprint == fingerprint {
|
|
// Key matches — update the last-seen timestamp.
|
|
existing.LastSeen = time.Now()
|
|
_ = s.storage.SaveKnownHost(existing)
|
|
return true, nil
|
|
}
|
|
|
|
// Fingerprint mismatch — the host key has changed.
|
|
return false, fmt.Errorf(
|
|
"knownhosts: host key for %s:%d has changed (was %s %s, now %s %s); "+
|
|
"this could indicate a man-in-the-middle attack",
|
|
host, port,
|
|
existing.KeyType, existing.Fingerprint,
|
|
keyType, fingerprint,
|
|
)
|
|
}
|
|
|
|
// Accept stores a new host key as trusted. If an entry already exists for the
|
|
// host/port combination it is overwritten.
|
|
func (s *Store) Accept(host string, port int, keyType string, fingerprint string, publicKey string) error {
|
|
now := time.Now()
|
|
kh := &models.KnownHost{
|
|
ID: uuid.New().String(),
|
|
Host: host,
|
|
Port: port,
|
|
KeyType: keyType,
|
|
Fingerprint: fingerprint,
|
|
PublicKey: publicKey,
|
|
FirstSeen: now,
|
|
LastSeen: now,
|
|
Trusted: true,
|
|
}
|
|
if err := s.storage.SaveKnownHost(kh); err != nil {
|
|
return fmt.Errorf("knownhosts: failed to accept key: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// List returns all stored known host entries.
|
|
func (s *Store) List() ([]*models.KnownHost, error) {
|
|
entries, err := s.storage.ListKnownHosts()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("knownhosts: failed to list entries: %w", err)
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
// Delete removes a known host entry by ID.
|
|
func (s *Store) Delete(id string) error {
|
|
if err := s.storage.DeleteKnownHost(id); err != nil {
|
|
return fmt.Errorf("knownhosts: failed to delete entry: %w", err)
|
|
}
|
|
return nil
|
|
}
|