- 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
203 lines
6.1 KiB
Go
203 lines
6.1 KiB
Go
package address_test
|
|
|
|
import (
|
|
"crypto/ed25519"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.n1ko.dev/Niko/niko_trust/pkg/address"
|
|
)
|
|
|
|
func seedCorpus(f *testing.F) {
|
|
f.Helper()
|
|
seed := make([]byte, ed25519.SeedSize)
|
|
for i := 0; i < 4; i++ {
|
|
for j := range seed {
|
|
seed[j] = byte(i*7 + j)
|
|
}
|
|
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
|
|
a, err := address.FromPubKey(pub)
|
|
if err != nil {
|
|
f.Fatal(err)
|
|
}
|
|
f.Add(a.String())
|
|
}
|
|
for _, s := range []string{
|
|
"",
|
|
"trust",
|
|
"trust1",
|
|
"trust11",
|
|
"trust1qqqqqqqq",
|
|
"TRUST1QQQQQQQQ",
|
|
"trust1qqak5faue6m2gttz5w5dq2n0p4ek2vs4wuw7ysax8tqy3gvtt8dzj0yfahr",
|
|
"trust1qqak5faue6m2gttz5w5dq2n0p4ek2vs4wuw7ysax8tqy3gvtt8dzj0yfahR",
|
|
"bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4",
|
|
"trust1\x00\xff",
|
|
"trust1" + strings.Repeat("q", 200),
|
|
"1qqqqqq",
|
|
"trust1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq",
|
|
"\x00\x00\x00\x00\x00\x00\x00\x00",
|
|
} {
|
|
f.Add(s)
|
|
}
|
|
}
|
|
|
|
// FuzzParse asserts that Parse is total: for arbitrary input it returns
|
|
// either a fully valid Address or an error, and never panics, hangs or
|
|
// allocates unboundedly.
|
|
//
|
|
// Every Address it does return must satisfy the package's invariants, which is
|
|
// what makes "a non-zero Address is always well formed" a safe assumption for
|
|
// the rest of the program.
|
|
func FuzzParse(f *testing.F) {
|
|
seedCorpus(f)
|
|
|
|
f.Fuzz(func(t *testing.T, s string) {
|
|
a, err := address.Parse(s)
|
|
if err != nil {
|
|
if !a.IsZero() {
|
|
t.Fatalf("Parse returned an address alongside an error")
|
|
}
|
|
return
|
|
}
|
|
|
|
// Property 1: an accepted address is exactly the input. If Parse ever
|
|
// normalised its input, two strings would denote one identity and
|
|
// INV-8 would be violated.
|
|
if a.String() != s {
|
|
t.Fatalf("Parse normalised input: got %q, input %q", a.String(), s)
|
|
}
|
|
|
|
// Property 2: the key inside is always valid.
|
|
if err := address.ValidatePubKey(a.PubKey()); err != nil {
|
|
t.Fatalf("accepted address carries an invalid key: %v", err)
|
|
}
|
|
|
|
// Property 3: re-encoding the recovered key reproduces the address
|
|
// byte for byte. Encode and Decode are inverse on the accepted set.
|
|
re, err := address.FromPubKey(a.PubKey())
|
|
if err != nil {
|
|
t.Fatalf("could not re-encode a key recovered from a valid address: %v", err)
|
|
}
|
|
if re.String() != s {
|
|
t.Fatalf("encode(decode(x)) != x:\n got %q\nwant %q", re.String(), s)
|
|
}
|
|
|
|
// Property 4: parsing is idempotent and stable.
|
|
again, err := address.Parse(a.String())
|
|
if err != nil || !again.Equal(a) {
|
|
t.Fatalf("Parse is not idempotent: %v", err)
|
|
}
|
|
|
|
// Property 5: shape guarantees the rest of the code may rely on.
|
|
if len(s) != address.EncodedLen {
|
|
t.Fatalf("accepted address of length %d, want %d", len(s), address.EncodedLen)
|
|
}
|
|
if !strings.HasPrefix(s, address.HRP+"1") {
|
|
t.Fatalf("accepted address without the trust1 prefix")
|
|
}
|
|
if strings.ToLower(s) != s {
|
|
t.Fatalf("accepted a non-lowercase address")
|
|
}
|
|
if a.Version() != address.Version0 {
|
|
t.Fatalf("accepted unknown version %d", a.Version())
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzRoundTrip asserts the property in the other direction: every valid key
|
|
// encodes to a string that parses back to the same key.
|
|
func FuzzRoundTrip(f *testing.F) {
|
|
f.Add(make([]byte, ed25519.SeedSize))
|
|
f.Add([]byte("0123456789abcdef0123456789abcdef"))
|
|
f.Fuzz(func(t *testing.T, seed []byte) {
|
|
if len(seed) != ed25519.SeedSize {
|
|
return
|
|
}
|
|
pub := ed25519.NewKeyFromSeed(seed).Public().(ed25519.PublicKey)
|
|
|
|
a, err := address.FromPubKey(pub)
|
|
if err != nil {
|
|
// Only a degenerate key may be refused, and a key derived from a
|
|
// seed through the standard construction is never degenerate.
|
|
t.Fatalf("valid derived key rejected: %v", err)
|
|
}
|
|
back, err := address.Parse(a.String())
|
|
if err != nil {
|
|
t.Fatalf("own output rejected by Parse: %v", err)
|
|
}
|
|
if !ed25519.PublicKey(back.PubKey()).Equal(pub) {
|
|
t.Fatalf("key not preserved through the round trip")
|
|
}
|
|
if back.KeyBytes() != a.KeyBytes() {
|
|
t.Fatalf("KeyBytes differ after round trip")
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzValidatePubKey asserts that key validation is total and agrees with
|
|
// itself: it never panics on arbitrary bytes, and a key it accepts is always
|
|
// canonical, meaning it survives an encode/decode cycle unchanged.
|
|
func FuzzValidatePubKey(f *testing.F) {
|
|
f.Add(make([]byte, 32))
|
|
f.Add(make([]byte, 31))
|
|
f.Add([]byte{})
|
|
pub, _, _ := ed25519.GenerateKey(nil)
|
|
f.Add([]byte(pub))
|
|
|
|
f.Fuzz(func(t *testing.T, key []byte) {
|
|
err := address.ValidatePubKey(key)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(key) != ed25519.PublicKeySize {
|
|
t.Fatalf("accepted a key of length %d", len(key))
|
|
}
|
|
|
|
// An accepted key must be usable as an address and recoverable.
|
|
a, aerr := address.FromPubKey(key)
|
|
if aerr != nil {
|
|
t.Fatalf("ValidatePubKey accepted a key FromPubKey rejected: %v", aerr)
|
|
}
|
|
got := a.PubKey()
|
|
for i := range key {
|
|
if key[i] != got[i] {
|
|
t.Fatalf("key altered by the address round trip at byte %d", i)
|
|
}
|
|
}
|
|
|
|
// Validation must be deterministic.
|
|
if err2 := address.ValidatePubKey(key); err2 != nil {
|
|
t.Fatalf("validation is not deterministic: %v", err2)
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzNoAliasingBetweenAddresses asserts the injectivity property that matters
|
|
// for delivery: two different accepted addresses never denote the same key.
|
|
// If they could, an approval request addressed to one identity could be
|
|
// answered by another.
|
|
func FuzzNoAliasingBetweenAddresses(f *testing.F) {
|
|
a1, _ := address.FromPubKey(ed25519.NewKeyFromSeed(make([]byte, 32)).Public().(ed25519.PublicKey))
|
|
seed2 := make([]byte, 32)
|
|
seed2[0] = 1
|
|
a2, _ := address.FromPubKey(ed25519.NewKeyFromSeed(seed2).Public().(ed25519.PublicKey))
|
|
f.Add(a1.String(), a2.String())
|
|
|
|
f.Fuzz(func(t *testing.T, s1, s2 string) {
|
|
x, err1 := address.Parse(s1)
|
|
y, err2 := address.Parse(s2)
|
|
if err1 != nil || err2 != nil {
|
|
return
|
|
}
|
|
sameString := s1 == s2
|
|
sameKey := x.KeyBytes() == y.KeyBytes()
|
|
if sameKey != sameString {
|
|
t.Fatalf("address/key correspondence is not one to one:\n%q\n%q\nsameKey=%v sameString=%v",
|
|
s1, s2, sameKey, sameString)
|
|
}
|
|
if x.Equal(y) != sameString {
|
|
t.Fatalf("Equal disagrees with string identity")
|
|
}
|
|
})
|
|
}
|