- 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
346 lines
8.7 KiB
Go
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")
|
|
}
|
|
}
|
|
}
|