diff --git a/internal/security/sshkey.go b/internal/security/sshkey.go index 30abea1..5af34a5 100644 --- a/internal/security/sshkey.go +++ b/internal/security/sshkey.go @@ -26,6 +26,17 @@ const ( sshPubFile = "orca_ssh_key.pub" ) +// SSHFingerprintSHA256 returns the canonical SSH public-key fingerprint +// in the form `SHA256:base64` (no trailing padding), as produced by +// `ssh-keygen -lf` and OpenSSH's host-key verification prompts. This is +// a thin wrapper over ssh.FingerprintSHA256 (AD-027) for use by the +// proxmox bootstrap pinned-host-key callback (REQ-058) and any other +// SSH-domain identity checks. Do NOT reuse security.Fingerprint — that +// returns an X.509 DER hex digest (different domain; RESEARCH §2.2). +func SSHFingerprintSHA256(pubKey ssh.PublicKey) string { + return ssh.FingerprintSHA256(pubKey) +} + // GenerateOrLoadSSHKey returns the orca SSH keypair, generating it // lazily on first call (D-037). The key is Ed25519 (smaller, faster, // more secure than RSA for SSH auth), persisted as PKCS8 PEM to diff --git a/internal/security/sshkey_test.go b/internal/security/sshkey_test.go index 2cb6b53..60dd6e4 100644 --- a/internal/security/sshkey_test.go +++ b/internal/security/sshkey_test.go @@ -1,6 +1,8 @@ package security import ( + "crypto/ed25519" + "crypto/rand" "os" "path/filepath" "strings" @@ -91,3 +93,43 @@ func TestGenerateOrLoadSSHKey_CreatesDir(t *testing.T) { t.Errorf("nested dir not created: %v", err) } } + +func TestSSHFingerprintSHA256_Ed25519(t *testing.T) { + pub, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("ed25519 gen: %v", err) + } + sshPub, err := ssh.NewPublicKey(pub) + if err != nil { + t.Fatalf("new pubkey: %v", err) + } + + got := SSHFingerprintSHA256(sshPub) + + // Canonical form: SHA256: followed by unpadded base64. + if !strings.HasPrefix(got, "SHA256:") { + t.Fatalf("fingerprint = %q, want SHA256: prefix", got) + } + // Must match the reference implementation exactly. + want := ssh.FingerprintSHA256(sshPub) + if got != want { + t.Errorf("SSHFingerprintSHA256 = %q, want %q", got, want) + } +} + +func TestSSHFingerprintSHA256_StableAcrossCalls(t *testing.T) { + pub, _, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("ed25519 gen: %v", err) + } + sshPub, err := ssh.NewPublicKey(pub) + if err != nil { + t.Fatalf("new pubkey: %v", err) + } + + a := SSHFingerprintSHA256(sshPub) + b := SSHFingerprintSHA256(sshPub) + if a != b { + t.Errorf("fingerprint not stable: %q vs %q", a, b) + } +}