- 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
148 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|