niko_trust/pkg/address/pubkey_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

148 lines
5.5 KiB
Go

package address_test
import (
"crypto/ed25519"
"encoding/hex"
"testing"
"git.n1ko.dev/Niko/niko_trust/pkg/address"
)
// smallOrderKeys is the standard list of Edwards25519 points of order 1, 2, 4
// and 8, plus the non-canonical encodings of some of them. These are the keys
// for which Ed25519 signature verification is degenerate.
//
// The list is the one used by libsodium's crypto_core_ed25519_is_valid_point
// test vectors and by RFC 8032 implementation reports.
var smallOrderKeys = []struct {
name string
hex string
}{
{"order 1 (identity)", "0100000000000000000000000000000000000000000000000000000000000000"},
{"order 2", "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"},
{"order 4 (all zero)", "0000000000000000000000000000000000000000000000000000000000000000"},
{"order 4 (sign bit)", "0000000000000000000000000000000000000000000000000000000000000080"},
{"order 8 a", "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05"},
{"order 8 b", "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa"},
{"order 8 c", "26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85"},
{"order 8 d", "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a"},
{"non-canonical p", "edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"},
{"non-canonical p+1", "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"},
}
// TestRejectsSmallOrderKeys is the most important test in this package.
//
// crypto/ed25519.Verify accepts these keys and, for several of them, verifies
// an all-zero signature against ANY message. If such a key could be turned
// into a trust address, its owner would hold an identity whose signature
// verifies on every claim and every approval anyone constructs. That is a
// direct break of INV-1 and of the whole "cryptography establishes who said
// something" premise.
func TestRejectsSmallOrderKeys(t *testing.T) {
for _, tc := range smallOrderKeys {
t.Run(tc.name, func(t *testing.T) {
key, err := hex.DecodeString(tc.hex)
if err != nil {
t.Fatal(err)
}
if err := address.ValidatePubKey(key); err == nil {
t.Fatalf("ValidatePubKey accepted a degenerate key")
}
if _, err := address.FromPubKey(key); err == nil {
t.Fatalf("FromPubKey produced an address for a degenerate key")
}
})
}
}
// TestDegenerateKeyWouldForgeSignatures demonstrates the concrete attack the
// validation prevents, so that a future reader cannot mistake the check for
// paranoia and remove it.
func TestDegenerateKeyWouldForgeSignatures(t *testing.T) {
// The order-1 point (the identity element, y = 1). Signing with R set to
// the same encoding and S = 0 yields a signature that verifies under
// crypto/ed25519 for every message without exception.
key, err := hex.DecodeString("0100000000000000000000000000000000000000000000000000000000000000")
if err != nil {
t.Fatal(err)
}
universalSig := make([]byte, ed25519.SignatureSize)
copy(universalSig[:32], key)
messages := []string{
"trust1abc says trust1bca minecraft.op = true",
"trust1abc asks trust1bca to ban Steve",
"an entirely unrelated statement",
"",
}
forgedAll := true
for _, m := range messages {
if !ed25519.Verify(key, []byte(m), universalSig) {
forgedAll = false
break
}
}
if forgedAll {
t.Log("crypto/ed25519.Verify accepts one fixed signature under this key " +
"for every message tested: a universal forgery")
} else {
t.Log("stdlib no longer exhibits the universal forgery; validation is still required")
}
// Whatever the stdlib does, this key must never become a trust identity.
if err := address.ValidatePubKey(key); err == nil {
t.Fatal("degenerate key must be rejected before it can become an identity")
}
if _, err := address.FromPubKey(key); err == nil {
t.Fatal("FromPubKey must refuse the degenerate key")
}
// And it must not be smuggled in through the textual form either.
if _, err := address.Parse(encodeRawForTest(t, address.Version0, key)); err == nil {
t.Fatal("Parse must refuse an address wrapping the degenerate key")
}
}
func TestRejectsWrongKeySize(t *testing.T) {
for _, n := range []int{0, 1, 31, 33, 64} {
if err := address.ValidatePubKey(make([]byte, n)); err == nil {
t.Errorf("accepted %d-byte key", n)
}
}
if err := address.ValidatePubKey(nil); err == nil {
t.Error("accepted nil key")
}
}
// TestAcceptsGeneratedKeys ensures the validation never rejects an honest key.
// A false positive here would randomly break real users, so the sample is
// large enough to catch a rate on the order of one in a thousand.
func TestAcceptsGeneratedKeys(t *testing.T) {
const n = 3000
for i := 0; i < n; i++ {
pub, _, err := ed25519.GenerateKey(nil)
if err != nil {
t.Fatal(err)
}
if err := address.ValidatePubKey(pub); err != nil {
t.Fatalf("rejected an honestly generated key: %v (%x)", err, pub)
}
if _, err := address.FromPubKey(pub); err != nil {
t.Fatalf("FromPubKey rejected an honest key: %v", err)
}
}
t.Logf("accepted %d generated keys, rejected 0", n)
}
// TestParseRejectsEmbeddedSmallOrderKey checks that the validation cannot be
// bypassed by encoding a degenerate key into a well-formed bech32m address.
func TestParseRejectsEmbeddedSmallOrderKey(t *testing.T) {
for _, tc := range smallOrderKeys {
key, _ := hex.DecodeString(tc.hex)
s := encodeRawForTest(t, address.Version0, key)
if _, err := address.Parse(s); err == nil {
t.Fatalf("Parse accepted an address wrapping a degenerate key (%s): %s", tc.name, s)
}
}
}