- 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
176 lines
5.5 KiB
Go
176 lines
5.5 KiB
Go
package protocol_test
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
|
|
"git.n1ko.dev/Niko/niko_trust/pkg/identity/signer"
|
|
"git.n1ko.dev/Niko/niko_trust/pkg/protocol"
|
|
"git.n1ko.dev/Niko/niko_trust/pkg/tce"
|
|
)
|
|
|
|
// loadVectorsF is loadVectors for the seed-corpus phase of a fuzz test,
|
|
// where the failure controller is a *testing.F.
|
|
func loadVectorsF(f *testing.F) *vectorFile {
|
|
b, err := os.ReadFile(vectorsPath)
|
|
if err != nil {
|
|
f.Fatalf("read vectors: %v", err)
|
|
}
|
|
var vf vectorFile
|
|
if err := json.Unmarshal(b, &vf); err != nil {
|
|
f.Fatalf("parse vectors: %v", err)
|
|
}
|
|
return &vf
|
|
}
|
|
|
|
// FuzzDecodeIsTotalAndNonMalleable asserts the two injection properties of
|
|
// PROTOCOL.md section 12.4 against arbitrary byte strings:
|
|
//
|
|
// - the decoder never panics, hangs or returns a usable object alongside an
|
|
// error (totality);
|
|
// - any input that does decode is exactly the canonical encoding of the
|
|
// decoded object: re-encoding reproduces the input byte for byte
|
|
// (encode(decode(b)) == b). If that failed, two byte strings would denote
|
|
// one object and a signature could be transplanted.
|
|
func FuzzDecodeIsTotalAndNonMalleable(f *testing.F) {
|
|
vf := loadVectorsF(f)
|
|
for _, v := range vf.Vectors {
|
|
b, err := hex.DecodeString(v.TCEHex)
|
|
if err != nil {
|
|
f.Fatalf("corpus hex: %v", err)
|
|
}
|
|
f.Add(b)
|
|
}
|
|
f.Add([]byte(nil))
|
|
f.Add([]byte{0x74})
|
|
|
|
decode := []struct {
|
|
name string
|
|
parse func([]byte) (interface{ TCE() []byte }, error)
|
|
}{
|
|
{"identity", func(b []byte) (interface{ TCE() []byte }, error) { return protocol.DecodeIdentity(b) }},
|
|
{"claim", func(b []byte) (interface{ TCE() []byte }, error) { return protocol.DecodeClaim(b) }},
|
|
{"revocation", func(b []byte) (interface{ TCE() []byte }, error) { return protocol.DecodeRevocation(b) }},
|
|
{"request", func(b []byte) (interface{ TCE() []byte }, error) { return protocol.DecodeApprovalRequest(b) }},
|
|
{"response", func(b []byte) (interface{ TCE() []byte }, error) { return protocol.DecodeApprovalResponse(b) }},
|
|
{"assertion", func(b []byte) (interface{ TCE() []byte }, error) { return protocol.DecodeAuthAssertion(b) }},
|
|
}
|
|
encode := map[string]func(interface{ TCE() []byte }) ([]byte, error){
|
|
"identity": func(o interface{ TCE() []byte }) ([]byte, error) {
|
|
return protocol.EncodeIdentity(o.(*protocol.Identity))
|
|
},
|
|
"claim": func(o interface{ TCE() []byte }) ([]byte, error) { return protocol.EncodeClaim(o.(*protocol.Claim)) },
|
|
"revocation": func(o interface{ TCE() []byte }) ([]byte, error) {
|
|
return protocol.EncodeRevocation(o.(*protocol.Revocation))
|
|
},
|
|
"request": func(o interface{ TCE() []byte }) ([]byte, error) {
|
|
return protocol.EncodeApprovalRequest(o.(*protocol.ApprovalRequest))
|
|
},
|
|
"response": func(o interface{ TCE() []byte }) ([]byte, error) {
|
|
return protocol.EncodeApprovalResponse(o.(*protocol.ApprovalResponse))
|
|
},
|
|
"assertion": func(o interface{ TCE() []byte }) ([]byte, error) {
|
|
return protocol.EncodeAuthAssertion(o.(*protocol.AuthAssertion))
|
|
},
|
|
}
|
|
|
|
f.Fuzz(func(t *testing.T, b []byte) {
|
|
for _, d := range decode {
|
|
obj, err := d.parse(b)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if obj == nil {
|
|
t.Fatalf("nil interface from %s", d.name)
|
|
}
|
|
rv := reflect.ValueOf(obj)
|
|
if rv.Kind() == reflect.Ptr && rv.IsNil() {
|
|
t.Fatalf("boxed-nil *%s from %s", rv.Type().Elem(), d.name)
|
|
}
|
|
out, err := encode[d.name](obj)
|
|
if err != nil {
|
|
t.Fatalf("%s: encoder rejected the decoder's own object: %v", d.name, err)
|
|
}
|
|
if !eqBytes(out, b) {
|
|
t.Fatalf("%s: encode(decode(b)) != b:\n got %x\n in %x", d.name, out, b)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// FuzzClaimBuildRoundTrip builds claims of random valid shape and asserts
|
|
// decode(encode(x)) == x and object-ID stability. Encoder-determinism is the
|
|
// half of section 12.4 that starts from a structured object rather than from
|
|
// received bytes.
|
|
func FuzzClaimBuildRoundTrip(f *testing.F) {
|
|
f.Add([]byte("seed corpus"))
|
|
f.Add([]byte{})
|
|
|
|
f.Fuzz(func(t *testing.T, data []byte) {
|
|
byteAt := func(k int) byte {
|
|
if len(data) == 0 {
|
|
return 0
|
|
}
|
|
return data[k%len(data)]
|
|
}
|
|
issuerSeed := sha256.Sum256(append([]byte("issuer"), data...))
|
|
subjectSeed := sha256.Sum256(append([]byte("subject"), data...))
|
|
issuer, err := signer.FromSeed(issuerSeed[:])
|
|
if err != nil {
|
|
t.Skip("degenerate derived key")
|
|
}
|
|
subject, err := signer.FromSeed(subjectSeed[:])
|
|
if err != nil {
|
|
t.Skip("degenerate derived key")
|
|
}
|
|
|
|
claims := map[string]tce.Value{}
|
|
n := 1 + int(byteAt(0))%3
|
|
for i := 0; i < n; i++ {
|
|
key := "k" + string(rune('a'+i))
|
|
switch byteAt(i+1) % 5 {
|
|
case 0:
|
|
claims[key] = tce.Null()
|
|
case 1:
|
|
claims[key] = tce.Bool(true)
|
|
case 2:
|
|
claims[key] = tce.Bool(false)
|
|
case 3:
|
|
claims[key] = tce.String("value-" + key)
|
|
default:
|
|
claims[key] = tce.Int(int64(i) * int64(n))
|
|
}
|
|
}
|
|
|
|
c := &protocol.Claim{
|
|
Issuer: issuer.Public(), Subject: subject.Public(),
|
|
Claims: claims, CreatedAt: 1_700_000_000, ExpiresAt: 1_700_086_400,
|
|
Serial: 1, Nonce: nonce(0x5a),
|
|
}
|
|
b, err := protocol.EncodeClaim(c)
|
|
if err != nil {
|
|
t.Fatalf("encode: %v", err)
|
|
}
|
|
dec, err := protocol.DecodeClaim(b)
|
|
if err != nil {
|
|
t.Fatalf("own encoding rejected: %v", err)
|
|
}
|
|
if !eqBytes(dec.TCE(), b) {
|
|
t.Fatal("decoded claim did not retain the encoded bytes")
|
|
}
|
|
re, err := protocol.EncodeClaim(dec)
|
|
if err != nil {
|
|
t.Fatalf("re-encode: %v", err)
|
|
}
|
|
if !eqBytes(re, b) {
|
|
t.Fatalf("decode(encode(x)) != x")
|
|
}
|
|
if !tce.ComputeID(b).Equal(tce.ComputeID(re)) {
|
|
t.Fatal("object ID unstable across a decode/encode cycle")
|
|
}
|
|
})
|
|
}
|