package signing import ( "crypto/ed25519" "errors" "testing" ) type doc struct { Revision struct { ID string `json:"id"` State string `json:"state"` Upstream string `json:"upstream"` Signature *Signature `json:"signature,omitempty"` } `json:"revision"` } func newDoc(id, state string) doc { var d doc d.Revision.ID = id d.Revision.State = state d.Revision.Upstream = "http://adapter:8080" return d } func TestSignAndVerify(t *testing.T) { signer, pub, err := GenerateKey("key-1") if err != nil { t.Fatal(err) } v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub}) d := newDoc("R-1", "stable") sig, err := signer.Sign(d) if err != nil { t.Fatal(err) } if err := v.Verify(d, &sig); err != nil { t.Fatalf("freshly signed document does not verify: %v", err) } } // TestSignatureCoversContent is the property the whole barrier depends on: a // tampered descriptor must not verify. func TestSignatureCoversContent(t *testing.T) { signer, pub, _ := GenerateKey("key-1") v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub}) d := newDoc("R-1", "experiment") sig, _ := signer.Sign(d) // Promoting a signed experiment to stable by editing the descriptor is // exactly the attack the signature exists to stop. tampered := d tampered.Revision.State = "stable" if err := v.Verify(tampered, &sig); !errors.Is(err, ErrBadSignature) { t.Errorf("a tampered descriptor verified: %v", err) } redirected := d redirected.Revision.Upstream = "http://attacker:8080" if err := v.Verify(redirected, &sig); !errors.Is(err, ErrBadSignature) { t.Errorf("a redirected upstream verified: %v", err) } } // TestSignatureMemberIsExcluded checks that a document signs its content and // not its own signature, so a signed document round-trips. func TestSignatureMemberIsExcluded(t *testing.T) { signer, pub, _ := GenerateKey("key-1") v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub}) d := newDoc("R-1", "stable") sig, _ := signer.Sign(d) attached := d attached.Revision.Signature = &sig if err := v.Verify(attached, &sig); err != nil { t.Fatalf("a document carrying its own signature does not verify: %v", err) } } func TestUnsignedIsRefused(t *testing.T) { _, pub, _ := GenerateKey("key-1") v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub}) // A verifier that passes unsigned input is worse than no verifier, because // it reports success. if err := v.Verify(newDoc("R-1", "stable"), nil); !errors.Is(err, ErrUnsigned) { t.Errorf("unsigned document returned %v, want ErrUnsigned", err) } } func TestUntrustedKeyIsRefused(t *testing.T) { rogue, _, _ := GenerateKey("rogue-key") _, trustedPub, _ := GenerateKey("key-1") v := NewVerifier(map[string]ed25519.PublicKey{"key-1": trustedPub}) d := newDoc("R-1", "stable") sig, _ := rogue.Sign(d) if err := v.Verify(d, &sig); !errors.Is(err, ErrUnknownKey) { t.Errorf("a signature from an untrusted key returned %v, want ErrUnknownKey", err) } } func TestAlgorithmCannotBeDowngraded(t *testing.T) { signer, pub, _ := GenerateKey("key-1") v := NewVerifier(map[string]ed25519.PublicKey{"key-1": pub}) d := newDoc("R-1", "stable") sig, _ := signer.Sign(d) sig.Algorithm = "none" if err := v.Verify(d, &sig); !errors.Is(err, ErrUnsupportedAlgorithm) { t.Errorf("an algorithm downgrade returned %v, want ErrUnsupportedAlgorithm", err) } } // TestCanonicalizationIsStable matters because an adapter or Daimon in another // language must be able to produce a signature this verifier accepts. func TestCanonicalizationIsStable(t *testing.T) { d := newDoc("R-1", "stable") first, err := Canonicalize(d) if err != nil { t.Fatal(err) } for i := 0; i < 100; i++ { again, err := Canonicalize(d) if err != nil { t.Fatal(err) } if string(again) != string(first) { t.Fatalf("canonical form varied between runs:\n%s\n%s", first, again) } } // Key order in the source must not change the canonical bytes. fromMap := map[string]any{ "revision": map[string]any{ "upstream": "http://adapter:8080", "state": "stable", "id": "R-1", }, } mapForm, err := Canonicalize(fromMap) if err != nil { t.Fatal(err) } if string(mapForm) != string(first) { t.Errorf("canonical form depends on key order:\n%s\n%s", first, mapForm) } } func TestNewSignerRejectsBadInput(t *testing.T) { _, priv, _ := ed25519.GenerateKey(nil) if _, err := NewSigner("", priv); err == nil { t.Error("a signer with no key id was accepted") } if _, err := NewSigner("key-1", ed25519.PrivateKey("short")); err == nil { t.Error("a malformed private key was accepted") } }