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") } }) }