diff --git a/crypto/verify_test.go b/crypto/verify_test.go new file mode 100644 --- /dev/null +++ b/crypto/verify_test.go @@ -0,0 +1,134 @@ +package crypto + +import ( + "bytes" + "crypto/ed25519" + "crypto/rand" + "strings" + "testing" + + "github.com/hiddeco/sshsig" + "golang.org/x/crypto/ssh" +) + +// testKey generates an ephemeral ed25519 key pair and returns the ssh.Signer +// and the public key in authorized_keys format. +func testKey(t *testing.T) (ssh.Signer, []byte) { + t.Helper() + _, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + t.Fatalf("generate ed25519 key: %v", err) + } + signer, err := ssh.NewSignerFromKey(priv) + if err != nil { + t.Fatalf("create signer: %v", err) + } + return signer, ssh.MarshalAuthorizedKey(signer.PublicKey()) +} + +// testSign signs payload with signer using the same parameters as VerifySignature +// expects (SHA-512, "git" namespace) and returns the armored signature. +func testSign(t *testing.T, signer ssh.Signer, payload []byte) []byte { + t.Helper() + sig, err := sshsig.Sign(bytes.NewReader(payload), signer, sshsig.HashSHA512, "git") + if err != nil { + t.Fatalf("sign payload: %v", err) + } + return sshsig.Armor(sig) +} + +func TestSSHFingerprint(t *testing.T) { + t.Run("valid key returns SHA256 fingerprint", func(t *testing.T) { + _, pubKeyBytes := testKey(t) + fp, err := SSHFingerprint(string(pubKeyBytes)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.HasPrefix(fp, "SHA256:") { + t.Errorf("fingerprint %q does not start with SHA256:", fp) + } + }) + + t.Run("same key returns identical fingerprint", func(t *testing.T) { + _, pubKeyBytes := testKey(t) + fp1, _ := SSHFingerprint(string(pubKeyBytes)) + fp2, _ := SSHFingerprint(string(pubKeyBytes)) + if fp1 != fp2 { + t.Errorf("fingerprint not deterministic: %q != %q", fp1, fp2) + } + }) + + t.Run("different keys return different fingerprints", func(t *testing.T) { + _, pub1 := testKey(t) + _, pub2 := testKey(t) + fp1, _ := SSHFingerprint(string(pub1)) + fp2, _ := SSHFingerprint(string(pub2)) + if fp1 == fp2 { + t.Error("different keys produced the same fingerprint") + } + }) + + t.Run("malformed key returns error", func(t *testing.T) { + _, err := SSHFingerprint("not a valid ssh public key") + if err == nil { + t.Error("expected error for malformed key") + } + }) +} + +func TestVerifySignature(t *testing.T) { + signer, pubKeyBytes := testKey(t) + payload := []byte("test payload") + armoredSig := testSign(t, signer, payload) + + t.Run("valid signature verifies successfully", func(t *testing.T) { + err, ok := VerifySignature(pubKeyBytes, armoredSig, payload) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !ok { + t.Error("expected ok=true for valid signature") + } + }) + + t.Run("malformed public key returns error", func(t *testing.T) { + err, ok := VerifySignature([]byte("not a valid key"), armoredSig, payload) + if err == nil { + t.Error("expected error for malformed public key") + } + if ok { + t.Error("expected ok=false") + } + }) + + t.Run("malformed signature returns error", func(t *testing.T) { + err, ok := VerifySignature(pubKeyBytes, []byte("not a valid signature"), payload) + if err == nil { + t.Error("expected error for malformed signature") + } + if ok { + t.Error("expected ok=false") + } + }) + + t.Run("tampered payload fails verification", func(t *testing.T) { + err, ok := VerifySignature(pubKeyBytes, armoredSig, []byte("tampered")) + if err == nil { + t.Error("expected error for tampered payload") + } + if ok { + t.Error("expected ok=false for tampered payload") + } + }) + + t.Run("wrong public key fails verification", func(t *testing.T) { + _, otherPubKey := testKey(t) + err, ok := VerifySignature(otherPubKey, armoredSig, payload) + if err == nil { + t.Error("expected error for wrong public key") + } + if ok { + t.Error("expected ok=false for wrong public key") + } + }) +}