package claim import ( "encoding/hex" "encoding/json" "errors" "os" "strings" "testing" "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/metazla/meta-core/internal/identity" ) // The golden file every MMUR2 implementation is pinned to. Regenerate with // CLAIM_VECTORS_UPDATE=1 go test ./internal/claim/ — then re-sync the vendored // copies (scripts/check-claim-vectors.sh --fix). A change here is a wire break. const vectorsPath = "testdata/claim-vectors.json" // Fixed test key: the scalar 0x0101…01. Never used outside tests. const testKeyHex = "0101010101010101010101010101010101010101010101010101010101010101" type vector struct { Name string `json:"name"` CID string `json:"cid"` UID string `json:"uid"` Key string `json:"key"` Value *string `json:"value"` // text value, or null ValueB64 *string `json:"valueBytesHex,omitempty"` // bytes value as hex TS uint64 `json:"ts"` Signed string `json:"signedBytesHex"` Sig string `json:"sigHex"` // deterministic RFC 6979, low-S } type vectorFile struct { Comment string `json:"_comment"` PrivateKeyHex string `json:"privateKeyHex"` UID string `json:"uid"` Vectors []vector `json:"vectors"` HighS struct { Note string `json:"note"` Vector string `json:"vector"` SigHex string `json:"sigHex"` } `json:"highS"` } func (v vector) value(t *testing.T) Value { t.Helper() switch { case v.ValueB64 != nil: b, err := hex.DecodeString(*v.ValueB64) if err != nil { t.Fatal(err) } return Bytes(b) case v.Value != nil: return Text(*v.Value) } return Null() } func strp(s string) *string { return &s } func testIdentity(t *testing.T) *identity.Identity { t.Helper() id, err := identity.Import(testKeyHex) if err != nil { t.Fatal(err) } return id } func cases(uid string) []vector { return []vector{ {Name: "text", CID: "bafkreih5kapbjzq", UID: uid, Key: "title", Value: strp("The Matrix"), TS: 1793001234567}, {Name: "empty-text", CID: "bafkreih5kapbjzq", UID: uid, Key: "title", Value: strp(""), TS: 1}, {Name: "unicode", CID: "bafkreih5kapbjzq", UID: uid, Key: "titles/jpn/攻殻機動隊", Value: strp("true"), TS: 1793001234567}, {Name: "keyset-member", CID: "bagacbabaec7v3", UID: uid, Key: "cids/bafkreih5kapbjzq", Value: strp("true"), TS: 23}, {Name: "bytes", CID: "bafkreih5kapbjzq", UID: uid, Key: "seek", ValueB64: strp("00ff10ab"), TS: 24}, {Name: "retraction", CID: "bafkreih5kapbjzq", UID: uid, Key: "genres/Drama", TS: 255}, {Name: "long-value", CID: "bafkreih5kapbjzq", UID: uid, Key: "plot/eng", Value: strp(strings.Repeat("a", 300)), TS: 65536}, {Name: "ts-64bit", CID: "bafkreih5kapbjzq", UID: uid, Key: "year", Value: strp("1999"), TS: 1 << 40}, } } func TestVectors(t *testing.T) { id := testIdentity(t) if os.Getenv("CLAIM_VECTORS_UPDATE") == "1" { writeVectors(t, id) } raw, err := os.ReadFile(vectorsPath) if err != nil { t.Fatalf("read vectors: %v (generate with CLAIM_VECTORS_UPDATE=1)", err) } var f vectorFile if err := json.Unmarshal(raw, &f); err != nil { t.Fatal(err) } if f.UID != id.UID { t.Fatalf("vector uid %s != test key uid %s", f.UID, id.UID) } for _, v := range f.Vectors { c := Claim{CID: v.CID, UID: v.UID, Key: v.Key, Value: v.value(t), TS: v.TS} if got := hex.EncodeToString(c.SignedBytes()); got != v.Signed { t.Errorf("%s: signed bytes\n got %s\nwant %s", v.Name, got, v.Signed) } if err := c.Sign(id); err != nil { t.Fatal(err) } if got := hex.EncodeToString(c.Sig); got != v.Sig { t.Errorf("%s: sig\n got %s\nwant %s", v.Name, got, v.Sig) } if err := c.Verify(); err != nil { t.Errorf("%s: verify: %v", v.Name, err) } } // The high-S twin of a valid signature must be refused. for _, v := range f.Vectors { if v.Name != f.HighS.Vector { continue } sig, _ := hex.DecodeString(f.HighS.SigHex) c := Claim{CID: v.CID, UID: v.UID, Key: v.Key, Value: v.value(t), TS: v.TS, Sig: sig} if err := c.Verify(); !errors.Is(err, ErrBadSignature) { t.Errorf("high-S signature accepted: %v", err) } } } func writeVectors(t *testing.T, id *identity.Identity) { t.Helper() f := vectorFile{ Comment: "MMUR2 golden vectors. Canonical copy: meta-core internal/claim/testdata. Do not edit by hand; see claim_test.go.", PrivateKeyHex: testKeyHex, UID: id.UID, } for _, v := range cases(id.UID) { c := Claim{CID: v.CID, UID: v.UID, Key: v.Key, Value: v.value(t), TS: v.TS} if err := c.Sign(id); err != nil { t.Fatal(err) } v.Signed = hex.EncodeToString(c.SignedBytes()) v.Sig = hex.EncodeToString(c.Sig) f.Vectors = append(f.Vectors, v) } first := f.Vectors[0] sig, _ := hex.DecodeString(first.Sig) f.HighS.Note = "same claim as `vector`, S replaced by n-S: valid ECDSA, but MMUR2 verifiers must refuse it" f.HighS.Vector = first.Name f.HighS.SigHex = hex.EncodeToString(highS(sig)) out, err := json.MarshalIndent(f, "", " ") if err != nil { t.Fatal(err) } if err := os.MkdirAll("testdata", 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(vectorsPath, append(out, '\n'), 0o644); err != nil { t.Fatal(err) } } func highS(sig []byte) []byte { var s secp256k1.ModNScalar s.SetByteSlice(sig[32:]) s.Negate() out := append([]byte{}, sig[:32]...) b := s.Bytes() return append(out, b[:]...) } func TestJSONRoundTrip(t *testing.T) { id := testIdentity(t) for _, v := range []Value{Text("x"), Bytes([]byte{0, 1, 2}), Null()} { c := Claim{CID: "bafy", UID: id.UID, Key: "k", Value: v, TS: 42} if err := c.Sign(id); err != nil { t.Fatal(err) } b, err := json.Marshal(c) if err != nil { t.Fatal(err) } var back Claim if err := json.Unmarshal(b, &back); err != nil { t.Fatalf("%s: %v", b, err) } if !back.Value.Equal(v) || back.TS != 42 || string(back.Sig) != string(c.Sig) { t.Errorf("round trip changed the claim: %s", b) } if err := back.Verify(); err != nil { t.Errorf("%s: %v", b, err) } } } func TestTamperRejected(t *testing.T) { id := testIdentity(t) c := Claim{CID: "bafy", UID: id.UID, Key: "title", Value: Text("A"), TS: 1} if err := c.Sign(id); err != nil { t.Fatal(err) } c.Value = Text("B") if err := c.Verify(); !errors.Is(err, ErrBadSignature) { t.Fatalf("tampered value verified: %v", err) } }