package identity_test import ( "crypto/ed25519" "encoding/json" "testing" "git.n1ko.dev/Niko/niko_trust/internal/address" "git.n1ko.dev/Niko/niko_trust/internal/identity" "git.n1ko.dev/Niko/niko_trust/internal/identity/signer" ) func mustSigner(t *testing.T) *signer.Signer { t.Helper() s, err := signer.Generate() if err != nil { t.Fatal(err) } return s } func TestGenerateProducesUsableIdentity(t *testing.T) { s := mustSigner(t) id := s.Identity() if id.IsZero() { t.Fatal("generated identity is zero") } if _, err := identity.Parse(id.String()); err != nil { t.Fatalf("generated address does not parse: %v", err) } if err := address.ValidatePubKey(id.PubKey()); err != nil { t.Fatalf("generated key fails validation: %v", err) } if !id.Address().Equal(s.Address()) { t.Fatal("signer address and identity address disagree") } } func TestGenerateIsUnique(t *testing.T) { seen := make(map[string]bool) for i := 0; i < 200; i++ { s := mustSigner(t) addr := s.Identity().String() if seen[addr] { t.Fatalf("duplicate identity generated: %s", addr) } seen[addr] = true } } func TestSignAndVerify(t *testing.T) { s := mustSigner(t) id := s.Identity() msg := []byte("canonical protocol bytes") sig := s.Sign(msg) if len(sig) != identity.SignatureSize { t.Fatalf("signature size = %d", len(sig)) } if !id.Verify(msg, sig) { t.Fatal("valid signature did not verify") } if err := id.VerifyErr(msg, sig); err != nil { t.Fatalf("VerifyErr on a valid signature: %v", err) } } // TestVerifyRejectsTampering covers the modification cases in the threat // model: a changed message, a changed signature, and a signature made by a // different identity. func TestVerifyRejectsTampering(t *testing.T) { s := mustSigner(t) other := mustSigner(t) id := s.Identity() msg := []byte("trust1abc says trust1bca minecraft.op = true") sig := s.Sign(msg) t.Run("modified message", func(t *testing.T) { for i := range msg { bad := make([]byte, len(msg)) copy(bad, msg) bad[i] ^= 0x01 if id.Verify(bad, sig) { t.Fatalf("verified a message modified at byte %d", i) } } }) t.Run("modified signature", func(t *testing.T) { for i := range sig { bad := make([]byte, len(sig)) copy(bad, sig) bad[i] ^= 0x01 if id.Verify(msg, bad) { t.Fatalf("verified a signature modified at byte %d", i) } } }) t.Run("wrong signer", func(t *testing.T) { if other.Identity().Verify(msg, sig) { t.Fatal("a signature verified under the wrong identity") } if id.Verify(msg, other.Sign(msg)) { t.Fatal("another identity's signature verified under this one") } }) t.Run("truncated and extended", func(t *testing.T) { if id.Verify(msg, sig[:len(sig)-1]) { t.Fatal("verified a truncated signature") } if id.Verify(msg, append(append([]byte{}, sig...), 0)) { t.Fatal("verified an over-long signature") } if id.Verify(msg, nil) { t.Fatal("verified a nil signature") } if id.Verify(msg, []byte{}) { t.Fatal("verified an empty signature") } }) t.Run("empty message is still bound", func(t *testing.T) { empty := s.Sign(nil) if !id.Verify(nil, empty) { t.Fatal("signature over an empty message did not verify") } if id.Verify([]byte("x"), empty) { t.Fatal("empty-message signature verified over other content") } }) } // TestZeroIdentityVerifiesNothing ensures an uninitialised Identity cannot be // used to accept data. Fail-closed behaviour matters because a zero value is // what a struct field holds when a caller forgets to populate it. func TestZeroIdentityVerifiesNothing(t *testing.T) { var zero identity.Identity s := mustSigner(t) msg := []byte("m") if !zero.IsZero() { t.Fatal("zero value must report IsZero") } if zero.Verify(msg, s.Sign(msg)) { t.Fatal("the zero identity verified a signature") } if zero.Verify(nil, nil) { t.Fatal("the zero identity verified empty input") } if err := zero.VerifyErr(msg, s.Sign(msg)); err == nil { t.Fatal("VerifyErr on the zero identity returned nil") } if zero.Equal(zero) { t.Fatal("the zero identity must not equal itself") } } func TestEqualIgnoresAlias(t *testing.T) { s := mustSigner(t) a, err := identity.FromPubKey(s.Public()) if err != nil { t.Fatal(err) } b, err := identity.Parse(s.Identity().String()) if err != nil { t.Fatal(err) } if !a.Equal(b) { t.Fatal("the same key must produce equal identities") } if !a.Equal(s.Identity()) { t.Fatal("identity from signer differs from identity from key") } if a.Equal(mustSigner(t).Identity()) { t.Fatal("different keys must not be equal") } } func TestFromPubKeyRejectsBadKeys(t *testing.T) { if _, err := identity.FromPubKey(make([]byte, 32)); err == nil { t.Fatal("accepted the all-zero key") } if _, err := identity.FromPubKey(make([]byte, 31)); err == nil { t.Fatal("accepted a short key") } if _, err := identity.FromPubKey(nil); err == nil { t.Fatal("accepted a nil key") } } func TestJSONRoundTrip(t *testing.T) { s := mustSigner(t) id := s.Identity() type envelope struct { Issuer identity.Identity `json:"issuer"` } b, err := json.Marshal(envelope{Issuer: id}) if err != nil { t.Fatal(err) } var back envelope if err := json.Unmarshal(b, &back); err != nil { t.Fatal(err) } if !back.Issuer.Equal(id) { t.Fatal("identity did not survive a JSON round trip") } // Decoding must validate, so a hostile document cannot inject a // degenerate key through the JSON path. var bad envelope if err := json.Unmarshal([]byte(`{"issuer":"trust1qqqqqqqqqqqqqqqq"}`), &bad); err == nil { t.Fatal("unmarshal accepted an invalid address") } } func TestPubKeyIsCopied(t *testing.T) { s := mustSigner(t) id := s.Identity() k := id.PubKey() for i := range k { k[i] = 0 } if err := address.ValidatePubKey(id.PubKey()); err != nil { t.Fatalf("identity key was mutated through a returned copy: %v", err) } } func TestSignerFromSeedAndPrivateKey(t *testing.T) { orig := mustSigner(t) seed := orig.Seed() fromSeed, err := signer.FromSeed(seed) if err != nil { t.Fatal(err) } if !fromSeed.Identity().Equal(orig.Identity()) { t.Fatal("seed did not restore the same identity") } fromPriv, err := signer.FromPrivateKey(orig.PrivateKey()) if err != nil { t.Fatal(err) } if !fromPriv.Identity().Equal(orig.Identity()) { t.Fatal("private key did not restore the same identity") } // Signatures from a restored key must verify under the original identity. msg := []byte("restored") if !orig.Identity().Verify(msg, fromSeed.Sign(msg)) { t.Fatal("signature from a seed-restored key did not verify") } for _, n := range []int{0, 31, 33, 64} { if _, err := signer.FromSeed(make([]byte, n)); err == nil { t.Errorf("FromSeed accepted a %d-byte seed", n) } } for _, n := range []int{0, 32, 63, 65} { if _, err := signer.FromPrivateKey(make([]byte, n)); err == nil { t.Errorf("FromPrivateKey accepted a %d-byte key", n) } } } // TestFromPrivateKeyRejectsInconsistentKey covers a tampered key file whose // public half no longer matches its seed. Such a key would sign under one // identity while claiming another. func TestFromPrivateKeyRejectsInconsistentKey(t *testing.T) { a := mustSigner(t) b := mustSigner(t) frankenstein := make(ed25519.PrivateKey, ed25519.PrivateKeySize) copy(frankenstein[:32], a.PrivateKey()[:32]) // seed from a copy(frankenstein[32:], b.Public()) // public half from b if _, err := signer.FromPrivateKey(frankenstein); err == nil { t.Fatal("accepted a private key whose public half does not match its seed") } } func TestSeedAndPrivateKeyAreCopies(t *testing.T) { s := mustSigner(t) id := s.Identity() seed := s.Seed() for i := range seed { seed[i] = 0 } priv := s.PrivateKey() for i := range priv { priv[i] = 0 } msg := []byte("still works") if !id.Verify(msg, s.Sign(msg)) { t.Fatal("signer was corrupted by a caller mutating returned key material") } } func TestGenerateFromDeterministicSource(t *testing.T) { src := func() *zeroReader { return &zeroReader{} } a, err := signer.GenerateFrom(src()) if err != nil { t.Fatal(err) } b, err := signer.GenerateFrom(src()) if err != nil { t.Fatal(err) } if !a.Identity().Equal(b.Identity()) { t.Fatal("the same entropy must yield the same identity") } if a.Identity().Equal(mustSigner(t).Identity()) { t.Fatal("a random identity collided with the deterministic one") } } type zeroReader struct{} func (z *zeroReader) Read(p []byte) (int, error) { for i := range p { p[i] = 0 } return len(p), nil } func BenchmarkVerify(b *testing.B) { s, _ := signer.Generate() id := s.Identity() msg := []byte("canonical protocol bytes for benchmarking") sig := s.Sign(msg) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { if !id.Verify(msg, sig) { b.Fatal("verification failed") } } }