package sign import ( "crypto/ed25519" "encoding/json" "errors" "testing" ) func mustKey(t *testing.T) (ed25519.PublicKey, ed25519.PrivateKey) { t.Helper() pub, priv, err := GenerateKey() if err != nil { t.Fatalf("GenerateKey: %v", err) } return pub, priv } func TestSignVerifyRoundTrip(t *testing.T) { pub, priv := mustKey(t) env := Envelope{ Version: 1, IssuedAt: "2026-06-13T00:00:00Z", KeyID: "k1", Payload: json.RawMessage(`{"api_domains":["a.example.com"]}`), } if err := Sign(priv, &env); err != nil { t.Fatalf("Sign: %v", err) } if env.Sig == "" { t.Fatal("signature not set") } if err := Verify(env, KeyRing{"k1": pub}); err != nil { t.Fatalf("Verify: %v", err) } } func TestVerifyRejectsTamperedPayload(t *testing.T) { pub, priv := mustKey(t) env := Envelope{Version: 1, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{"x":1}`)} if err := Sign(priv, &env); err != nil { t.Fatal(err) } env.Payload = json.RawMessage(`{"x":2}`) // tamper after signing if err := Verify(env, KeyRing{"k1": pub}); !errors.Is(err, ErrBadSignature) { t.Fatalf("want ErrBadSignature, got %v", err) } } func TestVerifyRejectsVersionTamper(t *testing.T) { pub, priv := mustKey(t) env := Envelope{Version: 5, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{"x":1}`)} if err := Sign(priv, &env); err != nil { t.Fatal(err) } env.Version = 99 // attacker tries to inflate version if err := Verify(env, KeyRing{"k1": pub}); !errors.Is(err, ErrBadSignature) { t.Fatalf("want ErrBadSignature, got %v", err) } } func TestVerifyUnknownKeyID(t *testing.T) { pub, priv := mustKey(t) env := Envelope{Version: 1, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{}`)} if err := Sign(priv, &env); err != nil { t.Fatal(err) } if err := Verify(env, KeyRing{"other": pub}); !errors.Is(err, ErrUnknownKeyID) { t.Fatalf("want ErrUnknownKeyID, got %v", err) } } func TestKeyRotationDoublePublicKey(t *testing.T) { oldPub, _ := mustKey(t) newPub, newPriv := mustKey(t) // Document signed with the NEW key, key_id "v2". env := Envelope{Version: 1, IssuedAt: "t", KeyID: "v2", Payload: json.RawMessage(`{}`)} if err := Sign(newPriv, &env); err != nil { t.Fatal(err) } // During the rotation window the client holds BOTH public keys. ring := KeyRing{"v1": oldPub, "v2": newPub} if err := Verify(env, ring); err != nil { t.Fatalf("rotation window verify failed: %v", err) } // A client that only has the old key must reject it. if err := Verify(env, KeyRing{"v1": oldPub}); !errors.Is(err, ErrUnknownKeyID) { t.Fatalf("want ErrUnknownKeyID for old-only ring, got %v", err) } } func TestCanonicalizeFieldOrderInvariant(t *testing.T) { _, priv := mustKey(t) // Same logical payload, different key order -> identical signature. envA := Envelope{Version: 1, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{"a":1,"b":2}`)} envB := Envelope{Version: 1, IssuedAt: "t", KeyID: "k1", Payload: json.RawMessage(`{"b":2,"a":1}`)} if err := Sign(priv, &envA); err != nil { t.Fatal(err) } if err := Sign(priv, &envB); err != nil { t.Fatal(err) } if envA.Sig != envB.Sig { t.Fatalf("canonicalization not order-invariant:\n A=%s\n B=%s", envA.Sig, envB.Sig) } } func TestCanonicalizePreservesIntegers(t *testing.T) { out, err := Canonicalize([]byte(`{"v":12345678901234567}`)) if err != nil { t.Fatal(err) } if string(out) != `{"v":12345678901234567}` { t.Fatalf("integer not preserved: %s", out) } } func TestParseKeyRing(t *testing.T) { pub, _ := mustKey(t) ring, err := ParseKeyRing([]string{"k1=" + EncodePublic(pub)}) if err != nil { t.Fatal(err) } if _, ok := ring["k1"]; !ok { t.Fatal("k1 missing from ring") } if _, err := ParseKeyRing([]string{"bad"}); err == nil { t.Fatal("want error for malformed spec") } } func TestDecodePrivateRoundTrip(t *testing.T) { _, priv := mustKey(t) got, err := DecodePrivate(EncodePrivate(priv)) if err != nil { t.Fatal(err) } if !got.Equal(priv) { t.Fatal("private key round-trip mismatch") } }