niko_trust/pkg/identity/identity_test.go
Niko Marmeladkov 3bf13fa488 Public SDK packages, proxy-aware rate limits, service login recipe
- internal/{address,identity,protocol,tce,transport,verify} -> pkg/ so
  external Go projects can import the verified core; invariant tests
  updated for the new paths
- Config.TrustProxy: key rate limiting by X-Forwarded-For when the relay
  sits behind a reverse proxy (off by default, header never trusted
  otherwise)
- examples/service + examples/approve: complete passwordless login round
  trip (mint request -> wallet approves -> local verify), run live in CI
- docs/SERVICE-GUIDE.md: the integration recipe
2026-08-26 12:49:54 +03:00

346 lines
8.7 KiB
Go

package identity_test
import (
"crypto/ed25519"
"encoding/json"
"testing"
"git.n1ko.dev/Niko/niko_trust/pkg/address"
"git.n1ko.dev/Niko/niko_trust/pkg/identity"
"git.n1ko.dev/Niko/niko_trust/pkg/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")
}
}
}