diff --git a/smart-byte/conformance/README.md b/smart-byte/conformance/README.md index 9c19c8cb2..33a095d9b 100644 --- a/smart-byte/conformance/README.md +++ b/smart-byte/conformance/README.md @@ -18,16 +18,18 @@ contain no `$ref` pointers, no external URLs, and no runtime dependencies. Each spec version has its own subdirectory (`v1/`, `v2/`, ...). Today only `v1/` exists; it corresponds to the spec text in `../spec/` -(`treatise_v1_parts_I_II_III_combined.md` and -`identity_and_key_rotation.md`). +(`treatise_v1_parts_I_II_III_combined.md`, `identity_and_key_rotation.md` +and `envelope_wire_format.md`). -A pack for a given version contains five JSON files plus `regeneration.md`: +A pack for a given version contains seven JSON files plus `regeneration.md`: | File | Tests | | -------------------------------- | --------------------------------------------------------------------- | | `canonicalisation_vectors.json` | Canonical CBOR: each encoding rule, accepted and refused (identity §2.3) | | `said_vectors.json` | SAID derivation and verification (identity §2.1–§2.5; treatise §8.2) | -| `signature_vectors.json` | Ed25519 signatures over SAIDs (treatise §8.3, §8.4; RFC 8032) | +| `signature_vectors.json` | Ed25519 over the messages Smart Byte signs, verified strictly (RFC 8032) | +| `kel_vectors.json` | Key-event logs: inception, rotation, interaction, thresholds (identity §3, §4) | +| `envelope_vectors.json` | Envelopes decoded and verified against the logs' key states (envelope wire format) | | `lockstep_vectors.json` | Supermajority commit, frame outcomes, dissent log (treatise §13, §14) | | `gossip_vectors.json` | Receiving SAID-addressed objects (identity §2.6; treatise §11.4, §21.4) | @@ -94,12 +96,31 @@ they are lowercase hex. compute (`said_of_received_bytes`, `said_after_reencoding`). - **`signature_vectors.json`** — the Ed25519 rules every Smart Byte signature follows: RFC 8032, with strict decoding of keys and scalars. The message is - `signed_message_hex`, the 32 digest bytes `envelope_said` spells — not its - text. `verifying_key` is the key handed to the verifier; when - `signing_key_seed` is not null, it is the key that seed derives. Verify - `signature` under `verifying_key`: the result must be `expected_verify`, - failing with `error` when false. A positive vector's `signature` is the one - deterministic signature its seed makes. + `message_hex`, the canonical encoding of what is signed: sig-001's is an + issuer's attestation, sig-002's a key event. `verifying_key` is the key + handed to the verifier; when `signing_key_seed` is not null, it is the key + that seed derives. Verify `signature` under `verifying_key`: the result must + be `expected_verify`, failing with `error` when false. A positive vector's + `signature` is the one deterministic signature its seed makes. +- **`kel_vectors.json`** — `events` is one controller's log, in order. Each + event gives its bytes (`cbor_hex`), the same event as typed JSON (`event`) + when the bytes are canonical CBOR, and its `signatures`: each a key `index` + and a `signature` — the key's algorithm identifier, `01` for Ed25519, + followed by the signature over the event's bytes (identity §3.5). A + signature with a `seed` is the one that seed's key makes. Replay the log as + a v1 verifier (identity §4). An accepted log gives `expected`: the + controller AID, each event's SAID, the key state each establishment event + put in force (`sequence`, `keys` in hex, `threshold`), and the next-key + commitment in force at the end. A refused log gives the `error` class and + `error_at`, the position of the event refused; no later event is + considered. +- **`envelope_vectors.json`** — `cbor_hex` is an envelope, also given as + typed JSON (`envelope`) when the bytes are canonical CBOR. `key_event_logs` + names accepted vectors of `kel_vectors.json`; signatures are checked + against the key states their replay establishes. Decode and verify the + envelope (envelope wire format §8). An accepted envelope gives `expected`: + its identity, snapshot SAID, issuer, current owner, history hash (hex) and + number of transfers. A refused one gives the `error` class. - **`lockstep_vectors.json`** — `node_count` is the cluster size n. `node_reported_hashes` are the opaque 32-byte state hashes each node broadcast, and `node_computed_hashes` the ones each computed; they differ @@ -133,13 +154,35 @@ they are lowercase hex. | `trailing_bytes` | bytes after the one top-level item | | `said_mismatch` | content that does not hash to the SAID it states or arrived under | | `signature_invalid` | a signature that does not verify, including a non-canonical S | -| `malformed_signature` | a signature that is not 64 bytes | +| `malformed_signature` | an Ed25519 signature that is not 64 bytes | | `malformed_verifying_key` | a key that is not 32 bytes, or encodes y outside the field | +| `unsupported_algorithm` | a key and signature in an algorithm other than Ed25519 (`01`) | +| `algorithm_mismatch` | a signature whose algorithm identifier differs from its key's | +| `key_index_out_of_range` | a signature by a key index outside the key set | +| `duplicate_key_index` | one key's signature listed twice | +| `below_threshold` | fewer valid signatures than the key state's threshold | +| `unknown_key_state` | a signature naming an establishment event no log establishes | +| `malformed_event` | canonical CBOR that is not exactly a v1 event: an undefined member, a member of the wrong type or form, an unknown type or an extension v1 does not define; or an event out of place, such as a log that does not begin with an inception or an inception with anchors | +| `unsupported_version` | an event whose `v` is not `SBYTE10JSON` | +| `unsupported_event_type` | a `rec` event, which v1 reserves, or a `dip`, which v1 does not define | +| `out_of_sequence` | an event whose `s` does not follow the previous event's | +| `prior_mismatch` | an event whose `p` is not the previous event's `d`, or an inception's that is not empty | +| `controller_mismatch` | an event whose `i` is not its log's controller, or an inception's that is not the AID its SAID names | +| `pre_rotation_mismatch` | a rotation revealing keys other than those committed to | +| `rotation_not_permitted` | a rotation after an establishment event that committed to no next keys | +| `interaction_changes_keys`| an interaction whose key fields differ from those in force | +| `invalid_threshold` | a `kt` outside 1 to the number of keys, or an `nt` that is not 0 for no next keys and 1 to their number otherwise | +| `duplicate_key` | a key or next-key digest listed twice in one event | +| `not_an_envelope` | canonical CBOR that is not exactly an envelope: an undefined member, or a member of the wrong type or form | +| `broken_history_chain` | a transition that does not continue the history it follows | +| `not_current_owner` | a transition not signed away by the byte's current owner | +| `frame_not_increasing` | a transition in the same frame as the one before it, or an earlier one | The reference implementation also reports `truncated`, `malformed`, `too_deep`, `not_a_map`, `missing_said_field` and `malformed_said` for -inputs no vector exercises. An input with several defects reports the first -in stream order. +inputs no vector exercises, and its extensions outside spec v1 have classes of +their own. An input with several defects reports the first in stream order; +a key-event log reports the first event refused. ### Completeness @@ -205,6 +248,12 @@ The pack is the wire-format gate. It does not cover: - Operational concerns. Cluster topology choice, key custody, hardware procurement, regulatory compliance for specific cargo types — all out of scope for the substrate's conformance pack. +- What the cluster decides about a byte (envelope wire format §9): whether + an issuer is registered for a cargo type, and whether a signer's + establishment event was the one in force at a transfer's frame. +- Witnessing, delegation and recovery. Spec v1 defines none of them, and a + v1 verifier refuses events that use them; the reference implementation + tests its extensions for them in its own suite. ## License diff --git a/smart-byte/conformance/VERSIONS.md b/smart-byte/conformance/VERSIONS.md index ef611f852..ee36a16bb 100644 --- a/smart-byte/conformance/VERSIONS.md +++ b/smart-byte/conformance/VERSIONS.md @@ -7,7 +7,7 @@ Smart Byte spec versions. | Vector pack | Spec version | Spec source | Status | Notes | | ----------- | ------------ | ------------------------------------------------------------- | -------- | --------------------------------------------------------------------- | -| `v1/` | v1.0-draft | `../spec/treatise_v1_parts_I_II_III_combined.md` (Parts I-III), `../spec/identity_and_key_rotation.md` | DRAFT | Generated by the reference implementation (`smart-byte-conformance`) and reproduced by `crosscheck/verify_v1.py`. Regenerated in place while DRAFT (rule 4); every change is in `v1/regeneration.md`. | +| `v1/` | v1.0-draft | `../spec/treatise_v1_parts_I_II_III_combined.md` (Parts I-III), `../spec/identity_and_key_rotation.md`, `../spec/envelope_wire_format.md` | DRAFT | Generated by the reference implementation (`smart-byte-conformance`) and reproduced by `crosscheck/verify_v1.py`. Regenerated in place while DRAFT (rule 4); every change is in `v1/regeneration.md`. | Future entries are appended below this table when a new spec version is introduced. Older entries are never removed; an implementation that pins to diff --git a/smart-byte/conformance/crosscheck/verify_v1.py b/smart-byte/conformance/crosscheck/verify_v1.py index 9f8170e29..a7183d034 100644 --- a/smart-byte/conformance/crosscheck/verify_v1.py +++ b/smart-byte/conformance/crosscheck/verify_v1.py @@ -6,8 +6,10 @@ The pack in ../v1 is generated by the Rust reference implementation implementation of everything the pack exercises, written in another language from the spec text rather than ported from the Rust: canonical CBOR and its strict decoder (identity spec §2.3), SAID derivation and verification (§2.1, -§2.2), Ed25519 (RFC 8032), the lockstep commit rule and dissent log (treatise -§13, §14), and the content-addressed gossip intake the gossip vectors describe. +§2.2), Ed25519 (RFC 8032), key-event-log replay (identity spec §3, §4), +envelope verification (envelope wire format §8), the lockstep commit rule and +dissent log (treatise §13, §14), and the content-addressed gossip intake the +gossip vectors describe. It uses the Python standard library only. BLAKE3 and Ed25519 are implemented here from their specifications, and both are checked against published @@ -450,6 +452,267 @@ def receive(held, claimed, data): return {"outcome": "accepted", "said": text} +# --------------------------------------------------------------------------- +# Controller signatures (identity spec §3.5; envelope wire format §7) +# --------------------------------------------------------------------------- + +ED25519 = 0x01 +U64 = 1 << 64 + + +def is_uint(x): + return isinstance(x, int) and not isinstance(x, bool) and 0 <= x < U64 + + +def parse_aid(text): + """The digest a controller AID spells — `B` and a SAID's 52 characters — + or None.""" + if not (isinstance(text, str) and len(text) == 53 and text[0] == "B"): + return None + return parse_said("E" + text[1:]) + + +def aid_text(digest): + return "B" + said_text(digest)[1:] + + +def verify_indexed(keys, threshold, message, signatures): + """Every listed (index, algorithm byte ‖ signature) valid over `message`, + no key index twice, and at least the threshold — never fewer than one — + of keys signing. None, or the error class.""" + seen = set() + for index, sig in signatures: + if not 0 <= index < len(keys): + return "key_index_out_of_range" + if index in seen: + return "duplicate_key_index" + seen.add(index) + key = keys[index] + if not key: + return "malformed_verifying_key" + if not sig or sig[0] != key[0]: + return "algorithm_mismatch" + if key[0] != ED25519: + return "unsupported_algorithm" + err = ed25519_verify(key[1:], message, sig[1:]) + if err: + return err + if len(seen) < max(threshold, 1): + return "below_threshold" + return None + + +# --------------------------------------------------------------------------- +# Key-event logs (identity spec §3, §4) +# --------------------------------------------------------------------------- + +EVENT_MEMBERS = {"a", "d", "i", "k", "kt", "n", "nt", "p", "s", "t", "v"} + + +def members(obj, names, cls): + """`obj` as a dict, if it is a map with exactly the text members `names`.""" + if not isinstance(obj, Map) or {k for k, _ in obj} != names or not all(isinstance(k, str) for k, _ in obj): + raise Refused(cls) + return dict(obj) + + +def decode_event(data): + """Identity spec §4 step 1: canonical bytes, a v1 event type, exactly the + §3.3 members of their types, the version, the SAID, and an inception's + own AID. Returns the event's members.""" + obj = decode(data) + t = dict(obj).get("t") if isinstance(obj, Map) else None + if not isinstance(t, str): + raise Refused("malformed_event") + if t in ("rec", "dip"): + raise Refused("unsupported_event_type") + if t not in ("icp", "rot", "ixn"): + raise Refused("malformed_event") + e = members(obj, EVENT_MEMBERS, "malformed_event") + well_typed = ( + isinstance(e["v"], str) + and parse_said(e["d"]) is not None + and parse_aid(e["i"]) is not None + and all(is_uint(e[f]) for f in ("s", "kt", "nt")) + and (isinstance(e["p"], str) if t == "icp" else parse_said(e["p"]) is not None) + and isinstance(e["k"], list) and all(isinstance(k, bytes) for k in e["k"]) + and isinstance(e["n"], list) and all(parse_said(x) is not None for x in e["n"]) + and isinstance(e["a"], list) and all(parse_said(x) is not None for x in e["a"]) + ) + if not well_typed: + raise Refused("malformed_event") + if e["v"] != "SBYTE10JSON": + raise Refused("unsupported_version") + placeholdered = {"d", "i"} if t == "icp" else {"d"} + digest = blake3(encode(Map((k, PLACEHOLDER if k in placeholdered else v) for k, v in obj))) + if said_text(digest) != e["d"]: + raise Refused("said_mismatch") + if t == "icp" and e["i"] != aid_text(digest): + raise Refused("controller_mismatch") + return e + + +def per_key_said(key): + """Identity spec §3.4.""" + return said_text(blake3(encode(Map([("d", PLACEHOLDER), ("k", key)])))) + + +def check_key_fields(e): + """An establishment event's key fields (§3.3): no key or digest twice, and + each threshold in range.""" + if len(set(e["k"])) != len(e["k"]): + raise Refused("duplicate_key") + if not 1 <= e["kt"] <= len(e["k"]): + raise Refused("invalid_threshold") + if len(set(e["n"])) != len(e["n"]): + raise Refused("duplicate_key") + if (not e["n"] and e["nt"] != 0) or (e["n"] and not 1 <= e["nt"] <= len(e["n"])): + raise Refused("invalid_threshold") + + +class LogRefused(Exception): + def __init__(self, cls, at): + super().__init__(f"{cls} at event {at}") + self.cls, self.at = cls, at + + +def replay(log): + """Identity spec §4 over [(bytes, [(index, signature)])]. Returns the final + state, with every establishment event's key state; raises LogRefused at + the first event refused.""" + state = None + for at, (data, sigs) in enumerate(log): + try: + e = decode_event(data) + t = e["t"] + if (state is None) != (t == "icp"): + raise Refused("malformed_event") + if e["s"] != (0 if state is None else state["s"] + 1): + raise Refused("out_of_sequence") + if e["p"] != ("" if state is None else state["d"]): + raise Refused("prior_mismatch") + if state is not None and e["i"] != state["i"]: + raise Refused("controller_mismatch") + if t == "icp": + if e["a"]: + raise Refused("malformed_event") + check_key_fields(e) + keys, threshold = e["k"], e["kt"] + elif t == "rot": + if not state["n"]: + raise Refused("rotation_not_permitted") + if len(e["k"]) != len(state["n"]) or any( + per_key_said(k) != n for k, n in zip(e["k"], state["n"])): + raise Refused("pre_rotation_mismatch") + if e["a"]: + raise Refused("malformed_event") + check_key_fields(e) + # Signed by the keys it reveals, under the committed threshold + # and its own (§3.5). + keys, threshold = e["k"], max(state["nt"], e["kt"]) + else: + if [e[f] for f in ("k", "kt", "n", "nt")] != [state[f] for f in ("k", "kt", "n", "nt")]: + raise Refused("interaction_changes_keys") + keys, threshold = state["k"], state["kt"] + err = verify_indexed(keys, threshold, data, sigs) + if err: + raise Refused(err) + except Refused as r: + raise LogRefused(r.cls, at) from None + if t == "ixn": + state = dict(state, s=e["s"], d=e["d"]) + else: + establishments = (state["establishments"] if state else []) + [ + {"sequence": e["s"], "keys": e["k"], "threshold": e["kt"]}] + state = dict(e, establishments=establishments) + if state is None: + raise LogRefused("malformed_event", 0) + return state + + +# --------------------------------------------------------------------------- +# Envelopes (envelope wire format §2–§8) +# --------------------------------------------------------------------------- + +def controller_signature(x): + sig = members(x, {"establishment", "signatures"}, "not_an_envelope") + if not is_uint(sig["establishment"]) or not isinstance(sig["signatures"], list): + raise Refused("not_an_envelope") + out = [] + for item in sig["signatures"]: + one = members(item, {"index", "signature"}, "not_an_envelope") + if not is_uint(one["index"]) or not isinstance(one["signature"], bytes): + raise Refused("not_an_envelope") + out.append((one["index"], one["signature"])) + return sig["establishment"], out + + +def decode_envelope(data): + """§8 step 1: canonical bytes that are exactly an envelope, both SAIDs + checking. Returns (envelope members, provenance members, its signature, + [(transition members, its signature)]).""" + obj = decode(data) + env = members(obj, {"d", "provenance", "ownership_chain", "joule_cost"}, "not_an_envelope") + prov = members(env["provenance"], {"d", "issuer", "recipient", "nonce", "cargo", "signature"}, + "not_an_envelope") + cargo = members(prov["cargo"], {"type", "bytes"}, "not_an_envelope") + cost = members(env["joule_cost"], {"measured_microjoules", "estimated_microjoules"}, "not_an_envelope") + if not (parse_said(env["d"]) and parse_said(prov["d"]) and parse_aid(prov["issuer"]) + and parse_aid(prov["recipient"]) and isinstance(prov["nonce"], bytes) + and len(prov["nonce"]) == 16 and is_uint(cargo["type"]) and isinstance(cargo["bytes"], bytes) + and all(is_uint(x) for x in cost.values()) and isinstance(env["ownership_chain"], list)): + raise Refused("not_an_envelope") + issuer_sig = controller_signature(prov["signature"]) + chain = [] + for t in env["ownership_chain"]: + tm = members(t, {"previous_history_hash", "from", "to", "frame", "signature"}, "not_an_envelope") + if not (isinstance(tm["previous_history_hash"], bytes) and len(tm["previous_history_hash"]) == 32 + and parse_aid(tm["from"]) and parse_aid(tm["to"]) and is_uint(tm["frame"])): + raise Refused("not_an_envelope") + chain.append((tm, controller_signature(tm["signature"]))) + if derive(obj, "d")[1] != parse_said(env["d"]): + raise Refused("said_mismatch") + if derive(env["provenance"], "d")[1] != parse_said(prov["d"]): + raise Refused("said_mismatch") + return env, prov, issuer_sig, chain + + +def verify_envelope(data, key_states): + """§8: decode, the issuer's signature, then every transition in order. + `key_states` maps (AID, establishment sequence) to (keys, threshold).""" + env, prov, issuer_sig, chain = decode_envelope(data) + + def signed_by(aid, signature, message): + establishment, sigs = signature + state = key_states.get((aid, establishment)) + if state is None: + raise Refused("unknown_key_state") + err = verify_indexed(state[0], state[1], message, sigs) + if err: + raise Refused(err) + + attested = Map([("cargo", prov["cargo"]), ("issuer", prov["issuer"]), + ("nonce", prov["nonce"]), ("recipient", prov["recipient"])]) + signed_by(prov["issuer"], issuer_sig, encode(attested)) + identity = prov["d"] + owner, history, last_frame = prov["recipient"], parse_said(identity), None + for t, signature in chain: + if t["previous_history_hash"] != history: + raise Refused("broken_history_chain") + if t["from"] != owner: + raise Refused("not_current_owner") + if last_frame is not None and t["frame"] <= last_frame: + raise Refused("frame_not_increasing") + signed_by(t["from"], signature, encode(Map([ + ("byte", identity), ("previous_history_hash", t["previous_history_hash"]), + ("from", t["from"]), ("to", t["to"]), ("frame", t["frame"])]))) + history = blake3(history + encode(Map([ + ("from", t["from"]), ("to", t["to"]), ("frame", t["frame"]), ("signature", t["signature"])]))) + owner, last_frame = t["to"], t["frame"] + return {"identity": identity, "snapshot": env["d"], "issuer": prov["issuer"], "owner": owner, + "history_hash": history.hex(), "transfers": len(chain)} + + # --------------------------------------------------------------------------- # The checks. Each raises Mismatch; none uses `assert`, so `python -O` # cannot switch them off. @@ -512,25 +775,83 @@ def check_said(v): "said_after_reencoding") -def check_signature(v, saids): - check(v["envelope_said"] in saids, "envelope_said is not a SAID vector's expected_said") - digest = parse_said(v["envelope_said"]) - check(digest is not None and digest.hex() == v["signed_message_hex"], "signed_message_hex") +def check_signature(v): + message = bytes.fromhex(v["message_hex"]) seed = v["signing_key_seed"] if seed is not None: check(ed25519_public(bytes.fromhex(seed)).hex() == v["verifying_key"], "verifying_key is not the key the seed derives") sig = bytes.fromhex(v["signature"]) - got = ed25519_verify(bytes.fromhex(v["verifying_key"]), digest, sig) + got = ed25519_verify(bytes.fromhex(v["verifying_key"]), message, sig) if v["expected_verify"] is True: check(got is None and v["error"] is None, f"a positive vector fails: {got}") check(seed is not None, "a positive vector must carry its seed") - check(ed25519_sign(bytes.fromhex(seed), digest) == sig, "the deterministic signature differs") + check(ed25519_sign(bytes.fromhex(seed), message) == sig, "the deterministic signature differs") else: check(v["expected_verify"] is False, "expected_verify is not a boolean") check(got == v["error"], f"refused with {got}, vector says {v['error']}") +def typed_matches(v, member, data): + if member in v: + check(encode(from_json(v[member])) == data, f"`{member}` does not encode to the vector's bytes") + + +def check_kel(v, logs): + """Replay the vector's log; an accepted log's key states are recorded in + `logs` for the envelope vectors.""" + log = [] + for i, e in enumerate(v["events"]): + data = bytes.fromhex(e["cbor_hex"]) + typed_matches(e, "event", data) + sigs = [] + for s in e["signatures"]: + sig = bytes.fromhex(s["signature"]) + if s["seed"] is not None: + check(sig == b"\x01" + ed25519_sign(bytes.fromhex(s["seed"]), data), + f"event {i}: a signature is not the one its seed makes") + sigs.append((s["index"], sig)) + log.append((data, sigs)) + check(("expected" in v) != ("error" in v), "a vector has exactly one of expected and error") + try: + state = replay(log) + except LogRefused as r: + check("error" in v, f"refused with {r.cls} at event {r.at}") + check((r.cls, r.at) == (v["error"], v["error_at"]), + f"refused with {r.cls} at event {r.at}, vector says {v['error']} at {v['error_at']}") + return + check("expected" in v, f"accepted, vector says {v.get('error')}") + computed = { + "controller": state["i"], + "event_saids": [decode_event(data)["d"] for data, _ in log], + "establishments": [{"sequence": x["sequence"], "keys": [k.hex() for k in x["keys"]], + "threshold": x["threshold"]} for x in state["establishments"]], + "next_key_digests": state["n"], + "next_threshold": state["nt"], + } + check(v["expected"] == computed, f"expected: computed {computed}") + logs[v["name"]] = state + + +def check_envelope(v, logs): + key_states = {} + for name in v["key_event_logs"]: + check(name in logs, f"{name} is not an accepted log of kel_vectors.json") + for x in logs[name]["establishments"]: + key_states[(logs[name]["i"], x["sequence"])] = (x["keys"], x["threshold"]) + data = bytes.fromhex(v["cbor_hex"]) + typed_matches(v, "envelope", data) + check(("expected" in v) != ("error" in v), "a vector has exactly one of expected and error") + try: + computed = verify_envelope(data, key_states) + except Refused as r: + check("error" in v, f"refused with {r.cls}") + check(r.cls == v["error"], f"refused with {r.cls}, vector says {v['error']}") + return + check("expected" in v, f"accepted, vector says {v.get('error')}") + check(v["expected"] == computed, f"expected: computed {computed}") + + def tally(n, reports): """(supermajority hash or None, sorted dissenters) — treatise §13.1.""" threshold = 2 * n // 3 + 1 @@ -628,10 +949,13 @@ BLAKE3_KAT = [ (31744, "62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47"), ] +# In order: the envelope vectors draw key states from the accepted logs. CHECKS = { "canonicalisation_vectors.json": check_canonicalisation, "said_vectors.json": check_said, - "signature_vectors.json": None, # needs the SAID set; dispatched in main + "signature_vectors.json": check_signature, + "kel_vectors.json": check_kel, + "envelope_vectors.json": check_envelope, "lockstep_vectors.json": check_lockstep, "gossip_vectors.json": check_gossip, } @@ -704,17 +1028,17 @@ def main(pack): except (Mismatch, ValueError, KeyError, OSError) as e: failed += 1 print(f"FAIL {name}: {e}") - saids = {v["expected_said"] for v in files.get("said_vectors.json", []) if "expected_said" in v} + logs = {} for name, vectors in files.items(): for v in vectors: total += 1 try: - if name == "signature_vectors.json": - check_signature(v, saids) + if name in ("kel_vectors.json", "envelope_vectors.json"): + CHECKS[name](v, logs) else: CHECKS[name](v) print(f"ok {name:<32} {v['name']}") - except (Mismatch, Refused, KeyError, ValueError, TypeError) as e: + except (Mismatch, Refused, LogRefused, KeyError, ValueError, TypeError) as e: failed += 1 print(f"FAIL {name:<32} {v.get('name')}: {e!r}") if len(files) == len(CHECKS): diff --git a/smart-byte/conformance/v1/envelope_vectors.json b/smart-byte/conformance/v1/envelope_vectors.json new file mode 100644 index 000000000..f756f6e47 --- /dev/null +++ b/smart-byte/conformance/v1/envelope_vectors.json @@ -0,0 +1,1150 @@ +[ + { + "name": "env-001-issued", + "description": "A USD byte of 100.00 (100000000 micros, cargo type 1) as the issuer issues it to Alice, with a measured and an estimated joule cost. The issuer signs the canonical encoding of {cargo, issuer, nonce, recipient} under its establishment event 0 (envelope wire format §3, §7). The provenance's SAID is the byte's identity; the envelope's own `d` is this snapshot's SAID (§4). With no transfers, the owner is the recipient and the history hash is the identity's digest.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Erqli3pxnbfezip4pupex4yowmnp7icyzskbodacataepytrmifpa", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [] + }, + "cbor_hex": "a4616478354572716c693370786e6266657a697034707570657834796f776d6e70376963797a736b626f64616361746165707974726d696670616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "expected": { + "identity": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "snapshot": "Erqli3pxnbfezip4pupex4yowmnp7icyzskbodacataepytrmifpa", + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "owner": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "history_hash": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f", + "transfers": 0 + } + }, + { + "name": "env-002-transfers-across-a-rotation", + "description": "env-001's byte after three transfers: Alice to the custodian at frame 10, under Alice's establishment event 0; back to Alice at frame 11, signed by two of the custodian's three keys; to the custodian again at frame 12, signed with the key Alice rotated to, under her establishment event 1. Each signature names the establishment event whose keys made it, so the chain spans the rotation unbroken (§7). The identity is env-001's; the snapshot SAID is new.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "E7ky52ezznazzhds2k2s5mfbag7opcivzoy7efb4mdpg5dyrnuoxq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + }, + { + "to": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "from": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 11, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0f" + } + }, + { + "index": 2, + "signature": { + "$bytes": "0197288385a060626b2866f8ba49334223985a5120cffeab84f65e87573d410a705488f43a6ffd61de745b1b5134b71a030d4ae98c398ef2e6cb0ffd2d5655b500" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "0a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834" + } + }, + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 12, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01854eb95c94a56944912a25634fac3bd3a2ade24ccea7da3a733154fb106cfa0264d0df79f8b0344f591e2256a060f851613724a82bb5ae467a8cbd28771be205" + } + } + ], + "establishment": 1 + }, + "previous_history_hash": { + "$bytes": "53b295243fb7fad0619379612a2d1906fc17b8da522bee8d43545d4f0e738766" + } + } + ] + }, + "cbor_hex": "a46164783545376b793532657a7a6e617a7a686473326b3273356d66626167376f706369767a6f7937656662346d647067356479726e756f78716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e83a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7fa562746f7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f71616466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650b697369676e6174757265a26a7369676e61747572657382a265696e64657800697369676e6174757265584101a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0fa265696e64657802697369676e617475726558410197288385a060626b2866f8ba49334223985a5120cffeab84f65e87573d410a705488f43a6ffd61de745b1b5134b71a030d4ae98c398ef2e6cb0ffd2d5655b5006d65737461626c6973686d656e74007570726576696f75735f686973746f72795f6861736858200a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650c697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e6174757265584101854eb95c94a56944912a25634fac3bd3a2ade24ccea7da3a733154fb106cfa0264d0df79f8b0344f591e2256a060f851613724a82bb5ae467a8cbd28771be2056d65737461626c6973686d656e74017570726576696f75735f686973746f72795f68617368582053b295243fb7fad0619379612a2d1906fc17b8da522bee8d43545d4f0e738766", + "expected": { + "identity": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "snapshot": "E7ky52ezznazzhds2k2s5mfbag7opcivzoy7efb4mdpg5dyrnuoxq", + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "owner": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "history_hash": "401d14039ec708543d65b1180e0413d2a6db6019f5fcbabd0bc530ab257713b6", + "transfers": 3 + } + }, + { + "name": "env-003-empty-opaque-cargo", + "description": "An opaque byte (cargo type 0) carrying no bytes, issued to the custodian with an all-zero nonce and no joule cost: the smallest values each member takes.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Enz47nlrjhwgsbqtvh2dg4icwdher3s2gwkocfpw36s74byxoa5xa", + "joule_cost": { + "measured_microjoules": 0, + "estimated_microjoules": 0 + }, + "provenance": { + "d": "E2kadjdxn3y2qkqetrlduaufujvm4cl4uhavromlm2wyp4kabt4iq", + "cargo": { + "type": 0, + "bytes": { + "$bytes": "" + } + }, + "nonce": { + "$bytes": "00000000000000000000000000000000" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01fba7e0971d94f94bd692228779ca8c3459308c4c1c474ad53e82e3a5c86b840ee6aec7f5b20269e2eb6316987e0204021faf41922b467a91d14f54597921ce0c" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [] + }, + "cbor_hex": "a461647835456e7a34376e6c726a687767736271747668326467346963776468657233733267776b6f6366707733367337346279786f613578616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730075657374696d617465645f6d6963726f6a6f756c6573006a70726f76656e616e6365a66164783545326b61646a64786e337932716b716574726c6475617566756a766d34636c3475686176726f6d6c6d32777970346b61627434697165636172676fa264747970650065627974657340656e6f6e636550000000000000000000000000000000006669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e7478354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e6174757265584101fba7e0971d94f94bd692228779ca8c3459308c4c1c474ad53e82e3a5c86b840ee6aec7f5b20269e2eb6316987e0204021faf41922b467a91d14f54597921ce0c6d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "expected": { + "identity": "E2kadjdxn3y2qkqetrlduaufujvm4cl4uhavromlm2wyp4kabt4iq", + "snapshot": "Enz47nlrjhwgsbqtvh2dg4icwdher3s2gwkocfpw36s74byxoa5xa", + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "owner": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "history_hash": "d280348eedde350540938ac74050b44d59c12f94382b17316cd5b0fe28019f11", + "transfers": 0 + } + }, + { + "name": "env-004-cargo-changed-identity-kept", + "description": "env-001 with its cargo raised to 1,000,000.00 after issuance and the snapshot SAID recomputed, but the identity left as it was. The provenance no longer hashes to the identity it states.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Ezwzb7jvhr4kktlkjudyvudh6d5aaxi4a4qev7ndrsfsjs2twzpgq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731b000000e8d4a51000" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [] + }, + "cbor_hex": "a461647835457a777a62376a766872346b6b746c6b6a7564797675646836643561617869346134716576376e64727366736a733274777a7067716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa264747970650165627974657351a1666d6963726f731b000000e8d4a51000656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "error": "said_mismatch" + }, + { + "name": "env-005-cargo-changed-identity-restamped", + "description": "env-004 with the identity recomputed as well, so both SAIDs check. The issuer never signed this cargo: its signature is over the attested fields, which the cargo is one of (§3).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Eljfjynzhfm4dmhsbb777mirke2puzikmxk5hwrszloalboqvoy2a", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eypa7rxa44d7ndjk4o3xn6qi4zmvn7u3lfdkutugugykpfnoowf3a", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731b000000e8d4a51000" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [] + }, + "cbor_hex": "a461647835456c6a666a796e7a68666d34646d687362623737376d69726b653270757a696b6d786b35687772737a6c6f616c626f71766f7932616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a6616478354579706137727861343464376e646a6b346f33786e367169347a6d766e3775336c66646b757475677567796b70666e6f6f7766336165636172676fa264747970650165627974657351a1666d6963726f731b000000e8d4a51000656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "error": "signature_invalid" + }, + { + "name": "env-006-stale-snapshot-said", + "description": "env-001 with its measured joule cost changed and its snapshot SAID left as it was. A snapshot SAID covers the whole envelope, joule cost included (§4).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Erqli3pxnbfezip4pupex4yowmnp7icyzskbodacataepytrmifpa", + "joule_cost": { + "measured_microjoules": 1, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [] + }, + "cbor_hex": "a4616478354572716c693370786e6266657a697034707570657834796f776d6e70376963797a736b626f64616361746165707974726d696670616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730175657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "error": "said_mismatch" + }, + { + "name": "env-007-transfer-by-non-owner", + "description": "env-001, which Alice owns, transferred by the custodian — validly signed by two of its keys, but the custodian does not own the byte (§8 step 3).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Enha3fyz4vtertt6lr3o2rew4ug2vyii6fjtsuvbefblqvrsi3fwq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "from": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01adcc13c151e49673d8daa271409522522c86b2be5b25262e88517f8b5558cf9f0226c5a590f757253520d773f273e9f661dbb99d0c24fe1fce92988c3ea47e05" + } + }, + { + "index": 1, + "signature": { + "$bytes": "0190ef0c7d6cd8eb314257b4ce1527dbf15850fab0e2bf5a3c584bfa6ae4c044e97c8a374e77756753f1b3345d123a8cd05649259e8d96c6a2c0a0d9e21dd92608" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + } + ] + }, + "cbor_hex": "a461647835456e68613366797a34767465727474366c72336f32726577347567327679696936666a74737576626566626c7176727369336677716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e81a562746f78354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c696972686336697663716466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650a697369676e6174757265a26a7369676e61747572657382a265696e64657800697369676e6174757265584101adcc13c151e49673d8daa271409522522c86b2be5b25262e88517f8b5558cf9f0226c5a590f757253520d773f273e9f661dbb99d0c24fe1fce92988c3ea47e05a265696e64657801697369676e617475726558410190ef0c7d6cd8eb314257b4ce1527dbf15850fab0e2bf5a3c584bfa6ae4c044e97c8a374e77756753f1b3345d123a8cd05649259e8d96c6a2c0a0d9e21dd926086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f", + "error": "not_current_owner" + }, + { + "name": "env-008-grafted-transition", + "description": "env-002's first two transfers, with the second re-signed over a previous history hash taken from the start of the chain rather than after the first transfer. Its signature verifies, but it does not continue the history it follows (§5).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Etfc6qammmrukgmqlq3zhle4igq4355e3twugswd5lwci7fbkn4vq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + }, + { + "to": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "from": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 11, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01735e0e2091f97bc92891987802fb30bf080565e191d173b5d2ac3bd57922e04c2d9dd6e39535734fd2f8d54d017b7dfe83e6c764276c99fb0e55e0f2221beb05" + } + }, + { + "index": 2, + "signature": { + "$bytes": "01fa88265708bf97de7afa53428dbd5239a162a5cb18c25659c2a42cd44900ee0ec035aff2c92bb9448aca4243a729ee59679ac3a87e53c6d23743556bbf8d1e03" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + } + ] + }, + "cbor_hex": "a461647835457466633671616d6d6d72756b676d716c71337a686c653469677134333535653374777567737764356c7763693766626b6e3476716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e82a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7fa562746f7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f71616466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650b697369676e6174757265a26a7369676e61747572657382a265696e64657800697369676e6174757265584101735e0e2091f97bc92891987802fb30bf080565e191d173b5d2ac3bd57922e04c2d9dd6e39535734fd2f8d54d017b7dfe83e6c764276c99fb0e55e0f2221beb05a265696e64657802697369676e6174757265584101fa88265708bf97de7afa53428dbd5239a162a5cb18c25659c2a42cd44900ee0ec035aff2c92bb9448aca4243a729ee59679ac3a87e53c6d23743556bbf8d1e036d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f", + "error": "broken_history_chain" + }, + { + "name": "env-009-two-transfers-in-one-frame", + "description": "env-001 transferred to the custodian and straight back, both at frame 10. A byte moves at most once per frame, so frames rise strictly along the chain (§5).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Etxwbqfwl5pe66fcgfnf5kahvdvbaz37wp5aowv2guangs36x6r7a", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + }, + { + "to": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "from": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01034e71712702c8f8259d22f8f9118dafe1297a5e98c15009901b7a6b6b09ffd7f8e21363395e3a716ce973b6338e0af8707b3cc98d039e06ce37e762803eff0a" + } + }, + { + "index": 2, + "signature": { + "$bytes": "0178e4f9f210e03569bdd74ebfcac49bcff78745528251a772c459e6a72a20bc71023a2803afe6477a6ad2fd4b13e639badd2d49c6fbde771b8c846b3bb454d004" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "0a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834" + } + } + ] + }, + "cbor_hex": "a46164783545747877627166776c3570653636666367666e66356b616876647662617a3337777035616f7776326775616e6773333678367237616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e82a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7fa562746f7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f71616466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650a697369676e6174757265a26a7369676e61747572657382a265696e64657800697369676e6174757265584101034e71712702c8f8259d22f8f9118dafe1297a5e98c15009901b7a6b6b09ffd7f8e21363395e3a716ce973b6338e0af8707b3cc98d039e06ce37e762803eff0aa265696e64657802697369676e617475726558410178e4f9f210e03569bdd74ebfcac49bcff78745528251a772c459e6a72a20bc71023a2803afe6477a6ad2fd4b13e639badd2d49c6fbde771b8c846b3bb454d0046d65737461626c6973686d656e74007570726576696f75735f686973746f72795f6861736858200a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834", + "error": "frame_not_increasing" + }, + { + "name": "env-010-names-a-non-establishment-event", + "description": "env-001 transferred by Alice with a signature naming her event 2. That is her interaction, which establishes no keys, so no key state answers to it (§7).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "E4zsnekm2ertm6gd3o2bloy33ub2f5lyrcsvyfa5qguwwomevvvlq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01a74062c555c571b46ecc0aa15f674b1a780ae1881d2ea3592f5cfa0ab819c6b5b9fe66633aa20f01f01a300448caa0e2499df5c27674f9d0029c8fda057e6902" + } + } + ], + "establishment": 2 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + } + ] + }, + "cbor_hex": "a46164783545347a736e656b6d326572746d366764336f32626c6f79333375623266356c79726373767966613571677577776f6d657676766c716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e81a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e6174757265584101a74062c555c571b46ecc0aa15f674b1a780ae1881d2ea3592f5cfa0ab819c6b5b9fe66633aa20f01f01a300448caa0e2499df5c27674f9d0029c8fda057e69026d65737461626c6973686d656e74027570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f", + "error": "unknown_key_state" + }, + { + "name": "env-011-old-key-under-new-establishment", + "description": "env-001 transferred by Alice with a signature naming her establishment event 1 but made with the key event 0 put in force. A signature verifies against the key state it names.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Eco5hbwszixc4gh6m6u7jwup3ybg73rmtvrbdaldaknvtteyyqrbq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ], + "establishment": 1 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + } + ] + }, + "cbor_hex": "a46164783545636f35686277737a697863346768366d3675376a777570337962673733726d7476726264616c64616b6e767474657979717262716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e81a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74017570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f", + "error": "signature_invalid" + }, + { + "name": "env-012-custodian-below-threshold", + "description": "The custodian transfers the byte back to Alice with one signature; its key state needs two.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Ejrd4txwqz5zstoeq7xshgjkf7ckjaoje7hqlh6rsxra5vai5enna", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + }, + { + "to": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "from": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 11, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0f" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "0a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834" + } + } + ] + }, + "cbor_hex": "a461647835456a726434747877717a357a73746f657137787368676a6b6637636b6a616f6a653768716c683672737872613576616935656e6e616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e82a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7fa562746f7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f71616466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650b697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e6174757265584101a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0f6d65737461626c6973686d656e74007570726576696f75735f686973746f72795f6861736858200a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834", + "error": "below_threshold" + }, + { + "name": "env-013-one-key-signing-twice", + "description": "The custodian's transfer with key 0's signature listed twice. Counted naively, one key meets the custodian's threshold of two (§7).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Emhkqqihvk3tarqqz3s5lbkzlfprfoa27efw5wnu2yboineeo7joq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + }, + { + "to": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "from": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 11, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0f" + } + }, + { + "index": 0, + "signature": { + "$bytes": "01a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0f" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "0a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834" + } + } + ] + }, + "cbor_hex": "a461647835456d686b71716968766b3374617271717a3373356c626b7a6c667072666f61323765667735776e753279626f696e65656f376a6f716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e82a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7fa562746f7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f71616466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650b697369676e6174757265a26a7369676e61747572657382a265696e64657800697369676e6174757265584101a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0fa265696e64657800697369676e6174757265584101a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0f6d65737461626c6973686d656e74007570726576696f75735f686973746f72795f6861736858200a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834", + "error": "duplicate_key_index" + }, + { + "name": "env-014-key-index-out-of-range", + "description": "The custodian's transfer with a signature at key index 3 of a three-key set.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "E53liytof2f44ovoegoywerq64naulankfilxsr5q3nh6oeu4tzvq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + }, + { + "to": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "from": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 11, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0f" + } + }, + { + "index": 3, + "signature": { + "$bytes": "0197288385a060626b2866f8ba49334223985a5120cffeab84f65e87573d410a705488f43a6ffd61de745b1b5134b71a030d4ae98c398ef2e6cb0ffd2d5655b500" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "0a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834" + } + } + ] + }, + "cbor_hex": "a4616478354535336c6979746f66326634346f766f65676f797765727136346e61756c616e6b66696c7873723571336e68366f657534747a76716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e82a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7fa562746f7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f71616466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650b697369676e6174757265a26a7369676e61747572657382a265696e64657800697369676e6174757265584101a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0fa265696e64657803697369676e617475726558410197288385a060626b2866f8ba49334223985a5120cffeab84f65e87573d410a705488f43a6ffd61de745b1b5134b71a030d4ae98c398ef2e6cb0ffd2d5655b5006d65737461626c6973686d656e74007570726576696f75735f686973746f72795f6861736858200a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834", + "error": "key_index_out_of_range" + }, + { + "name": "env-015-algorithm-mismatch", + "description": "Alice's transfer with its signature's algorithm identifier changed from 0x01 to 0x02. A signature carries its key's algorithm (§7).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Evl3xs7jwm7z265pi2okwxo2jwnun4ul6e5apqadej5nz5fp67gvq", + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [ + { + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 10, + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0276f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ], + "establishment": 0 + }, + "previous_history_hash": { + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" + } + } + ] + }, + "cbor_hex": "a46164783545766c337873376a776d377a3236357069326f6b77786f326a776e756e34756c3665356170716164656a356e7a35667036376776716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e81a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410276f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f", + "error": "algorithm_mismatch" + }, + { + "name": "env-016-non-canonical-bytes", + "description": "env-001's bytes with the measured joule cost, 1200, written with a four-byte argument (0x1a 0x00 0x00 0x04 0xb0) instead of two. Both SAIDs are unchanged and check against the canonical form, but these bytes are not it, so they are refused before anything is hashed.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "cbor_hex": "a4616478354572716c693370786e6266657a697034707570657834796f776d6e70376963797a736b626f64616361746165707974726d696670616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731a000004b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "error": "non_shortest_integer" + }, + { + "name": "env-017-undefined-member", + "description": "env-001 with a top-level member `fee` added and its snapshot SAID re-derived. An envelope has exactly the members the wire format defines (§2).", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Eahzpmrehkznlqmyutahcyr7engwwwt2hkjvb25gci2c3kpq2v27q", + "fee": 0, + "joule_cost": { + "measured_microjoules": 1200, + "estimated_microjoules": 1500 + }, + "provenance": { + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [] + }, + "cbor_hex": "a5616478354561687a706d7265686b7a6e6c716d797574616863797237656e677777777432686b6a766232356763693263336b7071327632377163666565006a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "error": "not_an_envelope" + }, + { + "name": "env-018-cargo-bytes-written-as-integers", + "description": "env-003 with its cargo's byte string written as an array of integers instead — empty either way — and both SAIDs re-derived over that form. The bytes are canonical CBOR, and a verifier that hashes what it decoded finds both SAIDs check; but they are not the envelope's encoding, in which cargo bytes are a byte string (§6). Accepting them would give the byte a second encoding.", + "key_event_logs": [ + "kel-001-single-key-inception", + "kel-002-rotation-then-interaction", + "kel-003-two-of-three-rotation" + ], + "envelope": { + "d": "Edovosdwu34sh7wsnhbnafqukg66iex2wzkj5rjflczmbnfgpmcya", + "joule_cost": { + "measured_microjoules": 0, + "estimated_microjoules": 0 + }, + "provenance": { + "d": "Er3agurmnx67bd5nzvyurvhcuxcs3q4um3pifllozwmijc7pre6yq", + "cargo": { + "type": 0, + "bytes": [] + }, + "nonce": { + "$bytes": "00000000000000000000000000000000" + }, + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "signature": { + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01fba7e0971d94f94bd692228779ca8c3459308c4c1c474ad53e82e3a5c86b840ee6aec7f5b20269e2eb6316987e0204021faf41922b467a91d14f54597921ce0c" + } + } + ], + "establishment": 0 + } + }, + "ownership_chain": [] + }, + "cbor_hex": "a46164783545646f766f73647775333473683777736e68626e616671756b67363669657832777a6b6a35726a666c637a6d626e6667706d6379616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730075657374696d617465645f6d6963726f6a6f756c6573006a70726f76656e616e6365a661647835457233616775726d6e7836376264356e7a7679757276686375786373337134756d337069666c6c6f7a776d696a633770726536797165636172676fa264747970650065627974657380656e6f6e636550000000000000000000000000000000006669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e7478354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e6174757265584101fba7e0971d94f94bd692228779ca8c3459308c4c1c474ad53e82e3a5c86b840ee6aec7f5b20269e2eb6316987e0204021faf41922b467a91d14f54597921ce0c6d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "error": "not_an_envelope" + } +] diff --git a/smart-byte/conformance/v1/kel_vectors.json b/smart-byte/conformance/v1/kel_vectors.json new file mode 100644 index 000000000..c6099951e --- /dev/null +++ b/smart-byte/conformance/v1/kel_vectors.json @@ -0,0 +1,1497 @@ +[ + { + "name": "kel-001-single-key-inception", + "description": "The issuer's log: one inception, self-signed by the key it reveals (identity spec §3.5). The key is RFC 8032's first test key. `n` commits to one next key by its per-key SAID (§3.4), and both thresholds are 1. The controller AID is the inception's SAID with `B` for `E` (§3.1); while the event is hashed, `i` holds the placeholder just as `d` does.", + "events": [ + { + "event": { + "a": [], + "d": "Ei473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "i": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "k": [ + { + "$bytes": "01d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + } + ], + "n": [ + "E4fnvjywz23zeyb426croyvctrtqsse237th3kfjid4pfeewx7noa" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab616180616478354569343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616978354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616b81582101d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a616e8178354534666e766a79777a32337a65796234323663726f797663747274717373653233377468336b666a696434706665657778376e6f6161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01e90f89475321e5febce3d271ccb2e304301d0975a6e49aa49a9e76f54ecf4d027097eda8d45ff0d0bf14c94d86f26d7100868c1af79bcfb609792e720e6b1604", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + } + ] + } + ], + "expected": { + "controller": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "event_saids": [ + "Ei473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq" + ], + "establishments": [ + { + "sequence": 0, + "keys": [ + "01d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + ], + "threshold": 1 + } + ], + "next_key_digests": [ + "E4fnvjywz23zeyb426croyvctrtqsse237th3kfjid4pfeewx7noa" + ], + "next_threshold": 1 + } + }, + { + "name": "kel-002-rotation-then-interaction", + "description": "Alice's log: an inception, a rotation and an interaction. The rotation reveals the key the inception committed to and is signed by that key, the one it reveals (§3.5). The interaction anchors the identity of the byte Alice was issued (env-001), carries the key fields forward unchanged, and is signed with the key the rotation put in force. Two establishment events, so two key states: envelope signatures name the one whose keys made them.", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + }, + { + "event": { + "a": [], + "d": "Easjfe7h6aloamed72k665rs6a7ekjrkkni3ffelnttgm3knjp5xa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + } + ], + "n": [ + "E645lnczbq6zrnrsaqq5ncuhmvsws4gsdjnc4hwvusflar5vtey4q" + ], + "p": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 1, + "t": "rot", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab616180616478354561736a6665376836616c6f616d656437326b3636357273366137656b6a726b6b6e69336666656c6e7474676d336b6e6a7035786161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c61723576746579347161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161617301617463726f7461766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "0145034c547fe5ce1fdcbba545caea06462ef49eca66b066b43d4e9c1512a348d76958a819b463ad05bf4b550c5e3af22067aabda134cd91d60e6a961bf9468d08", + "seed": "94cc758a5ba6b9270d29eb7a6b5833b57d55573cd6a4b8ddc6a7408654809f31" + } + ] + }, + { + "event": { + "a": [ + "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q" + ], + "d": "Ep3e57xvov2amfgreyzeym5nhfkfwka6uzldtetfxftq4dklimk2a", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + } + ], + "n": [ + "E645lnczbq6zrnrsaqq5ncuhmvsws4gsdjnc4hwvusflar5vtey4q" + ], + "p": "Easjfe7h6aloamed72k665rs6a7ekjrkkni3ffelnttgm3knjp5xa", + "s": 2, + "t": "ixn", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab616181783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a37716164783545703365353778766f7632616d66677265797a65796d356e68666b66776b6136757a6c64746574667866747134646b6c696d6b326161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c617235767465793471617078354561736a6665376836616c6f616d656437326b3636357273366137656b6a726b6b6e69336666656c6e7474676d336b6e6a7035786161730261746369786e61766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01fbc6aec641e4ca9492bf58e521be44c0193a98f02a14e9eeffc195159b61e715a86d853abdc23001997102ebe20d5fe7b6009609f5dde330ca72a31ca9b84105", + "seed": "94cc758a5ba6b9270d29eb7a6b5833b57d55573cd6a4b8ddc6a7408654809f31" + } + ] + } + ], + "expected": { + "controller": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "event_saids": [ + "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "Easjfe7h6aloamed72k665rs6a7ekjrkkni3ffelnttgm3knjp5xa", + "Ep3e57xvov2amfgreyzeym5nhfkfwka6uzldtetfxftq4dklimk2a" + ], + "establishments": [ + { + "sequence": 0, + "keys": [ + "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + ], + "threshold": 1 + }, + { + "sequence": 1, + "keys": [ + "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + ], + "threshold": 1 + } + ], + "next_key_digests": [ + "E645lnczbq6zrnrsaqq5ncuhmvsws4gsdjnc4hwvusflar5vtey4q" + ], + "next_threshold": 1 + } + }, + { + "name": "kel-003-two-of-three-rotation", + "description": "The custodian's log: three keys, any two of which sign. The inception is signed by keys 0 and 1; the rotation reveals the three committed keys and is signed by keys 0 and 2, meeting both the committed next threshold and its own (§3.5). Which keys sign is up to the controller; how many is not.", + "events": [ + { + "event": { + "a": [], + "d": "Eqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "i": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "k": [ + { + "$bytes": "01f4ff420feb4040a73e51713a44f9966476c755e8e2d18b82677f8ff7a839afe2" + }, + { + "$bytes": "019b54b13b505ae2ddb12e49d8e10ed93725167ab9a0a77803538fc69858cdb0e2" + }, + { + "$bytes": "015c26708ee23779ee7b628241cc1735ed1ba4527639d7383bbc0ad9aa6295f535" + } + ], + "n": [ + "Efcf43zs566fqd5jlkxmbqnv2srtgk45uhenfpc4p5xzfkx7rdvma", + "Euaam4ynyfrxpmxiyogpumg3m6bdb4s5jigslwv3oqnp2gswv4buq", + "Ell3qkq2hpnwirn4m6hgulph32looiev6e5hgst2oc5c5f6jiwjjq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 2, + "nt": 2 + }, + "cbor_hex": "ab616180616478354571656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371616978354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371616b83582101f4ff420feb4040a73e51713a44f9966476c755e8e2d18b82677f8ff7a839afe25821019b54b13b505ae2ddb12e49d8e10ed93725167ab9a0a77803538fc69858cdb0e25821015c26708ee23779ee7b628241cc1735ed1ba4527639d7383bbc0ad9aa6295f535616e8378354566636634337a73353636667164356a6c6b786d62716e7632737274676b34357568656e667063347035787a666b78377264766d617835457561616d34796e79667278706d7869796f6770756d67336d366264623473356a6967736c7776336f716e703267737776346275717835456c6c33716b713268706e7769726e346d366867756c706833326c6f6f69657636653568677374326f6335633566366a69776a6a7161706061730061746369637061766b534259544531304a534f4e626b7402626e7402", + "signatures": [ + { + "index": 0, + "signature": "01e9f5ae4b064588588f8566045833d5860bd680c6fd7182246813fa73710393eec68a6a859931f597dc169950d4aa3539d1e7296372488821cf3b233e5eda940d", + "seed": "a13cea6aba73e2de6e8e19bfc0081a59de4305de0b91901fe325478164281227" + }, + { + "index": 1, + "signature": "01a2fb0fee6f1bfce74bc0b9e324fec7b3ee5415171740310a3fa769ff6aadd3ed3f75496a7eeb08dcf6db4e939810246c45a1c6f0474a64ec6e558cfe8e50910a", + "seed": "4ae1cd361d4765fca9649bbc4d8835a1594064b86c1b94e7412bb5d6e21e7d10" + } + ] + }, + { + "event": { + "a": [], + "d": "Ezx3ihmailb3zrgwymilb2ys4jahpxrmqs6pbd7scmequzeb4pwja", + "i": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "k": [ + { + "$bytes": "01091c2155a4686c049d97ef032881a2291c303939534380064076194d1c354761" + }, + { + "$bytes": "011e0815c33d4a9b95e5097f32e2193330d24ee2505e20fdd02c51e2f6ff51c7eb" + }, + { + "$bytes": "017f90c4a1f622dd21d395b1dc30ad7077c5945223004b936516cfaf583fd1f669" + } + ], + "n": [ + "Elmjjmq2ykqtw34oj7yvm3gqib7yuns2brzoylucu3eklrohsprka", + "E4y4e4wj3os4engc5acjvutwgawrpcrsuvxm24emnk4bq5cgtxneq", + "Eqwtis3sxfytzis2yfoibeqmrfmqze4uzqa4lnp6rzzfvkmsfugfa" + ], + "p": "Eqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "s": 1, + "t": "rot", + "v": "SBYTE10JSON", + "kt": 2, + "nt": 2 + }, + "cbor_hex": "ab61618061647835457a783369686d61696c62337a726777796d696c62327973346a61687078726d7173367062643773636d6571757a65623470776a61616978354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371616b83582101091c2155a4686c049d97ef032881a2291c303939534380064076194d1c3547615821011e0815c33d4a9b95e5097f32e2193330d24ee2505e20fdd02c51e2f6ff51c7eb5821017f90c4a1f622dd21d395b1dc30ad7077c5945223004b936516cfaf583fd1f669616e837835456c6d6a6a6d7132796b71747733346f6a3779766d33677169623779756e733262727a6f796c75637533656b6c726f687370726b617835453479346534776a336f7334656e67633561636a7675747767617772706372737576786d3234656d6e6b34627135636774786e657178354571777469733373786679747a69733279666f696265716d72666d717a6534757a7161346c6e7036727a7a66766b6d736675676661617078354571656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371617301617463726f7461766b534259544531304a534f4e626b7402626e7402", + "signatures": [ + { + "index": 0, + "signature": "01ec2416f0380c4f4a3912a4aba74433a41e27b8c90f0056d1ca5c34accebb7ec8b74eb0b35db32e26d5a7cf90feafe63889154ac0fba8726e20858838473f1307", + "seed": "f47ee7427cb3c3fab47b0faaf284c71120522958a0484f9c790dc55e9a8980c7" + }, + { + "index": 2, + "signature": "01559ad77833119220f4af24939430116e1845adb5e7ea102aa4b7fde57d44260644608f0feb7913c0136b65ed9335f54a92ad3c9c4576d4d52f5f73a1fb25a506", + "seed": "6faec1b96f547ab77f5d53bab76f07a3951ff64e6ef38fda8ae8e0abf8651b8b" + } + ] + } + ], + "expected": { + "controller": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "event_saids": [ + "Eqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "Ezx3ihmailb3zrgwymilb2ys4jahpxrmqs6pbd7scmequzeb4pwja" + ], + "establishments": [ + { + "sequence": 0, + "keys": [ + "01f4ff420feb4040a73e51713a44f9966476c755e8e2d18b82677f8ff7a839afe2", + "019b54b13b505ae2ddb12e49d8e10ed93725167ab9a0a77803538fc69858cdb0e2", + "015c26708ee23779ee7b628241cc1735ed1ba4527639d7383bbc0ad9aa6295f535" + ], + "threshold": 2 + }, + { + "sequence": 1, + "keys": [ + "01091c2155a4686c049d97ef032881a2291c303939534380064076194d1c354761", + "011e0815c33d4a9b95e5097f32e2193330d24ee2505e20fdd02c51e2f6ff51c7eb", + "017f90c4a1f622dd21d395b1dc30ad7077c5945223004b936516cfaf583fd1f669" + ], + "threshold": 2 + } + ], + "next_key_digests": [ + "Elmjjmq2ykqtw34oj7yvm3gqib7yuns2brzoylucu3eklrohsprka", + "E4y4e4wj3os4engc5acjvutwgawrpcrsuvxm24emnk4bq5cgtxneq", + "Eqwtis3sxfytzis2yfoibeqmrfmqze4uzqa4lnp6rzzfvkmsfugfa" + ], + "next_threshold": 2 + } + }, + { + "name": "kel-004-no-next-keys", + "description": "An inception that commits to no next keys: `n` is empty and `nt` is 0. The controller gives up rotating; from here only interactions are accepted (§3.3). kel-017 is a rotation after it.", + "events": [ + { + "event": { + "a": [], + "d": "E5qm4bv4oymc45al2tx2loo7p7xctv6mhy4k6johqdvlwsdfgezja", + "i": "B5qm4bv4oymc45al2tx2loo7p7xctv6mhy4k6johqdvlwsdfgezja", + "k": [ + { + "$bytes": "01cd769ca0c08e672c895d07253992cc5423a488a66cb59b63e72c281c1806321f" + } + ], + "n": [], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 0 + }, + "cbor_hex": "ab616180616478354535716d346276346f796d633435616c327478326c6f6f37703778637476366d6879346b366a6f687164766c7773646667657a6a61616978354235716d346276346f796d633435616c327478326c6f6f37703778637476366d6879346b366a6f687164766c7773646667657a6a61616b81582101cd769ca0c08e672c895d07253992cc5423a488a66cb59b63e72c281c1806321f616e8061706061730061746369637061766b534259544531304a534f4e626b7401626e7400", + "signatures": [ + { + "index": 0, + "signature": "0120383bb4694d56707225a31421d9ce3814e9fe074515c7c487572411fd7e5607a883962379cfc17707235395388f7a2d99040a7eae0bd2e5b1f20946ebc7b004", + "seed": "3558491c31cef5ce59fb6d3cc2a9867fe091283f979beb4331008dd887c558a5" + } + ] + } + ], + "expected": { + "controller": "B5qm4bv4oymc45al2tx2loo7p7xctv6mhy4k6johqdvlwsdfgezja", + "event_saids": [ + "E5qm4bv4oymc45al2tx2loo7p7xctv6mhy4k6johqdvlwsdfgezja" + ], + "establishments": [ + { + "sequence": 0, + "keys": [ + "01cd769ca0c08e672c895d07253992cc5423a488a66cb59b63e72c281c1806321f" + ], + "threshold": 1 + } + ], + "next_key_digests": [], + "next_threshold": 0 + } + }, + { + "name": "kel-005-reserved-recovery-refused", + "description": "The issuer's inception followed by a `rec` event, which identity spec §3.2 reserves: a v1 verifier refuses it as soon as it reads the type. This one reveals keys the issuer never committed to and is signed by them — the takeover that recovering without committed keys would allow.", + "events": [ + { + "event": { + "a": [], + "d": "Ei473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "i": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "k": [ + { + "$bytes": "01d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + } + ], + "n": [ + "E4fnvjywz23zeyb426croyvctrtqsse237th3kfjid4pfeewx7noa" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab616180616478354569343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616978354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616b81582101d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a616e8178354534666e766a79777a32337a65796234323663726f797663747274717373653233377468336b666a696434706665657778376e6f6161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01e90f89475321e5febce3d271ccb2e304301d0975a6e49aa49a9e76f54ecf4d027097eda8d45ff0d0bf14c94d86f26d7100868c1af79bcfb609792e720e6b1604", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + } + ] + }, + { + "event": { + "a": [], + "d": "E5cr3i6t3h4no7x3gpfortbapykbmwpfcx3tts7yo4aeuzxs5xdwa", + "i": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "k": [ + { + "$bytes": "01645d64eb8032633240eb046568599a1d9a50bd6d8cebcc91152fed0a873a2a9b" + } + ], + "n": [ + "Epglbdwltshobhke7zyzx63kcdxwb5a6q2zqnzybsdztwshparrxa" + ], + "p": "Ei473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "s": 1, + "t": "rec", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab6161806164783545356372336936743368346e6f3778336770666f7274626170796b626d77706663783374747337796f346165757a78733578647761616978354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616b81582101645d64eb8032633240eb046568599a1d9a50bd6d8cebcc91152fed0a873a2a9b616e8178354570676c6264776c7473686f62686b65377a797a7836336b636478776235613671327a716e7a796273647a74777368706172727861617078354569343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637161730161746372656361766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "017368dbe713732461ddf8ed4a56e36b2260d8c82ded211d53a4d5352bb3a5f080ac05b33fe0c8d101a6e0a31548c7cbeede6a4252f4bfc4760cdcd2f0b2d7800b", + "seed": "3f4eb93d3a6747aaddcb3a6e7539b8ada2c185a134b6f98ec6c2355b3d99c896" + } + ] + } + ], + "error": "unsupported_event_type", + "error_at": 1 + }, + { + "name": "kel-006-delegated-inception-refused", + "description": "A log that begins with a delegated inception (`dip`), naming the issuer as its parent. Spec v1 defines `icp`, `rot` and `ixn` (and reserves `rec`); delegation is not among them, so the event is refused.", + "events": [ + { + "event": { + "d": "Enktcxl4yypyopjkuhnomzyd5lupktyyf6zie3fzry45dcnzpyxia", + "i": "Bnktcxl4yypyopjkuhnomzyd5lupktyyf6zie3fzry45dcnzpyxia", + "k": [ + { + "$bytes": "017357b2442f366a4af01a7b09b7d7f088e427f2161500bffaf2fb6dd8bbc184fe" + } + ], + "n": [ + "En2skofjb5aifub63fspjpk5e3qyuz4wupyqhykb2lhzhpmilypca" + ], + "p": "", + "s": 0, + "t": "dip", + "v": "SBYTE10JSON", + "di": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61647835456e6b7463786c34797970796f706a6b75686e6f6d7a7964356c75706b74797966367a696533667a7279343564636e7a707978696161697835426e6b7463786c34797970796f706a6b75686e6f6d7a7964356c75706b74797966367a696533667a7279343564636e7a7079786961616b815821017357b2442f366a4af01a7b09b7d7f088e427f2161500bffaf2fb6dd8bbc184fe616e817835456e32736b6f666a6235616966756236336673706a706b3565337179757a34777570797168796b62326c687a68706d696c7970636161706061730061746364697061766b534259544531304a534f4e62646978354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01bfd8a5fac3e8511240085ce70145d46e876690ef671ffcbe04be21acb3cea5a91f67f5e43170cbf902d72b395a60624aaee130d5cdd471156948505835329900", + "seed": "820711f9efd42c930283fdbf4ed4aa576e6d6aab7c26824c3a095259197d29ab" + } + ] + } + ], + "error": "unsupported_event_type", + "error_at": 0 + }, + { + "name": "kel-007-rotation-to-uncommitted-key", + "description": "Alice's inception followed by a rotation to a key her inception never committed to, signed by that key. The revealed key's per-key SAID is not the committed one (§4 step 5), so the rotation is refused.", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + }, + { + "event": { + "a": [], + "d": "Eifuoplpfz3nbfypggzfiyugqganwdcf6umku52ssmdkwbiwlgzaa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "01645d64eb8032633240eb046568599a1d9a50bd6d8cebcc91152fed0a873a2a9b" + } + ], + "n": [ + "Epglbdwltshobhke7zyzx63kcdxwb5a6q2zqnzybsdztwshparrxa" + ], + "p": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 1, + "t": "rot", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835456966756f706c70667a336e6266797067677a66697975677167616e7764636636756d6b75353273736d646b776269776c677a616161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b81582101645d64eb8032633240eb046568599a1d9a50bd6d8cebcc91152fed0a873a2a9b616e8178354570676c6264776c7473686f62686b65377a797a7836336b636478776235613671327a716e7a796273647a7477736870617272786161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161617301617463726f7461766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01eeefa445f91f5f20bbb7527e1de861c25b2340a1d62e1745088b4a80c5e45fc24579699118eff157fa24a5ad860fa36355b07145c49d669d3f6c90a40410a606", + "seed": "3f4eb93d3a6747aaddcb3a6e7539b8ada2c185a134b6f98ec6c2355b3d99c896" + } + ] + } + ], + "error": "pre_rotation_mismatch", + "error_at": 1 + }, + { + "name": "kel-008-rotation-signed-by-current-keys", + "description": "Alice's rotation, revealing the right key, but signed by her current key — what an adversary who has stolen the current key can produce. A rotation is signed by the keys it reveals (§3.5), so the signature does not verify. A verifier that took rotations as signed by the previous keys would accept it, and hand the stolen key control of every later rotation.", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + }, + { + "event": { + "a": [], + "d": "Easjfe7h6aloamed72k665rs6a7ekjrkkni3ffelnttgm3knjp5xa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + } + ], + "n": [ + "E645lnczbq6zrnrsaqq5ncuhmvsws4gsdjnc4hwvusflar5vtey4q" + ], + "p": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 1, + "t": "rot", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab616180616478354561736a6665376836616c6f616d656437326b3636357273366137656b6a726b6b6e69336666656c6e7474676d336b6e6a7035786161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c61723576746579347161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161617301617463726f7461766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "010fafd88564231c3584a0aab5b213b8ae0b6a3b959f89c498904cdfb0517c749e6dca748f3611d49d72230e6c12210045baff18e7dabe233ee54dbf29d8784d06", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + } + ], + "error": "signature_invalid", + "error_at": 1 + }, + { + "name": "kel-009-rotation-lowers-threshold", + "description": "The custodian's rotation rewritten to set its own threshold to 1, signed by one revealed key. The inception committed `nt` = 2, and a rotation must meet the greater of the committed threshold and its own (§3.5), so one signature is not enough: the threshold cannot be lowered without the pre-rotated keys.", + "events": [ + { + "event": { + "a": [], + "d": "Eqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "i": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "k": [ + { + "$bytes": "01f4ff420feb4040a73e51713a44f9966476c755e8e2d18b82677f8ff7a839afe2" + }, + { + "$bytes": "019b54b13b505ae2ddb12e49d8e10ed93725167ab9a0a77803538fc69858cdb0e2" + }, + { + "$bytes": "015c26708ee23779ee7b628241cc1735ed1ba4527639d7383bbc0ad9aa6295f535" + } + ], + "n": [ + "Efcf43zs566fqd5jlkxmbqnv2srtgk45uhenfpc4p5xzfkx7rdvma", + "Euaam4ynyfrxpmxiyogpumg3m6bdb4s5jigslwv3oqnp2gswv4buq", + "Ell3qkq2hpnwirn4m6hgulph32looiev6e5hgst2oc5c5f6jiwjjq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 2, + "nt": 2 + }, + "cbor_hex": "ab616180616478354571656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371616978354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371616b83582101f4ff420feb4040a73e51713a44f9966476c755e8e2d18b82677f8ff7a839afe25821019b54b13b505ae2ddb12e49d8e10ed93725167ab9a0a77803538fc69858cdb0e25821015c26708ee23779ee7b628241cc1735ed1ba4527639d7383bbc0ad9aa6295f535616e8378354566636634337a73353636667164356a6c6b786d62716e7632737274676b34357568656e667063347035787a666b78377264766d617835457561616d34796e79667278706d7869796f6770756d67336d366264623473356a6967736c7776336f716e703267737776346275717835456c6c33716b713268706e7769726e346d366867756c706833326c6f6f69657636653568677374326f6335633566366a69776a6a7161706061730061746369637061766b534259544531304a534f4e626b7402626e7402", + "signatures": [ + { + "index": 0, + "signature": "01e9f5ae4b064588588f8566045833d5860bd680c6fd7182246813fa73710393eec68a6a859931f597dc169950d4aa3539d1e7296372488821cf3b233e5eda940d", + "seed": "a13cea6aba73e2de6e8e19bfc0081a59de4305de0b91901fe325478164281227" + }, + { + "index": 1, + "signature": "01a2fb0fee6f1bfce74bc0b9e324fec7b3ee5415171740310a3fa769ff6aadd3ed3f75496a7eeb08dcf6db4e939810246c45a1c6f0474a64ec6e558cfe8e50910a", + "seed": "4ae1cd361d4765fca9649bbc4d8835a1594064b86c1b94e7412bb5d6e21e7d10" + } + ] + }, + { + "event": { + "a": [], + "d": "Ekbgyooe3f64aa37j3kthznsphv3stgqvcwfznz345sb2umicxhtq", + "i": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "k": [ + { + "$bytes": "01091c2155a4686c049d97ef032881a2291c303939534380064076194d1c354761" + }, + { + "$bytes": "011e0815c33d4a9b95e5097f32e2193330d24ee2505e20fdd02c51e2f6ff51c7eb" + }, + { + "$bytes": "017f90c4a1f622dd21d395b1dc30ad7077c5945223004b936516cfaf583fd1f669" + } + ], + "n": [ + "Elmjjmq2ykqtw34oj7yvm3gqib7yuns2brzoylucu3eklrohsprka", + "E4y4e4wj3os4engc5acjvutwgawrpcrsuvxm24emnk4bq5cgtxneq", + "Eqwtis3sxfytzis2yfoibeqmrfmqze4uzqa4lnp6rzzfvkmsfugfa" + ], + "p": "Eqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "s": 1, + "t": "rot", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835456b6267796f6f6533663634616133376a336b74687a6e737068763373746771766377667a6e7a333435736232756d696378687471616978354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371616b83582101091c2155a4686c049d97ef032881a2291c303939534380064076194d1c3547615821011e0815c33d4a9b95e5097f32e2193330d24ee2505e20fdd02c51e2f6ff51c7eb5821017f90c4a1f622dd21d395b1dc30ad7077c5945223004b936516cfaf583fd1f669616e837835456c6d6a6a6d7132796b71747733346f6a3779766d33677169623779756e733262727a6f796c75637533656b6c726f687370726b617835453479346534776a336f7334656e67633561636a7675747767617772706372737576786d3234656d6e6b34627135636774786e657178354571777469733373786679747a69733279666f696265716d72666d717a6534757a7161346c6e7036727a7a66766b6d736675676661617078354571656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371617301617463726f7461766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01ccf34074747a16450e1b7d40468ea72f67748e763c549d620a4f5d614280d462722e9d7caadce855fecaee221ef46f1a6a3c6ee75d50c5018cbf5a82df81820a", + "seed": "f47ee7427cb3c3fab47b0faaf284c71120522958a0484f9c790dc55e9a8980c7" + } + ] + } + ], + "error": "below_threshold", + "error_at": 1 + }, + { + "name": "kel-010-inception-below-threshold", + "description": "The custodian's inception signed by one of its three keys. Its own `kt` is 2, so it is refused.", + "events": [ + { + "event": { + "a": [], + "d": "Eqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "i": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "k": [ + { + "$bytes": "01f4ff420feb4040a73e51713a44f9966476c755e8e2d18b82677f8ff7a839afe2" + }, + { + "$bytes": "019b54b13b505ae2ddb12e49d8e10ed93725167ab9a0a77803538fc69858cdb0e2" + }, + { + "$bytes": "015c26708ee23779ee7b628241cc1735ed1ba4527639d7383bbc0ad9aa6295f535" + } + ], + "n": [ + "Efcf43zs566fqd5jlkxmbqnv2srtgk45uhenfpc4p5xzfkx7rdvma", + "Euaam4ynyfrxpmxiyogpumg3m6bdb4s5jigslwv3oqnp2gswv4buq", + "Ell3qkq2hpnwirn4m6hgulph32looiev6e5hgst2oc5c5f6jiwjjq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 2, + "nt": 2 + }, + "cbor_hex": "ab616180616478354571656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371616978354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371616b83582101f4ff420feb4040a73e51713a44f9966476c755e8e2d18b82677f8ff7a839afe25821019b54b13b505ae2ddb12e49d8e10ed93725167ab9a0a77803538fc69858cdb0e25821015c26708ee23779ee7b628241cc1735ed1ba4527639d7383bbc0ad9aa6295f535616e8378354566636634337a73353636667164356a6c6b786d62716e7632737274676b34357568656e667063347035787a666b78377264766d617835457561616d34796e79667278706d7869796f6770756d67336d366264623473356a6967736c7776336f716e703267737776346275717835456c6c33716b713268706e7769726e346d366867756c706833326c6f6f69657636653568677374326f6335633566366a69776a6a7161706061730061746369637061766b534259544531304a534f4e626b7402626e7402", + "signatures": [ + { + "index": 0, + "signature": "01e9f5ae4b064588588f8566045833d5860bd680c6fd7182246813fa73710393eec68a6a859931f597dc169950d4aa3539d1e7296372488821cf3b233e5eda940d", + "seed": "a13cea6aba73e2de6e8e19bfc0081a59de4305de0b91901fe325478164281227" + } + ] + } + ], + "error": "below_threshold", + "error_at": 0 + }, + { + "name": "kel-011-interaction-changes-keys", + "description": "Alice's inception followed by an interaction that writes her next key into `k`. An interaction changes no keys (§3.2); one that did would be a rotation without the pre-rotated keys. It is refused whatever signs it.", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + }, + { + "event": { + "a": [], + "d": "E3wt3nysw35m7qmwbkksri7dyn4i7qzuxnlhzjhneuvx5nz72w3dq", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 1, + "t": "ixn", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab6161806164783545337774336e79737733356d37716d77626b6b7372693764796e346937717a75786e6c687a6a686e65757678356e7a37327733647161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161730161746369786e61766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01675fe498fb84f64cbb962b69f4a51ce30b5667557e661472d761cc100f42868e98cac9fa91fe427c26d29088ef822539855938b4ba12bfa60646d467e2acfd09", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + } + ], + "error": "interaction_changes_keys", + "error_at": 1 + }, + { + "name": "kel-012-zero-threshold", + "description": "An inception with `kt` = 0 and no signatures. Zero valid signatures meet a threshold of zero, so the log would accept events no one signed. A threshold is at least 1 and at most the number of keys (§3.3).", + "events": [ + { + "event": { + "a": [], + "d": "Ec6j6gzqswolwgcjoqbei34r7uytwa5rldytzhhcnsdquof7b6kjq", + "i": "Bc6j6gzqswolwgcjoqbei34r7uytwa5rldytzhhcnsdquof7b6kjq", + "k": [ + { + "$bytes": "01f279fa4ab88670b2dc85d183537de7a24f7115b668c45fc0ff1f5d8073c721bc" + } + ], + "n": [ + "Epivxz62hmtnbf2zbmqmu4kg7fiqprpyur2im2uwoostt7chdvosq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 0, + "nt": 1 + }, + "cbor_hex": "ab616180616478354563366a36677a7173776f6c7767636a6f7162656933347237757974776135726c6479747a6868636e736471756f663762366b6a71616978354263366a36677a7173776f6c7767636a6f7162656933347237757974776135726c6479747a6868636e736471756f663762366b6a71616b81582101f279fa4ab88670b2dc85d183537de7a24f7115b668c45fc0ff1f5d8073c721bc616e81783545706976787a3632686d746e6266327a626d716d75346b673766697170727079757232696d3275776f6f73747437636864766f737161706061730061746369637061766b534259544531304a534f4e626b7400626e7401", + "signatures": [] + } + ], + "error": "invalid_threshold", + "error_at": 0 + }, + { + "name": "kel-013-duplicate-key", + "description": "An inception listing one key twice with `kt` = 2, signed twice by that key. Counted naively, one holder meets a two-key threshold alone. Keys in `k` are distinct (§3.3).", + "events": [ + { + "event": { + "a": [], + "d": "Epjhhcvsu5dpgvn4ssmuzxdgi6blhls4akqd6qkdzom5jjq4fjt5q", + "i": "Bpjhhcvsu5dpgvn4ssmuzxdgi6blhls4akqd6qkdzom5jjq4fjt5q", + "k": [ + { + "$bytes": "016853e0935f632b4795a27340e17ab3203bd06cb009026a4aa1ff0d6eb23148e2" + }, + { + "$bytes": "016853e0935f632b4795a27340e17ab3203bd06cb009026a4aa1ff0d6eb23148e2" + } + ], + "n": [ + "Ed6676ohjmth3ry2uau4oinnnymkqxretaavt3mj6na76y5ogvyaa" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 2, + "nt": 1 + }, + "cbor_hex": "ab6161806164783545706a68686376737535647067766e3473736d757a7864676936626c686c7334616b716436716b647a6f6d356a6a7134666a7435716169783542706a68686376737535647067766e3473736d757a7864676936626c686c7334616b716436716b647a6f6d356a6a7134666a743571616b825821016853e0935f632b4795a27340e17ab3203bd06cb009026a4aa1ff0d6eb23148e25821016853e0935f632b4795a27340e17ab3203bd06cb009026a4aa1ff0d6eb23148e2616e8178354564363637366f686a6d746833727932756175346f696e6e6e796d6b717872657461617674336d6a366e61373679356f677679616161706061730061746369637061766b534259544531304a534f4e626b7402626e7401", + "signatures": [ + { + "index": 0, + "signature": "01f3e15c05beac1737f3da25029151325cea87822b51215745edf1fa9e22357745c49f4c273e941729455056c11ae49cda98f53092012775abe799b0bcf5294800", + "seed": "1295f4e232533cf954ec289d63049edda72589bb4d573acc2f180b0f4fe59040" + }, + { + "index": 1, + "signature": "01f3e15c05beac1737f3da25029151325cea87822b51215745edf1fa9e22357745c49f4c273e941729455056c11ae49cda98f53092012775abe799b0bcf5294800", + "seed": "1295f4e232533cf954ec289d63049edda72589bb4d573acc2f180b0f4fe59040" + } + ] + } + ], + "error": "duplicate_key", + "error_at": 0 + }, + { + "name": "kel-014-another-controllers-event", + "description": "Alice's inception followed by an interaction naming the issuer's AID in `i`, signed with Alice's key. An event in a log bears that log's controller (§4 step 4).", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + }, + { + "event": { + "a": [], + "d": "E2pakkhrpr66uurwkgrtqn6nxdkawpmhilj5biuqilmzbmbnwnn7a", + "i": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 1, + "t": "ixn", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835453270616b6b687270723636757572776b677274716e366e78646b6177706d68696c6a3562697571696c6d7a626d626e776e6e3761616978354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161730161746369786e61766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "011e9ceec4c7607b65e907d6cf91621344f1dad3df6f6896dc61c1cdea563fe417a42a3c70e4f8239390dd6778ac30f466a6f5d04045012c6b2f6b649bbfcb2202", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + } + ], + "error": "controller_mismatch", + "error_at": 1 + }, + { + "name": "kel-015-inception-claims-another-aid", + "description": "Alice's inception with `i` set to the issuer's AID after stamping. `i` holds the placeholder while an inception is hashed, so its SAID still checks; but `i` must be the AID that SAID names (§3.1, §4 step 1), or anyone could incept keys of their own under another controller's identifier.", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616978354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "019e0c6c8ac29a363d21eabb42414882755d8709ea9620bd995db1119b6c3468d426d0452683b7e51e5aac0a39d6d1a297e1e0a640434a9fc66425d0a31d20140b", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + } + ], + "error": "controller_mismatch", + "error_at": 0 + }, + { + "name": "kel-016-changed-after-stamping", + "description": "Alice's log with the interaction's anchor changed after its SAID was computed, and re-signed. The bytes no longer hash to the SAID they state (§4 step 1).", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + }, + { + "event": { + "a": [], + "d": "Easjfe7h6aloamed72k665rs6a7ekjrkkni3ffelnttgm3knjp5xa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + } + ], + "n": [ + "E645lnczbq6zrnrsaqq5ncuhmvsws4gsdjnc4hwvusflar5vtey4q" + ], + "p": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 1, + "t": "rot", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab616180616478354561736a6665376836616c6f616d656437326b3636357273366137656b6a726b6b6e69336666656c6e7474676d336b6e6a7035786161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c61723576746579347161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161617301617463726f7461766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "0145034c547fe5ce1fdcbba545caea06462ef49eca66b066b43d4e9c1512a348d76958a819b463ad05bf4b550c5e3af22067aabda134cd91d60e6a961bf9468d08", + "seed": "94cc758a5ba6b9270d29eb7a6b5833b57d55573cd6a4b8ddc6a7408654809f31" + } + ] + }, + { + "event": { + "a": [ + "Eqwprmmibbnmcybbt2lam5jlgloljdxn2ct7jkrpytg66z3olltca" + ], + "d": "Ep3e57xvov2amfgreyzeym5nhfkfwka6uzldtetfxftq4dklimk2a", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + } + ], + "n": [ + "E645lnczbq6zrnrsaqq5ncuhmvsws4gsdjnc4hwvusflar5vtey4q" + ], + "p": "Easjfe7h6aloamed72k665rs6a7ekjrkkni3ffelnttgm3knjp5xa", + "s": 2, + "t": "ixn", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab616181783545717770726d6d6962626e6d6379626274326c616d356a6c676c6f6c6a64786e326374376a6b727079746736367a336f6c6c7463616164783545703365353778766f7632616d66677265797a65796d356e68666b66776b6136757a6c64746574667866747134646b6c696d6b326161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c617235767465793471617078354561736a6665376836616c6f616d656437326b3636357273366137656b6a726b6b6e69336666656c6e7474676d336b6e6a7035786161730261746369786e61766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01af53ec049d5017c9b4c762dcc0fb64fd9f577f9a25ff1327bb792d64837420e83ed21cbc9f065952363d53461f9c8e2c48e7bf3a59222d9d82572e65a381bc03", + "seed": "94cc758a5ba6b9270d29eb7a6b5833b57d55573cd6a4b8ddc6a7408654809f31" + } + ] + } + ], + "error": "said_mismatch", + "error_at": 2 + }, + { + "name": "kel-017-rotation-after-no-next-keys", + "description": "kel-004's inception, which committed to no next keys, followed by a rotation to a key of someone else's choosing, signed by that key. With no commitment there is nothing to reveal: the rotation is refused before its keys are compared (§4 step 5).", + "events": [ + { + "event": { + "a": [], + "d": "E5qm4bv4oymc45al2tx2loo7p7xctv6mhy4k6johqdvlwsdfgezja", + "i": "B5qm4bv4oymc45al2tx2loo7p7xctv6mhy4k6johqdvlwsdfgezja", + "k": [ + { + "$bytes": "01cd769ca0c08e672c895d07253992cc5423a488a66cb59b63e72c281c1806321f" + } + ], + "n": [], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 0 + }, + "cbor_hex": "ab616180616478354535716d346276346f796d633435616c327478326c6f6f37703778637476366d6879346b366a6f687164766c7773646667657a6a61616978354235716d346276346f796d633435616c327478326c6f6f37703778637476366d6879346b366a6f687164766c7773646667657a6a61616b81582101cd769ca0c08e672c895d07253992cc5423a488a66cb59b63e72c281c1806321f616e8061706061730061746369637061766b534259544531304a534f4e626b7401626e7400", + "signatures": [ + { + "index": 0, + "signature": "0120383bb4694d56707225a31421d9ce3814e9fe074515c7c487572411fd7e5607a883962379cfc17707235395388f7a2d99040a7eae0bd2e5b1f20946ebc7b004", + "seed": "3558491c31cef5ce59fb6d3cc2a9867fe091283f979beb4331008dd887c558a5" + } + ] + }, + { + "event": { + "a": [], + "d": "E45azstwgy4kpc65sb7mpx5crvhfhejcx4bsmdivgbpgcm5plcraa", + "i": "B5qm4bv4oymc45al2tx2loo7p7xctv6mhy4k6johqdvlwsdfgezja", + "k": [ + { + "$bytes": "01645d64eb8032633240eb046568599a1d9a50bd6d8cebcc91152fed0a873a2a9b" + } + ], + "n": [ + "Epglbdwltshobhke7zyzx63kcdxwb5a6q2zqnzybsdztwshparrxa" + ], + "p": "E5qm4bv4oymc45al2tx2loo7p7xctv6mhy4k6johqdvlwsdfgezja", + "s": 1, + "t": "rot", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835453435617a7374776779346b706336357362376d707835637276686668656a63783462736d64697667627067636d35706c63726161616978354235716d346276346f796d633435616c327478326c6f6f37703778637476366d6879346b366a6f687164766c7773646667657a6a61616b81582101645d64eb8032633240eb046568599a1d9a50bd6d8cebcc91152fed0a873a2a9b616e8178354570676c6264776c7473686f62686b65377a797a7836336b636478776235613671327a716e7a796273647a74777368706172727861617078354535716d346276346f796d633435616c327478326c6f6f37703778637476366d6879346b366a6f687164766c7773646667657a6a61617301617463726f7461766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01592bab146ac2d998357b3fefe1b5ab391acd66f28e3c638c83c8ff450d9922bf1e14a02d793e23ea23959f77706898961cf4998833161d40d6bf497c02808f0e", + "seed": "3f4eb93d3a6747aaddcb3a6e7539b8ada2c185a134b6f98ec6c2355b3d99c896" + } + ] + } + ], + "error": "rotation_not_permitted", + "error_at": 1 + }, + { + "name": "kel-018-sequence-gap", + "description": "Alice's inception followed by an interaction at sequence 2. Sequence numbers rise by exactly one (§4 step 2).", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + }, + { + "event": { + "a": [], + "d": "E23skfmftnvusqxutny5df37c7pflspytlr5as6kc4ixsksyrrzja", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 2, + "t": "ixn", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835453233736b666d66746e767573717875746e793564663337633770666c737079746c72356173366b63346978736b737972727a6a6161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161730261746369786e61766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "0182c4c8b15e14da35f68e250123425d67c015f02f9452f932bb1a02947043f0d295d74eb1476687ea71e9a38031a9fc0189c0da20fcf50a56cab4f410cd954901", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + } + ], + "error": "out_of_sequence", + "error_at": 1 + }, + { + "name": "kel-019-prior-mismatch", + "description": "Alice's inception followed by an interaction whose `p` names the issuer's inception. An event names the event before it in its own log (§4 step 3).", + "events": [ + { + "event": { + "a": [], + "d": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + }, + { + "event": { + "a": [], + "d": "Eolqqkzq7kmi3noosucjouf73ecdqlvbxnxecrkcpgzmlbscow3pa", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "k": [ + { + "$bytes": "013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c" + } + ], + "n": [ + "Eqmsawig6oznh4b5cbovqukvzxa5u5jbiklkrlwhtdo6rbrsyjfnq" + ], + "p": "Ei473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "s": 1, + "t": "ixn", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835456f6c71716b7a71376b6d69336e6f6f7375636a6f75663733656364716c7662786e786563726b6370677a6d6c6273636f7733706161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e71617078354569343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637161730161746369786e61766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "0175bd0cba5b544b568bd9f674ba8bd2323ef3f0c48ff0e011cd569800cc8808eb4bf1584b7740e4594974c847b99c5c2471440ff010096b6cb641d889f4d3f003", + "seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb" + } + ] + } + ], + "error": "prior_mismatch", + "error_at": 1 + }, + { + "name": "kel-020-non-canonical-event", + "description": "The issuer's inception with `s` written 0x18 0x00 instead of 0x00. The bytes are not canonical, so they are refused before anything is hashed (§4 step 1, §2.6).", + "events": [ + { + "cbor_hex": "ab616180616478354569343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616978354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c69697268633669766371616b81582101d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a616e8178354534666e766a79777a32337a65796234323663726f797663747274717373653233377468336b666a696434706665657778376e6f616170606173180061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "0111da3daede60eab5998787dba99327d3535bb8a3c718722d176309a31a5be9598f0095889219cfc18e729b267808e15034ef48d40b772a8d69c617799202340b", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + } + ] + } + ], + "error": "non_shortest_integer", + "error_at": 0 + }, + { + "name": "kel-021-undefined-member", + "description": "The issuer's inception with a member `x` added and its SAID and AID re-derived, so they check. An event has exactly the members §3.3 defines.", + "events": [ + { + "event": { + "a": [], + "d": "Evkvvymsxxz36ats2aym5gzu46jjut5meqqqphzpyod5drvfy2gda", + "i": "Bvkvvymsxxz36ats2aym5gzu46jjut5meqqqphzpyod5drvfy2gda", + "k": [ + { + "$bytes": "01d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + } + ], + "n": [ + "E4fnvjywz23zeyb426croyvctrtqsse237th3kfjid4pfeewx7noa" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "x": null, + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ac6161806164783545766b7676796d7378787a33366174733261796d35677a7534366a6a7574356d6571717170687a70796f64356472766679326764616169783542766b7676796d7378787a33366174733261796d35677a7534366a6a7574356d6571717170687a70796f6435647276667932676461616b81582101d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a616e8178354534666e766a79777a32337a65796234323663726f797663747274717373653233377468336b666a696434706665657778376e6f6161706061730061746369637061766b534259544531304a534f4e6178f6626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "011d78511474bdc296050f81b51c1fc5607f1170e4f7c17410dd8acf8bfec716d4fa70b967428997d152d6fd9b4ca9217f71e1745ff5fe10e31cae0f0f29bd2f04", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + } + ] + } + ], + "error": "malformed_event", + "error_at": 0 + }, + { + "name": "kel-022-key-written-as-integers", + "description": "The issuer's inception with its key written as an array of 33 small integers, not a byte string, and its SAID and AID re-derived over that form. The bytes are canonical CBOR and decode to the same key, but they are not the event's encoding — a byte string is (§3.3) — so a verifier that hashes what it decoded would accept a second encoding of the event.", + "events": [ + { + "event": { + "a": [], + "d": "Ejr6jz34msc4k24zldtg4c4tmrtaai3dbwrshaasicwyv3o4bt5pa", + "i": "Bjr6jz34msc4k24zldtg4c4tmrtaai3dbwrshaasicwyv3o4bt5pa", + "k": [ + [ + 1, + 215, + 90, + 152, + 1, + 130, + 177, + 10, + 183, + 213, + 75, + 254, + 211, + 201, + 100, + 7, + 58, + 14, + 225, + 114, + 243, + 218, + 166, + 35, + 37, + 175, + 2, + 26, + 104, + 247, + 7, + 81, + 26 + ] + ], + "n": [ + "E4fnvjywz23zeyb426croyvctrtqsse237th3kfjid4pfeewx7noa" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835456a72366a7a33346d7363346b32347a6c647467346334746d7274616169336462777273686161736963777976336f34627435706161697835426a72366a7a33346d7363346b32347a6c647467346334746d7274616169336462777273686161736963777976336f346274357061616b8198210118d7185a189801188218b10a18b718d5184b18fe18d318c9186407183a0e18e1187218f318da18a61823182518af02181a186818f7071851181a616e8178354534666e766a79777a32337a65796234323663726f797663747274717373653233377468336b666a696434706665657778376e6f6161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "014b2c9c60980504447af388173163e0ef9f41fbc9a7b9bde702f34e9824f664cdb91ec1415cb74df6bb9bc7f5ab1dcc5d1525bad1e40d619b53af68cc7eccf808", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + } + ] + } + ], + "error": "malformed_event", + "error_at": 0 + }, + { + "name": "kel-023-unsupported-version", + "description": "The issuer's inception with `v` = \"SBYTE20JSON\" and its SAID and AID re-derived. A v1 verifier speaks SBYTE10JSON (§3.3) and refuses any other version rather than guessing at it.", + "events": [ + { + "event": { + "a": [], + "d": "Ex3fnn5wqo5cmyisw5236qq3al57diinso7dnb5jiv6nbqgzu65ja", + "i": "Bx3fnn5wqo5cmyisw5236qq3al57diinso7dnb5jiv6nbqgzu65ja", + "k": [ + { + "$bytes": "01d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a" + } + ], + "n": [ + "E4fnvjywz23zeyb426croyvctrtqsse237th3kfjid4pfeewx7noa" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE20JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835457833666e6e3577716f35636d7969737735323336717133616c35376469696e736f37646e62356a6976366e6271677a7536356a6161697835427833666e6e3577716f35636d7969737735323336717133616c35376469696e736f37646e62356a6976366e6271677a7536356a61616b81582101d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a616e8178354534666e766a79777a32337a65796234323663726f797663747274717373653233377468336b666a696434706665657778376e6f6161706061730061746369637061766b534259544532304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "0122187643cc8bbbd2fa86508db60739000a8726f61e76faf4bdbeff4975864e6f8359c566820177a8468bcaee2b664ca573f2f507dadbf288fb09e23a915c0701", + "seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60" + } + ] + } + ], + "error": "unsupported_version", + "error_at": 0 + }, + { + "name": "kel-024-reserved-algorithm", + "description": "An inception whose key carries algorithm identifier 0x10, which identity spec §3.3 reserves for post-quantum schemes, signed with a signature carrying the same identifier. A v1 verifier verifies Ed25519 (0x01) and refuses the rest.", + "events": [ + { + "event": { + "a": [], + "d": "Eib2bdrm6fhj6td3q2ixz2cwbvlypyo7trmxdrkzvny5txwmyynba", + "i": "Bib2bdrm6fhj6td3q2ixz2cwbvlypyo7trmxdrkzvny5txwmyynba", + "k": [ + { + "$bytes": "105a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a" + } + ], + "n": [ + "Eqr7tdqfzgr7pomht3jnax7y7b6azij2nh3dfoaq6gwg5y5poynbq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ab61618061647835456962326264726d3666686a36746433713269787a32637762766c7970796f3774726d7864726b7a766e79357478776d79796e626161697835426962326264726d3666686a36746433713269787a32637762766c7970796f3774726d7864726b7a766e79357478776d79796e6261616b815821105a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a616e81783545717237746471667a677237706f6d6874336a6e61783779376236617a696a326e683364666f617136677767357935706f796e627161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "10a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5", + "seed": null + } + ] + } + ], + "error": "unsupported_algorithm", + "error_at": 0 + }, + { + "name": "kel-025-witness-pool-refused", + "description": "An inception naming a witness pool (`b`, `bt`). Witnessing is not part of spec v1: an event has exactly the members §3.3 defines, so a v1 verifier refuses it.", + "events": [ + { + "event": { + "a": [], + "b": [ + "Wnen35b4pxjysi3lrfyfkyvfae7v3ejjzil2ca6arsc4udbx3bm7a" + ], + "d": "Eqm2urp43aqysvhvo67frp2ougcgqturbuqyma5x77belmoeq52hq", + "i": "Bqm2urp43aqysvhvo67frp2ougcgqturbuqyma5x77belmoeq52hq", + "k": [ + { + "$bytes": "018dcc81f536fd5d7f90373a3c35fdfcb6f7bc1f0d7c84d3695a8952f34eae545b" + } + ], + "n": [ + "Eibdqwy5a7b45qbqeugt53o4ikqjyd6uemivxsxtx3knmxih3fvjq" + ], + "p": "", + "s": 0, + "t": "icp", + "v": "SBYTE10JSON", + "bt": 1, + "kt": 1, + "nt": 1 + }, + "cbor_hex": "ad6161806162817835576e656e3335623470786a797369336c726679666b7976666165377633656a6a7a696c3263613661727363347564627833626d37616164783545716d327572703433617179737668766f3637667270326f7567636771747572627571796d613578373762656c6d6f6571353268716169783542716d327572703433617179737668766f3637667270326f7567636771747572627571796d613578373762656c6d6f657135326871616b815821018dcc81f536fd5d7f90373a3c35fdfcb6f7bc1f0d7c84d3695a8952f34eae545b616e817835456962647177793561376234357162716575677435336f34696b716a79643675656d69767873787478336b6e6d7869683366766a7161706061730061746369637061766b534259544531304a534f4e62627401626b7401626e7401", + "signatures": [ + { + "index": 0, + "signature": "01b5e35673bf3dda02c34361593ce642701fc5e4bed1e5fcfab910e66e8d5a1746455eccb9070fa901d715c9c6157d0912ac7c1c296475e97769e1a56b5a1a080b", + "seed": "7ca0cc6713caeb098451d3134cd56f84d2b1a38c3c2adeb6e835f5d895615547" + } + ] + } + ], + "error": "malformed_event", + "error_at": 0 + } +] diff --git a/smart-byte/conformance/v1/regeneration.md b/smart-byte/conformance/v1/regeneration.md index 0142c418c..8e0839d75 100644 --- a/smart-byte/conformance/v1/regeneration.md +++ b/smart-byte/conformance/v1/regeneration.md @@ -6,13 +6,16 @@ placeholder. - **Generator and verifier:** the `smart-byte-conformance` crate in `../../smart-byte-rs`. Names, descriptions and inputs are written in its - `src/pack.rs`; every expected value is computed at generation time by - `smart-byte-core` (canonical CBOR, SAIDs, signatures) and - `smart-byte-lockstep` (the commit rule and dissent log). + `src/pack.rs` and `src/logs.rs`, and the parties the vectors are built from + in `src/world.rs`. Every expected value is computed at generation time by + `smart-byte-core` (canonical CBOR, SAIDs, signatures, envelopes), + `smart-byte-keri-witness` (key-event logs) and `smart-byte-lockstep` (the + commit rule and dissent log). - **Second implementation:** `../crosscheck/verify_v1.py`, Python standard library only, written from the spec text in another language. It implements BLAKE3 and Ed25519 itself and checks both against known answers before it - checks any vector. + checks any vector, then replays key-event logs and verifies envelopes as + the specs describe. ## Procedure @@ -34,7 +37,8 @@ python3 ../conformance/crosscheck/verify_v1.py ../conformance/v1 `gen-conformance-vectors --check` writes nothing and exits non-zero if any file differs from the generator's output. -To change a vector, edit `smart-byte-conformance/src/pack.rs` and regenerate. +To change a vector, edit `smart-byte-conformance/src/pack.rs` (or `logs.rs`, +`world.rs`) and regenerate. Editing the JSON by hand fails CI: `cargo test` in `smart-byte-rs` requires the published files to equal the generator's output byte for byte, verifies every vector, and plants a defect in each file to prove the verifier would catch it. @@ -53,27 +57,48 @@ The conformance gate runs the second implementation on every pack change. base32 is `expected_said`, and the object with its SAID in place is `said_cbor_hex`, which must verify. A negative vector's `input_cbor_hex` is refused by SAID verification with its `error` class. -3. **Signature vectors.** The signed message is the 32 digest bytes a SAID - spells (`signed_message_hex`), not its 53-character text. A positive +3. **Signature vectors.** The message is the canonical encoding of what is + signed (`message_hex`): an issuer's attestation, a key event. A positive vector's `signature` is the deterministic Ed25519 signature by `signing_key_seed`; a negative vector is refused with its `error` class. -4. **Lockstep vectors.** The hashes nodes reported are tallied against the +4. **Key-event-log vectors.** Each log is replayed by the reference's spec-v1 + verifier, from its bytes. An accepted log's `expected` is what the replay + establishes; a refused log names the class and the event (`error_at`) + refused, and generation fails if the reference refuses it anywhere else + or for any other reason. Every signature with a `seed` is the one that + seed's key makes. +5. **Envelope vectors.** Each envelope is decoded and verified by the + reference against the key states that kel-001 to kel-003 establish. An + accepted envelope's `expected` is what verification establishes; a refused + one names its class. +6. **Lockstep vectors.** The hashes nodes reported are tallied against the threshold floor(2n/3)+1 (treatise §13.1) for the frame's public record. Each node's outcome (§13.3) and dissent log (§14.1) come from its own view: its peers' reported hashes plus the hash it computed, so a node that lies on the wire (§13.4) is modelled exactly as the treatise describes. -5. **Gossip vectors.** Each delivery goes through `smart-byte-core`'s inbox, +7. **Gossip vectors.** Each delivery goes through `smart-byte-core`'s inbox, which implements identity spec §2.6: canonical bytes, then the SAID they state, then the SAID they arrived under, and only then deduplication. ## Invariant: cross-vector consistency -- Every `envelope_said` in `signature_vectors.json` is the `expected_said` of a - vector in `said_vectors.json` — the verifier and the second implementation - both enforce it. +The files describe one set of parties — an issuer, Alice and a custodian — +and the byte that passes between them. + +- said-001 to said-003 are the SAIDs of env-001's snapshot, its identity and + env-002's snapshot; said-004 and said-005 are kel-002's rotation and + interaction. said-007's key is the issuer's. +- sig-001 is the issuer's signature in env-001, and sig-002 is the signature + on kel-002's inception, each without its algorithm identifier. +- kel-002's interaction anchors env-001's identity. +- Every envelope vector is checked against the key states the accepted logs + it names establish. Both checkers enforce this: a log that fails takes the + envelope vectors that rely on it down with it. - Gossip deliveries carry SAID vectors' `said_cbor_hex` bytes, except where a vector alters them on purpose and says so. +`cargo test` checks the first three in `tests/pack.rs`. + ## Cross-language check This document has always required a second implementation to reproduce the @@ -86,6 +111,128 @@ this document planned do not exist. ## Editorial change log +### 2026-09-30 — key-event logs and envelopes, on the v1 wire format + +The pack gains `kel_vectors.json` (25 vectors) and `envelope_vectors.json` +(18), and the SAID and signature vectors that described the draft envelope +are rebuilt on the real one. v1 is DRAFT, so this is a regeneration in place. + +**What the new files pin.** Key-event logs replayed per identity spec §4, and +envelopes per the new envelope wire format. Each negative vector carries one +defect, and the reference refuses it for that defect alone. The defects: + +- **Takeovers:** + - a rotation to uncommitted keys; + - a rotation signed by the current keys rather than the keys it reveals; + - a rotation that lowers the committed threshold; + - a rotation after a log committed to no next keys; + - an interaction that changes keys; + - an inception claiming another controller's AID; + - a `rec` that reveals keys of its own. +- **Thresholds and keys:** a threshold of 0, which no signature needs to + meet; a key listed twice, which one holder could use to meet a two-key + threshold. +- **Log structure:** a sequence gap, a wrong prior event, another + controller's event, a changed event, and the `rec` and `dip` types v1 does + not define. +- **Decoding:** + - non-canonical bytes; + - an undefined member; + - a member written in another form; + - an unsupported version; + - a reserved algorithm; + - a witness pool. +- **Envelopes:** + - cargo changed after signing, with and without re-stamping; + - a stale snapshot SAID; + - a non-owner's transfer; + - a grafted transition; + - two transfers in one frame; + - a signature naming a non-establishment event; + - an old key under a new establishment; + - below-threshold, duplicate-index, out-of-range and wrong-algorithm + signatures; + - non-canonical bytes; + - an undefined member; + - cargo bytes written as an array of integers. + +**Rebuilt on the v1 wire format:** + +- said-001 to said-005 were objects shaped like the draft envelope, which + had `alg`, `issuer_pubkey` and a top-level `cargo`. That shape no longer + exists. They are now the issued envelope, its identity, the transferred + envelope, and a rotation and an interaction event, and they carry those + objects' real SAIDs. +- Signature vectors signed the 32 digest bytes of a SAID. Nothing in v1 signs + that: an issuer signs its attested fields, an owner its transition, a + controller its event, each as canonical CBOR. The vectors now sign real + messages (`message_hex`), and `envelope_said` and `signed_message_hex` are + gone. sig-003 is renamed `message-swapped`. + +**Found and fixed in the reference while building the pack:** + +- `wire::from_slice` accepted canonical CBOR that was not the decoded + object's own encoding. Serde takes an array of integers for a byte string, + a byte string for text, and an array for a struct, and it skips a member + written where it is omitted. Each gave one object a second byte string its + SAID does not cover. The decoder now re-encodes and refuses anything but + its input, which covers every type at once; kel-022 and env-018 pin it. +- The key-event-log verifier accepted all of the following: + - a `dip` event at any sequence, with no pre-rotation check, which is a + takeover path; + - thresholds of 0; + - duplicate keys; + - rotations after an empty `n`; + - every extension member, in spec-v1 mode. + + It now replays incrementally and reports which event it refused. Its + error classes match the README's. +- The extension mode is fixed as well: + - **Witness receipts** carried their own public key, so anyone could + receipt in any witness's name. A witness is now named by its key. A + receipt signs the controller, sequence and event, and counts only if + its witness is in the log's pool. + - **Duplicity evidence** was gathered from unverified receipts. + - **Recovery** accepted any keys a `rec` revealed. It now counts only with + its witnesses' receipts. + - **OOBIs** were signed by a key embedded in the OOBI itself. They are + now controller signatures, checked against the controller's log. + - **Configuration traits** (`c`) were never enforced, so they are gone. +- A controller signature with a key state threshold of 0 needed no + signatures. At least one is now always required. +- `pre_rotation_mismatch_is_rejected` asserted nothing: its `matches!` + result was discarded. + +**Spec text changed with it:** + +- `envelope_wire_format.md` is new: the envelope, its signatures and its + verification, with the error classes the pack uses. +- Identity spec: + - §3.1 says an inception placeholders `i` with `d`. + - §3.3 says an event has exactly its members, gives the threshold and + distinct-key rules, and drops the extension note. + - §3.5 adopts KERI's rule: a rotation is signed by the keys it reveals, + under both the committed and its own threshold. + - §4 steps 1, 5 and 6 match. + - §2.5 and §6 name the new vectors. + +**Falsified:** + +- **Rust checker:** 28 planted defects across the seven files, each caught + on its own vector. A defect in a log fails that log and every envelope + that relies on it. +- **Python checker:** 20 planted defects caught the same way, with and + without `-O`. +- **Python rules:** 30 rule mutations, each caught by the vector built for + it. +- **Rust rules:** 12 of the reference's rules mutated one at a time, + among them the rotation rules, the own-encoding check, duplicate keys and + indexes, frame order, the inception's AID, the version, and `rec`/`dip`. + Each mutant compiles, and each fails named tests. All but one break the + pack itself: generation refuses to emit a vector the reference no longer + refuses for its stated reason. The exception is a key state with + threshold 0, which no valid log can produce; a unit test holds that rule. + ### 2026-09-30 — the pack regenerated from the reference implementation Every `expected_*` value was a placeholder. The SAIDs were runs of one repeated diff --git a/smart-byte/conformance/v1/said_vectors.json b/smart-byte/conformance/v1/said_vectors.json index 7f580dfa7..476731cbc 100644 --- a/smart-byte/conformance/v1/said_vectors.json +++ b/smart-byte/conformance/v1/said_vectors.json @@ -1,172 +1,243 @@ [ { - "name": "said-001-empty-envelope", - "description": "A nested object with an envelope's top-level members — provenance, ownership chain, cargo, joule cost — all empty or zero. It exercises the whole pipeline (placeholder, canonical CBOR, BLAKE3-256, `E` + base32) on an object with no application data.", - "said_field": "d", - "input": { - "d": "#####################################################", - "provenance": null, - "ownership_chain": [], - "cargo": null, - "joule_cost": { - "measured_microjoules": 0, - "estimated_microjoules": 0 - } - }, - "canonical_cbor_hex": "a561647835232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232365636172676ff66a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730075657374696d617465645f6d6963726f6a6f756c6573006a70726f76656e616e6365f66f6f776e6572736869705f636861696e80", - "blake3_hex": "f715776741e2a8a4157d584ed68c4574b4185c693bfb635ff93efa48be55d736", - "expected_said": "E64kxoz2b4kukifl5lbhnndcfos2bqxdjhp5wgx7zh35erpsv243a", - "said_cbor_hex": "a5616478354536346b786f7a3262346b756b69666c356c62686e6e6463666f7332627178646a687035776778377a68333565727073763234336165636172676ff66a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730075657374696d617465645f6d6963726f6a6f756c6573006a70726f76656e616e6365f66f6f776e6572736869705f636861696e80" - }, - { - "name": "said-002-usd-cargo", - "description": "The same object carrying a cargo map: a USD amount of 100.00 (100000000 micros) and a 32-byte issuer key. Members of mixed types — text, integer, byte string — all sort by their encoded keys.", - "said_field": "d", - "input": { - "d": "#####################################################", - "provenance": null, - "ownership_chain": [], - "cargo": { - "type": "usd.cargo.v1", - "denomination_micros": 100000000, - "issuer_pubkey": { - "$bytes": "1111111111111111111111111111111111111111111111111111111111111111" - } - }, - "joule_cost": { - "measured_microjoules": 12, - "estimated_microjoules": 15 - } - }, - "canonical_cbor_hex": "a561647835232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232365636172676fa364747970656c7573642e636172676f2e76316d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111117364656e6f6d696e6174696f6e5f6d6963726f731a05f5e1006a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730c75657374696d617465645f6d6963726f6a6f756c65730f6a70726f76656e616e6365f66f6f776e6572736869705f636861696e80", - "blake3_hex": "98560e8b8d05e5125c055df313b3e58bd411888644d463b965f084c9f9167baf", - "expected_said": "Etbla5c4naxsrexaflxzrhm7frpkbdcegitkgholf6ccmt6iwpoxq", - "said_cbor_hex": "a5616478354574626c613563346e61787372657861666c787a72686d376672706b626463656769746b67686f6c663663636d74366977706f787165636172676fa364747970656c7573642e636172676f2e76316d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111117364656e6f6d696e6174696f6e5f6d6963726f731a05f5e1006a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730c75657374696d617465645f6d6963726f6a6f756c65730f6a70726f76656e616e6365f66f6f776e6572736869705f636861696e80" - }, - { - "name": "said-003-custom-cargo", - "description": "The same shape carrying a temperature reading as cargo. What a member means never affects its encoding: this cargo goes through the pipeline exactly as said-002's does.", - "said_field": "d", - "input": { - "d": "#####################################################", - "provenance": null, - "ownership_chain": [], - "cargo": { - "type": "sensor.temperature.v1", - "sensor_id": "rooftop-array-7", - "celsius_milli": 23475, - "reading_frame": 184236 - }, - "joule_cost": { - "measured_microjoules": 8, - "estimated_microjoules": 10 - } - }, - "canonical_cbor_hex": "a561647835232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232365636172676fa464747970657573656e736f722e74656d70657261747572652e76316973656e736f725f69646f726f6f66746f702d61727261792d376d63656c736975735f6d696c6c69195bb36d72656164696e675f6672616d651a0002cfac6a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730875657374696d617465645f6d6963726f6a6f756c65730a6a70726f76656e616e6365f66f6f776e6572736869705f636861696e80", - "blake3_hex": "30b1d5aad87f38aa3014135de70a49cd79c9df422fc62eeb3ef7d94bc3ec970f", - "expected_said": "Egcy5lkwyp44kumaucno6ocsjzv44tx2cf7dc52z667muxq7ms4hq", - "said_cbor_hex": "a56164783545676379356c6b77797034346b756d6175636e6f366f63736a7a763434747832636637646335327a3636376d757871376d7334687165636172676fa464747970657573656e736f722e74656d70657261747572652e76316973656e736f725f69646f726f6f66746f702d61727261792d376d63656c736975735f6d696c6c69195bb36d72656164696e675f6672616d651a0002cfac6a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730875657374696d617465645f6d6963726f6a6f756c65730a6a70726f76656e616e6365f66f6f776e6572736869705f636861696e80" - }, - { - "name": "said-004-with-provenance", - "description": "The same shape with a provenance map: algorithm identifier 0x01 (Ed25519), issuer key, issuance frame and signature. The SAID covers every member. The key and signature are fixed filler bytes: this vector tests SAID derivation over them, not their validity.", + "name": "said-001-issued-envelope", + "description": "env-001, the USD byte as the issuer issued it to Alice, with its SAID — the snapshot SAID — replaced by the placeholder. The provenance inside carries the byte's identity (said-002) in its own `d`, which is part of what is hashed here. Nested maps, byte strings, text and integers all go through one canonical encoding.", "said_field": "d", "input": { "d": "#####################################################", "provenance": { - "alg": 1, - "issuer_pubkey": { - "$bytes": "1111111111111111111111111111111111111111111111111111111111111111" + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } }, - "issuance_frame": 1024, "signature": { - "$bytes": "22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222" + "establishment": 0, + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ] } }, "ownership_chain": [], - "cargo": { - "type": "usd.cargo.v1", - "denomination_micros": 100000000, - "issuer_pubkey": { - "$bytes": "1111111111111111111111111111111111111111111111111111111111111111" - } - }, "joule_cost": { - "measured_microjoules": 14, - "estimated_microjoules": 18 + "measured_microjoules": 1200, + "estimated_microjoules": 1500 } }, - "canonical_cbor_hex": "a561647835232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232365636172676fa364747970656c7573642e636172676f2e76316d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111117364656e6f6d696e6174696f6e5f6d6963726f731a05f5e1006a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730e75657374696d617465645f6d6963726f6a6f756c6573126a70726f76656e616e6365a463616c6701697369676e61747572655840222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222226d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111116e69737375616e63655f6672616d651904006f6f776e6572736869705f636861696e80", - "blake3_hex": "24bef3e8d603a964c5de687b67d86eb9d77c100d6be2ff85f19284e6d023f03a", - "expected_said": "Ees7ph2gwaouwjro6nb5wpwdoxhlxyeannprp7bprskconubd6a5a", - "said_cbor_hex": "a561647835456573377068326777616f75776a726f366e6235777077646f78686c787965616e6e70727037627072736b636f6e7562643661356165636172676fa364747970656c7573642e636172676f2e76316d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111117364656e6f6d696e6174696f6e5f6d6963726f731a05f5e1006a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65730e75657374696d617465645f6d6963726f6a6f756c6573126a70726f76656e616e6365a463616c6701697369676e61747572655840222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222226d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111116e69737375616e63655f6672616d651904006f6f776e6572736869705f636861696e80" + "canonical_cbor_hex": "a46164783523232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323236a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80", + "blake3_hex": "8c168dbeed0949943f8fa3c97e61d6635ff40b199282e180409808fc4e2c415e", + "expected_said": "Erqli3pxnbfezip4pupex4yowmnp7icyzskbodacataepytrmifpa", + "said_cbor_hex": "a4616478354572716c693370786e6266657a697034707570657834796f776d6e70376963797a736b626f64616361746165707974726d696670616a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e80" }, { - "name": "said-005-with-ownership-transitions", - "description": "said-004's object with two ownership transitions appended and a larger joule cost: an array of maps holding byte strings. The first transition's previous_history_hash is the digest of said-004's SAID; the second is blake3 of that digest followed by the first transition's canonical encoding. The SAID differs from said-004's because a SAID covers the whole object it is derived over. A byte's identity is a different quantity — the hash of its origin attestation (treatise §8.2) — and does not change as transitions accrue.", + "name": "said-002-byte-identity", + "description": "env-001's provenance — the issuer's attestation to the cargo, the recipient and the nonce, with the issuer's signature — with its `d` replaced by the placeholder. Its SAID is the byte's identity (treatise §8.2), fixed at issuance: no transfer changes it.", + "said_field": "d", + "input": { + "d": "#####################################################", + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } + }, + "signature": { + "establishment": 0, + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ] + } + }, + "canonical_cbor_hex": "a661647835232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232365636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e7400", + "blake3_hex": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f", + "expected_said": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "said_cbor_hex": "a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e7400" + }, + { + "name": "said-003-transferred-envelope", + "description": "env-002, the same byte after three transfers. The identity inside is said-002's, unchanged; the snapshot SAID differs from said-001's because a SAID covers the whole object it is derived over, ownership chain included.", "said_field": "d", "input": { "d": "#####################################################", "provenance": { - "alg": 1, - "issuer_pubkey": { - "$bytes": "1111111111111111111111111111111111111111111111111111111111111111" + "d": "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q", + "issuer": "Bi473abxg23bmfn2aud3q6xot7apm3d6oca5zjaadlliirhc6ivcq", + "recipient": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "nonce": { + "$bytes": "dcc74ebbcedebda361fb18a7abec95d9" + }, + "cargo": { + "type": 1, + "bytes": { + "$bytes": "a1666d6963726f731a05f5e100" + } }, - "issuance_frame": 1024, "signature": { - "$bytes": "22222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222" + "establishment": 0, + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09" + } + } + ] } }, "ownership_chain": [ { - "alg": 1, - "from_pubkey": { - "$bytes": "1111111111111111111111111111111111111111111111111111111111111111" - }, - "to_pubkey": { - "$bytes": "3333333333333333333333333333333333333333333333333333333333333333" - }, - "frame": 1050, "previous_history_hash": { - "$bytes": "24bef3e8d603a964c5de687b67d86eb9d77c100d6be2ff85f19284e6d023f03a" + "$bytes": "c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7f" }, + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 10, "signature": { - "$bytes": "44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444" + "establishment": 0, + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "0176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb108" + } + } + ] } }, { - "alg": 1, - "from_pubkey": { - "$bytes": "3333333333333333333333333333333333333333333333333333333333333333" - }, - "to_pubkey": { - "$bytes": "5555555555555555555555555555555555555555555555555555555555555555" - }, - "frame": 1080, "previous_history_hash": { - "$bytes": "48dd9261e8f0836e26d963e2924a0441c2ad1620654cbe76159581d10775aa15" + "$bytes": "0a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834" }, + "from": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "to": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "frame": 11, "signature": { - "$bytes": "66666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666" + "establishment": 0, + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0f" + } + }, + { + "index": 2, + "signature": { + "$bytes": "0197288385a060626b2866f8ba49334223985a5120cffeab84f65e87573d410a705488f43a6ffd61de745b1b5134b71a030d4ae98c398ef2e6cb0ffd2d5655b500" + } + } + ] + } + }, + { + "previous_history_hash": { + "$bytes": "53b295243fb7fad0619379612a2d1906fc17b8da522bee8d43545d4f0e738766" + }, + "from": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "to": "Bqeerhhfrn2brgrbslwilntkcdfcngatst3j5acuu6krzsggi2tcq", + "frame": 12, + "signature": { + "establishment": 1, + "signatures": [ + { + "index": 0, + "signature": { + "$bytes": "01854eb95c94a56944912a25634fac3bd3a2ade24ccea7da3a733154fb106cfa0264d0df79f8b0344f591e2256a060f851613724a82bb5ae467a8cbd28771be205" + } + } + ] } } ], - "cargo": { - "type": "usd.cargo.v1", - "denomination_micros": 100000000, - "issuer_pubkey": { - "$bytes": "1111111111111111111111111111111111111111111111111111111111111111" - } - }, "joule_cost": { - "measured_microjoules": 47, - "estimated_microjoules": 56 + "measured_microjoules": 1200, + "estimated_microjoules": 1500 } }, - "canonical_cbor_hex": "a561647835232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232365636172676fa364747970656c7573642e636172676f2e76316d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111117364656e6f6d696e6174696f6e5f6d6963726f731a05f5e1006a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c6573182f75657374696d617465645f6d6963726f6a6f756c657318386a70726f76656e616e6365a463616c6701697369676e61747572655840222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222226d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111116e69737375616e63655f6672616d651904006f6f776e6572736869705f636861696e82a663616c6701656672616d6519041a697369676e617475726558404444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444469746f5f7075626b6579582033333333333333333333333333333333333333333333333333333333333333336b66726f6d5f7075626b6579582011111111111111111111111111111111111111111111111111111111111111117570726576696f75735f686973746f72795f68617368582024bef3e8d603a964c5de687b67d86eb9d77c100d6be2ff85f19284e6d023f03aa663616c6701656672616d65190438697369676e617475726558406666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666669746f5f7075626b6579582055555555555555555555555555555555555555555555555555555555555555556b66726f6d5f7075626b6579582033333333333333333333333333333333333333333333333333333333333333337570726576696f75735f686973746f72795f68617368582048dd9261e8f0836e26d963e2924a0441c2ad1620654cbe76159581d10775aa15", - "blake3_hex": "c670b45906645f9144fc20b1638b48ac183fe09f70ed6903ebc10283083901f2", - "expected_said": "Eyzyliwigmrpzcrh4ecywhc2ivqmd7ye7odwwsa7lyebigcbzahza", - "said_cbor_hex": "a56164783545797a796c697769676d72707a63726834656379776863326976716d64377965376f6477777361376c796562696763627a61687a6165636172676fa364747970656c7573642e636172676f2e76316d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111117364656e6f6d696e6174696f6e5f6d6963726f731a05f5e1006a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c6573182f75657374696d617465645f6d6963726f6a6f756c657318386a70726f76656e616e6365a463616c6701697369676e61747572655840222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222226d6973737565725f7075626b6579582011111111111111111111111111111111111111111111111111111111111111116e69737375616e63655f6672616d651904006f6f776e6572736869705f636861696e82a663616c6701656672616d6519041a697369676e617475726558404444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444469746f5f7075626b6579582033333333333333333333333333333333333333333333333333333333333333336b66726f6d5f7075626b6579582011111111111111111111111111111111111111111111111111111111111111117570726576696f75735f686973746f72795f68617368582024bef3e8d603a964c5de687b67d86eb9d77c100d6be2ff85f19284e6d023f03aa663616c6701656672616d65190438697369676e617475726558406666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666669746f5f7075626b6579582055555555555555555555555555555555555555555555555555555555555555556b66726f6d5f7075626b6579582033333333333333333333333333333333333333333333333333333333333333337570726576696f75735f686973746f72795f68617368582048dd9261e8f0836e26d963e2924a0441c2ad1620654cbe76159581d10775aa15" + "canonical_cbor_hex": "a46164783523232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323236a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e83a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7fa562746f7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f71616466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650b697369676e6174757265a26a7369676e61747572657382a265696e64657800697369676e6174757265584101a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0fa265696e64657802697369676e617475726558410197288385a060626b2866f8ba49334223985a5120cffeab84f65e87573d410a705488f43a6ffd61de745b1b5134b71a030d4ae98c398ef2e6cb0ffd2d5655b5006d65737461626c6973686d656e74007570726576696f75735f686973746f72795f6861736858200a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650c697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e6174757265584101854eb95c94a56944912a25634fac3bd3a2ade24ccea7da3a733154fb106cfa0264d0df79f8b0344f591e2256a060f851613724a82bb5ae467a8cbd28771be2056d65737461626c6973686d656e74017570726576696f75735f686973746f72795f68617368582053b295243fb7fad0619379612a2d1906fc17b8da522bee8d43545d4f0e738766", + "blake3_hex": "fab1dd13396833938e5a56a5d6142037dcf122b9763e42878c1bcdd1e22da3af", + "expected_said": "E7ky52ezznazzhds2k2s5mfbag7opcivzoy7efb4mdpg5dyrnuoxq", + "said_cbor_hex": "a46164783545376b793532657a7a6e617a7a686473326b3273356d66626167376f706369767a6f7937656662346d647067356479726e756f78716a6a6f756c655f636f7374a2746d656173757265645f6d6963726f6a6f756c65731904b075657374696d617465645f6d6963726f6a6f756c65731905dc6a70726f76656e616e6365a66164783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377165636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410188d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd096d65737461626c6973686d656e74006f6f776e6572736869705f636861696e83a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650a697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e617475726558410176f343a82d949a1f89606e53e69ca78896b8a7189bdfd1a8581d463e58cbb63a6d911fab2eb99fede19edef4f6c4376cfc2abb6b1bd2e6f3d179e9da916cb1086d65737461626c6973686d656e74007570726576696f75735f686973746f72795f686173685820c2f400d0e1c8507d81df3d52c34d38cb381ea49428f202b7cf61369f207dde7fa562746f7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f71616466726f6d78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a7367676932746371656672616d650b697369676e6174757265a26a7369676e61747572657382a265696e64657800697369676e6174757265584101a5c67191eb55e8d29cf30026e03ea5e66d827af43c413c73d250c059573156cb4bca8998e70d4a3f0a8745e4ad287007459eac1e6fef223b8b18589b796ebc0fa265696e64657802697369676e617475726558410197288385a060626b2866f8ba49334223985a5120cffeab84f65e87573d410a705488f43a6ffd61de745b1b5134b71a030d4ae98c398ef2e6cb0ffd2d5655b5006d65737461626c6973686d656e74007570726576696f75735f686973746f72795f6861736858200a2a977471247d441ef3b766422a7eea835cfdaab6f8b390697a055ba0234834a562746f78354271656572686866726e326272677262736c77696c6e746b636466636e6761747374336a3561637575366b727a73676769327463716466726f6d7835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161656672616d650c697369676e6174757265a26a7369676e61747572657381a265696e64657800697369676e6174757265584101854eb95c94a56944912a25634fac3bd3a2ade24ccea7da3a733154fb106cfa0264d0df79f8b0344f591e2256a060f851613724a82bb5ae467a8cbd28771be2056d65737461626c6973686d656e74017570726576696f75735f686973746f72795f68617368582053b295243fb7fad0619379612a2d1906fc17b8da522bee8d43545d4f0e738766" + }, + { + "name": "said-004-rotation-event", + "description": "Alice's rotation event (kel-002, event 1) with its `d` replaced by the placeholder. A rotation's `i` is the controller's AID, written out; only an inception's own `i` is placeholdered too (§3.1).", + "said_field": "d", + "input": { + "t": "rot", + "v": "SBYTE10JSON", + "d": "#####################################################", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 1, + "p": "Ezxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "kt": 1, + "k": [ + { + "$bytes": "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + } + ], + "nt": 1, + "n": [ + "E645lnczbq6zrnrsaqq5ncuhmvsws4gsdjnc4hwvusflar5vtey4q" + ], + "a": [] + }, + "canonical_cbor_hex": "ab61618061647835232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232361697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c61723576746579347161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161617301617463726f7461766b534259544531304a534f4e626b7401626e7401", + "blake3_hex": "0492527cfe02dc06107fd2bdeec65e07c8a4c54a6a3652916d9ccccda9a97f6e", + "expected_said": "Easjfe7h6aloamed72k665rs6a7ekjrkkni3ffelnttgm3knjp5xa", + "said_cbor_hex": "ab616180616478354561736a6665376836616c6f616d656437326b3636357273366137656b6a726b6b6e69336666656c6e7474676d336b6e6a7035786161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c61723576746579347161707835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161617301617463726f7461766b534259544531304a534f4e626b7401626e7401" + }, + { + "name": "said-005-interaction-event", + "description": "Alice's interaction event (kel-002, event 2), whose anchor is said-002's SAID: the identity of the byte she was issued.", + "said_field": "d", + "input": { + "t": "ixn", + "v": "SBYTE10JSON", + "d": "#####################################################", + "i": "Bzxpecfxm3pdlfe6xm6ps45kvhdxielo55ktsj5ndknyhgnr72oqa", + "s": 2, + "p": "Easjfe7h6aloamed72k665rs6a7ekjrkkni3ffelnttgm3knjp5xa", + "kt": 1, + "k": [ + { + "$bytes": "012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0" + } + ], + "nt": 1, + "n": [ + "E645lnczbq6zrnrsaqq5ncuhmvsws4gsdjnc4hwvusflar5vtey4q" + ], + "a": [ + "Eyl2abuhbzbih3ao7hvjmgtjyzm4b5jeufdzafn6pme3j6id53z7q" + ] + }, + "canonical_cbor_hex": "ab616181783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a377161647835232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232323232361697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c617235767465793471617078354561736a6665376836616c6f616d656437326b3636357273366137656b6a726b6b6e69336666656c6e7474676d336b6e6a7035786161730261746369786e61766b534259544531304a534f4e626b7401626e7401", + "blake3_hex": "7ec9dfdeaeae80c29a24c6498675a72a8b6503d4cac7324cb72ce1c1a96862b4", + "expected_said": "Ep3e57xvov2amfgreyzeym5nhfkfwka6uzldtetfxftq4dklimk2a", + "said_cbor_hex": "ab616181783545796c3261627568627a62696833616f3768766a6d67746a797a6d3462356a657566647a61666e36706d65336a36696435337a37716164783545703365353778766f7632616d66677265797a65796d356e68666b66776b6136757a6c64746574667866747134646b6c696d6b326161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821012354b51b90e2fe316f648ff7456178869ba360edee56635d0676fd54a9227ef0616e817835453634356c6e637a6271367a726e7273617171356e6375686d76737773346773646a6e63346877767573666c617235767465793471617078354561736a6665376836616c6f616d656437326b3636357273366137656b6a726b6b6e69336666656c6e7474676d336b6e6a7035786161730261746369786e61766b534259544531304a534f4e626b7401626e7401" }, { "name": "said-006-only-said-field", @@ -182,7 +253,7 @@ }, { "name": "said-007-one-field-per-key", - "description": "The one-field case: identity spec §3.4's per-key object {d, k}, whose SAID is a next-key commitment. k is a byte string: the algorithm identifier 0x01 (Ed25519) followed by the public key of RFC 8032's first test seed (§3.3).", + "description": "The one-field case: identity spec §3.4's per-key object {d, k}, whose SAID is a next-key commitment. k is a byte string: the algorithm identifier 0x01 (Ed25519) followed by the public key of RFC 8032's first test seed (§3.3) — the issuer's key in kel-001.", "said_field": "d", "input": { "d": "#####################################################", diff --git a/smart-byte/conformance/v1/signature_vectors.json b/smart-byte/conformance/v1/signature_vectors.json index 979433acb..7d0ae331a 100644 --- a/smart-byte/conformance/v1/signature_vectors.json +++ b/smart-byte/conformance/v1/signature_vectors.json @@ -1,78 +1,71 @@ [ { - "name": "sig-001-known-key-known-said", - "description": "Ed25519 (RFC 8032) is deterministic: signing said-002's SAID with the key of RFC 8032's first test seed gives exactly this signature. The message is the 32 digest bytes the SAID spells (signed_message_hex), not its 53-character text.", - "envelope_said": "Etbla5c4naxsrexaflxzrhm7frpkbdcegitkgholf6ccmt6iwpoxq", - "signed_message_hex": "98560e8b8d05e5125c055df313b3e58bd411888644d463b965f084c9f9167baf", + "name": "sig-001-issuer-attestation", + "description": "What the issuer signs to issue env-001: the canonical encoding of {cargo, issuer, nonce, recipient} (envelope wire format §3). The key is RFC 8032's first test key, and Ed25519 is deterministic, so this is the one signature that key makes over this message — the 64 bytes after the algorithm identifier in env-001's provenance signature.", + "message_hex": "a465636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161", "signing_key_seed": "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60", "verifying_key": "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a", - "signature": "ceadf887b7391770a3c4a4a4cf2062f2e41db1447851cf2a843bf7b7718ae7ec8a534846a1bf46fbdedf3f9ee4a5832c9406cf33fd598fe6d520f98946e09b04", + "signature": "88d2bbf7bf3ea95bb33866235bae150ac65c7371bc2776f17eb0944884cc8a4e7d402947d334d07e871acd0c305e6791d6e9b5747a683b6c58fc81f21dbcdd09", "expected_verify": true, "error": null }, { - "name": "sig-002-verify-positive", - "description": "A signature over said-001's SAID by the key of RFC 8032's second test seed. It verifies under that key.", - "envelope_said": "E64kxoz2b4kukifl5lbhnndcfos2bqxdjhp5wgx7zh35erpsv243a", - "signed_message_hex": "f715776741e2a8a4157d584ed68c4574b4185c693bfb635ff93efa48be55d736", + "name": "sig-002-key-event", + "description": "Alice's inception event (kel-002, event 0), signed by the key it reveals: RFC 8032's second test key. The message is the event's canonical encoding (identity spec §3.5).", + "message_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", "signing_key_seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", "verifying_key": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", - "signature": "ce9b9e7185967276bebe6a9c12f3570d821bcb7184e9c703bd278c1fa44ece65c0fb5b0855fe3fca5eb836d3f41b568a230af3156da0440cfaf5d4acd7123506", + "signature": "c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", "expected_verify": true, "error": null }, { - "name": "sig-003-tampered-envelope", - "description": "sig-002's signature presented with said-003's SAID: an adversary swaps the object and forwards the original signature. The signature is well-formed but binds another SAID, so verification MUST fail.", - "envelope_said": "Egcy5lkwyp44kumaucno6ocsjzv44tx2cf7dc52z667muxq7ms4hq", - "signed_message_hex": "30b1d5aad87f38aa3014135de70a49cd79c9df422fc62eeb3ef7d94bc3ec970f", + "name": "sig-003-message-swapped", + "description": "sig-002's signature presented with sig-001's message: an adversary swaps the message and forwards the original signature. The signature is well-formed but binds another message, so verification MUST fail.", + "message_hex": "a465636172676fa26474797065016562797465734da1666d6963726f731a05f5e100656e6f6e636550dcc74ebbcedebda361fb18a7abec95d96669737375657278354269343733616278673233626d666e32617564337136786f743761706d3364366f6361357a6a6161646c6c6969726863366976637169726563697069656e747835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161", "signing_key_seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", "verifying_key": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", - "signature": "ce9b9e7185967276bebe6a9c12f3570d821bcb7184e9c703bd278c1fa44ece65c0fb5b0855fe3fca5eb836d3f41b568a230af3156da0440cfaf5d4acd7123506", + "signature": "c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", "expected_verify": false, "error": "signature_invalid" }, { "name": "sig-004-wrong-key", - "description": "sig-002's signature over said-001's SAID, presented with another key — the one the all-zero seed derives. The signature is valid for the key that made it, which is not this one, so verification MUST fail.", - "envelope_said": "E64kxoz2b4kukifl5lbhnndcfos2bqxdjhp5wgx7zh35erpsv243a", - "signed_message_hex": "f715776741e2a8a4157d584ed68c4574b4185c693bfb635ff93efa48be55d736", + "description": "sig-002's message and signature, presented with another key — the one the all-zero seed derives. The signature is valid for the key that made it, which is not this one, so verification MUST fail.", + "message_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", "signing_key_seed": "0000000000000000000000000000000000000000000000000000000000000000", "verifying_key": "3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29", - "signature": "ce9b9e7185967276bebe6a9c12f3570d821bcb7184e9c703bd278c1fa44ece65c0fb5b0855fe3fca5eb836d3f41b568a230af3156da0440cfaf5d4acd7123506", + "signature": "c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", "expected_verify": false, "error": "signature_invalid" }, { "name": "sig-005-malformed-signature", "description": "sig-002's signature cut to 63 bytes. It is refused as malformed before any cryptography runs, and an implementation MUST NOT panic on it.", - "envelope_said": "E64kxoz2b4kukifl5lbhnndcfos2bqxdjhp5wgx7zh35erpsv243a", - "signed_message_hex": "f715776741e2a8a4157d584ed68c4574b4185c693bfb635ff93efa48be55d736", + "message_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", "signing_key_seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", "verifying_key": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", - "signature": "ce9b9e7185967276bebe6a9c12f3570d821bcb7184e9c703bd278c1fa44ece65c0fb5b0855fe3fca5eb836d3f41b568a230af3156da0440cfaf5d4acd71235", + "signature": "c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da", "expected_verify": false, "error": "malformed_signature" }, { "name": "sig-006-non-canonical-s-rejected", "description": "sig-002's signature with its scalar S replaced by S + L: the same scalar, written outside [0, L). RFC 8032 §5.1.7 requires refusing it; accepting it would make signatures malleable, giving a second valid-looking signature for one message and key.", - "envelope_said": "E64kxoz2b4kukifl5lbhnndcfos2bqxdjhp5wgx7zh35erpsv243a", - "signed_message_hex": "f715776741e2a8a4157d584ed68c4574b4185c693bfb635ff93efa48be55d736", + "message_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", "signing_key_seed": "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb", "verifying_key": "3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c", - "signature": "ce9b9e7185967276bebe6a9c12f3570d821bcb7184e9c703bd278c1fa44ece65adcf51656f61522235552e76d315359f230af3156da0440cfaf5d4acd7123516", + "signature": "c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9956e6bf8a27dee613185405d2be39db464da3f4efcb5f935c0c20723ab68da17", "expected_verify": false, "error": "signature_invalid" }, { "name": "sig-007-non-canonical-key-rejected", "description": "A verifying key spelled with y = p + 3: an out-of-range encoding of the valid, large-order point with y = 3. RFC 8032 §5.1.3 requires decoding to fail when y is not below p, so the key is refused as malformed before any verification runs — one key has one spelling. There is no seed: no key pair has this spelling.", - "envelope_said": "E64kxoz2b4kukifl5lbhnndcfos2bqxdjhp5wgx7zh35erpsv243a", - "signed_message_hex": "f715776741e2a8a4157d584ed68c4574b4185c693bfb635ff93efa48be55d736", + "message_hex": "ab61618061647835457a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f716161697835427a7870656366786d3370646c666536786d36707334356b7668647869656c6f35356b74736a356e646b6e7968676e7237326f7161616b815821013d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c616e81783545716d7361776967366f7a6e6834623563626f7671756b767a78613575356a62696b6c6b726c776874646f3672627273796a666e7161706061730061746369637061766b534259544531304a534f4e626b7401626e7401", "signing_key_seed": null, "verifying_key": "f0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f", - "signature": "ce9b9e7185967276bebe6a9c12f3570d821bcb7184e9c703bd278c1fa44ece65c0fb5b0855fe3fca5eb836d3f41b568a230af3156da0440cfaf5d4acd7123506", + "signature": "c0999c04eee90f056e29a2753f1a90665636394e819578d4cc2a2a40afb841b9a89a759b881adc095be848ba4ce9be9f64da3f4efcb5f935c0c20723ab68da07", "expected_verify": false, "error": "malformed_verifying_key" } diff --git a/smart-byte/smart-byte-rs/Cargo.lock b/smart-byte/smart-byte-rs/Cargo.lock index 0b5924726..9e936db07 100644 --- a/smart-byte/smart-byte-rs/Cargo.lock +++ b/smart-byte/smart-byte-rs/Cargo.lock @@ -5845,9 +5845,8 @@ name = "smart-byte" version = "0.1.0" dependencies = [ "anyhow", - "chrono", + "blake3", "clap", - "serde_cbor", "smart-byte-core", "smart-byte-lockstep", "smart-byte-net", @@ -5950,6 +5949,7 @@ dependencies = [ "serde", "serde_json", "smart-byte-core", + "smart-byte-keri-witness", "smart-byte-lockstep", ] @@ -5965,6 +5965,7 @@ dependencies = [ "serde", "serde_bytes", "serde_cbor", + "serde_json", "thiserror 2.0.18", "uuid", ] @@ -6076,7 +6077,6 @@ name = "smart-byte-keri-witness" version = "0.1.0" dependencies = [ "async-trait", - "blake3", "chrono", "dashmap", "data-encoding", @@ -6085,13 +6085,9 @@ dependencies = [ "rand 0.8.6", "serde", "serde_bytes", - "serde_cbor", "smart-byte-core", - "smart-byte-pq", "thiserror 2.0.18", "tokio", - "tracing", - "uuid", ] [[package]] @@ -6120,7 +6116,6 @@ version = "0.1.0" dependencies = [ "blake3", "serde", - "serde_cbor", "smart-byte-core", "thiserror 2.0.18", "tokio", diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/cargo_bridge.rs b/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/cargo_bridge.rs index 4ae68326a..8c54d3f76 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/cargo_bridge.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/cargo_bridge.rs @@ -1,63 +1,44 @@ //! Smart Byte cargo bridge for BBS+-signed credentials. //! -//! Following the pattern from `smart-byte-vc::cargo_bridge`, a BBS+ -//! credential is packaged as `Cargo::Custom { type_uri, body }` -//! where: +//! A BBS+ credential travels as cargo of the registered type +//! [`BBS_CREDENTIAL_CARGO_TYPE`], whose bytes are the canonical encoding of +//! [`BbsCredentialBody`]; a selective-disclosure presentation travels as +//! [`BBS_PRESENTATION_CARGO_TYPE`] cargo holding a [`BbsPresentationBody`]. //! -//! * `type_uri` is the constant [`BBS_CREDENTIAL_CARGO_TYPE_URI`]. -//! * `body` is the canonical CBOR encoding of [`BbsCredentialBody`]. +//! ## One credential, many presentations //! -//! ## SAID stability under disclosure +//! A holder may derive many selective-disclosure proofs from one signed +//! credential, each with different cryptographic artefacts (unlinkability) +//! and a smaller message set. Putting them inside the credential's byte would +//! make the credential's identity change with every disclosure — the wrong +//! model: the *credential* is one content-addressed object, and +//! presentations are derived views. So there are two kinds of byte: //! -//! A BBS+ holder may emit *many* selective-disclosure proofs from a -//! single signed credential. Each proof has different cryptographic -//! artefacts (unlinkability) and a smaller message set. If the -//! disclosure proofs were placed directly inside the envelope's -//! cargo, the envelope SAID would mutate with every disclosure, -//! which is the wrong identity model: the *credential* is one -//! content-addressed object; presentations are derived views. +//! 1. **Credential.** Carries [`BbsCredentialBody`] — issuance proof, claim +//! values, public key. Its identity is fixed at issuance. +//! 2. **Presentation.** Carries [`BbsPresentationBody`] — the credential's +//! identity and a selective-disclosure proof. Each presentation is a byte +//! of its own; the credential identity it references is the same in all. //! -//! Smart Byte therefore distinguishes two envelope flavours: -//! -//! 1. **Credential envelope.** Carries [`BbsCredentialBody`] — -//! issuance proof + claim values + public key. This is the -//! SAID-stable, canonically addressed object. -//! 2. **Presentation envelope.** Carries [`BbsPresentationBody`] — -//! references the credential envelope's SAID and embeds a -//! selective-disclosure proof. Its own SAID is fresh per -//! presentation; the *underlying* credential SAID it references -//! is stable. -//! -//! The canonical re-encoding rule that preserves the credential SAID -//! under disclosure is: *the CBOR body of a presentation envelope -//! MUST always be derived from the byte-identical CBOR body of the -//! referenced credential envelope — never re-serialised from a parsed -//! intermediate.* This rule is enforced by -//! [`presentation_envelope`] which takes the parsed credential -//! envelope and reuses its `cargo.body` bytes byte-for-byte when -//! constructing the presentation. +//! [`presentation_envelope`] takes the credential byte itself and refers to +//! it by identity, so no presentation ever depends on re-serialising a parsed +//! credential. use serde::{Deserialize, Serialize}; -use smart_byte_core::{ - Cargo, Envelope, JouleCost, OwnershipChain, Provenance, Said, -}; +use smart_byte_core::cargo::types; +use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, Said, Signer, wire}; -use crate::cryptosuite::{ - Bbs2023DisclosureProof, Bbs2023IssuanceProof, -}; +use crate::cryptosuite::{Bbs2023DisclosureProof, Bbs2023IssuanceProof}; use crate::error::BbsError; use crate::keys::PublicKey; -/// Cargo `type_uri` for a BBS+-signed credential envelope. -pub const BBS_CREDENTIAL_CARGO_TYPE_URI: &str = - "urn:smart-byte:cargo:bbs-credential:v1"; +/// The registered cargo type of a BBS+-signed credential. +pub const BBS_CREDENTIAL_CARGO_TYPE: u64 = types::BBS_CREDENTIAL; -/// Cargo `type_uri` for a BBS+ selective-disclosure presentation -/// envelope. -pub const BBS_PRESENTATION_CARGO_TYPE_URI: &str = - "urn:smart-byte:cargo:bbs-presentation:v1"; +/// The registered cargo type of a BBS+ selective-disclosure presentation. +pub const BBS_PRESENTATION_CARGO_TYPE: u64 = types::BBS_PRESENTATION; -/// The CBOR body of a BBS+ credential envelope. +/// The cargo of a BBS+ credential byte. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BbsCredentialBody { /// Cryptosuite identifier, e.g. `"bbs-2023"`. @@ -70,13 +51,13 @@ pub struct BbsCredentialBody { pub issuance: Bbs2023IssuanceProof, } -/// The CBOR body of a BBS+ presentation envelope. +/// The cargo of a BBS+ presentation byte. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BbsPresentationBody { - /// SAID of the credential envelope this presentation derives from. - pub credential_said: Said, + /// The identity of the credential byte this presentation derives from. + pub credential: Said, /// Issuer public key (so verifiers need not dereference the - /// credential envelope to verify the proof). + /// credential to verify the proof). pub public_key: PublicKey, /// Verifier-supplied nonce that was bound into the proof. #[serde(with = "serde_bytes")] @@ -85,14 +66,37 @@ pub struct BbsPresentationBody { pub disclosure: Bbs2023DisclosureProof, } -/// Build a credential envelope from a BBS+ issuance. +fn bridge(e: impl std::fmt::Display) -> BbsError { + BbsError::Bridge(e.to_string()) +} + +fn cargo_body( + envelope: &Envelope, + type_tag: u64, + what: &str, +) -> Result { + let cargo = &envelope.provenance.cargo; + if cargo.type_tag != type_tag { + return Err(BbsError::Bridge(format!( + "cargo type {} ({}) is not a {what}", + cargo.type_tag, + cargo.kind() + ))); + } + wire::from_slice(&cargo.bytes).map_err(bridge) +} + +/// Issue a BBS+ credential as a smart byte: `issuer` attests to it for +/// `recipient`. +#[allow(clippy::too_many_arguments)] pub fn bbs_credential_envelope( cryptosuite: impl Into, public_key: PublicKey, claims: Vec>, issuance: Bbs2023IssuanceProof, - provenance: Provenance, - ownership: OwnershipChain, + issuer: &Signer, + recipient: Aid, + nonce: [u8; 16], joule_cost: JouleCost, ) -> Result { let body = BbsCredentialBody { @@ -101,85 +105,45 @@ pub fn bbs_credential_envelope( claims, issuance, }; - let body_bytes = serde_cbor::to_vec(&body)?; - let cargo = Cargo::Custom { - type_uri: BBS_CREDENTIAL_CARGO_TYPE_URI.to_string(), - body: body_bytes, - }; - Envelope::new(provenance, ownership, cargo, joule_cost) - .map_err(|e| BbsError::Bridge(e.to_string())) + let cargo = Cargo::new(BBS_CREDENTIAL_CARGO_TYPE, wire::to_vec(&body).map_err(bridge)?); + Ok(Envelope::issue(issuer, recipient, nonce, cargo, joule_cost)) } -/// Recover the BBS+ credential body from an envelope. -pub fn bbs_credential_from_envelope( - envelope: &Envelope, -) -> Result { - match &envelope.cargo { - Cargo::Custom { type_uri, body } if type_uri == BBS_CREDENTIAL_CARGO_TYPE_URI => { - Ok(serde_cbor::from_slice(body)?) - } - Cargo::Custom { type_uri, .. } => Err(BbsError::Bridge(format!( - "unexpected cargo type_uri: {type_uri}" - ))), - other => Err(BbsError::Bridge(format!( - "cargo kind {} is not a BBS credential", - other.kind() - ))), - } +/// Recover the BBS+ credential body from a credential byte. +pub fn bbs_credential_from_envelope(envelope: &Envelope) -> Result { + cargo_body(envelope, BBS_CREDENTIAL_CARGO_TYPE, "BBS credential") } -/// Build a presentation envelope from a credential envelope plus a -/// disclosure proof. +/// Issue a presentation byte derived from `credential`, carrying a +/// disclosure proof bound to the verifier's `proof_nonce`. /// -/// The presentation's body records the *referenced credential SAID* -/// (so verifiers know which credential the proof is derived from) -/// and the disclosure artefact. This envelope has its own SAID -/// which intentionally differs from the credential's; the credential -/// SAID remains stable across any number of presentations. +/// The presentation refers to the credential by identity, which no +/// presentation, transfer or disclosure ever changes. +#[allow(clippy::too_many_arguments)] pub fn presentation_envelope( - credential_envelope: &Envelope, + credential: &Envelope, public_key: PublicKey, - nonce: Vec, + proof_nonce: Vec, disclosure: Bbs2023DisclosureProof, - provenance: Provenance, - ownership: OwnershipChain, + presenter: &Signer, + recipient: Aid, + nonce: [u8; 16], joule_cost: JouleCost, ) -> Result { - // Confirm the referenced envelope is a BBS credential — this also - // exercises the canonical re-encoding rule because the verifier - // never has to re-serialise the credential body. - let _ = bbs_credential_from_envelope(credential_envelope)?; + bbs_credential_from_envelope(credential)?; let body = BbsPresentationBody { - credential_said: credential_envelope.id, + credential: credential.identity(), public_key, - nonce, + nonce: proof_nonce, disclosure, }; - let body_bytes = serde_cbor::to_vec(&body)?; - let cargo = Cargo::Custom { - type_uri: BBS_PRESENTATION_CARGO_TYPE_URI.to_string(), - body: body_bytes, - }; - Envelope::new(provenance, ownership, cargo, joule_cost) - .map_err(|e| BbsError::Bridge(e.to_string())) + let cargo = Cargo::new(BBS_PRESENTATION_CARGO_TYPE, wire::to_vec(&body).map_err(bridge)?); + Ok(Envelope::issue(presenter, recipient, nonce, cargo, joule_cost)) } -/// Recover a presentation body from an envelope. -pub fn presentation_from_envelope( - envelope: &Envelope, -) -> Result { - match &envelope.cargo { - Cargo::Custom { type_uri, body } if type_uri == BBS_PRESENTATION_CARGO_TYPE_URI => { - Ok(serde_cbor::from_slice(body)?) - } - Cargo::Custom { type_uri, .. } => Err(BbsError::Bridge(format!( - "unexpected cargo type_uri: {type_uri}" - ))), - other => Err(BbsError::Bridge(format!( - "cargo kind {} is not a BBS presentation", - other.kind() - ))), - } +/// Recover a presentation body from a presentation byte. +pub fn presentation_from_envelope(envelope: &Envelope) -> Result { + cargo_body(envelope, BBS_PRESENTATION_CARGO_TYPE, "BBS presentation") } #[cfg(test)] @@ -189,18 +153,37 @@ mod tests { create_bbs_2023_disclosure, issue_bbs_2023, }; use crate::keys::keygen; - use chrono::TimeZone; use rand::rngs::OsRng; - use smart_byte_core::Said; + use smart_byte_core::{KeyBook, SigningKey}; - fn fixture_provenance() -> Provenance { - Provenance::new( - Said::hash(b"bbs-issuer"), - chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap(), - b"bbs-auth".to_vec(), + fn issuer() -> Signer { + Signer::single( + Aid::of_inception(Said::hash(b"bbs-issuer")), + 0, + SigningKey::from_bytes(&[0x33; 32]), ) } + fn holder() -> Signer { + Signer::single( + Aid::of_inception(Said::hash(b"bbs-holder")), + 0, + SigningKey::from_bytes(&[0x44; 32]), + ) + } + + fn verifier() -> Aid { + Aid::of_inception(Said::hash(b"bbs-verifier")) + } + + fn keys() -> KeyBook { + let mut book = KeyBook::new(); + for s in [issuer(), holder()] { + book.insert(s.aid, 0, s.key_state()); + } + book + } + #[test] fn credential_envelope_roundtrip() { let kp = keygen(&mut OsRng); @@ -213,12 +196,13 @@ mod tests { kp.public.clone(), claim_vecs.clone(), iss.clone(), - fixture_provenance(), - OwnershipChain::empty(), + &issuer(), + holder().aid, + [1; 16], JouleCost::measured(11), ) .unwrap(); - env.verify_said().unwrap(); + env.verify(&keys()).unwrap(); let back = bbs_credential_from_envelope(&env).unwrap(); assert_eq!(back.public_key, kp.public); assert_eq!(back.claims, claim_vecs); @@ -237,13 +221,14 @@ mod tests { kp.public.clone(), claim_vecs, iss.clone(), - fixture_provenance(), - OwnershipChain::empty(), + &issuer(), + holder().aid, + [2; 16], JouleCost::measured(7), ) .unwrap(); - let original_said = cred.id; - cred.verify_said().unwrap(); + let original_said = cred.identity(); + cred.verify(&keys()).unwrap(); // Build two distinct presentations from the same credential. let disc_a = create_bbs_2023_disclosure( @@ -269,8 +254,9 @@ mod tests { kp.public.clone(), b"nonce-a".to_vec(), disc_a, - fixture_provenance(), - OwnershipChain::empty(), + &holder(), + verifier(), + [3; 16], JouleCost::measured(1), ) .unwrap(); @@ -279,35 +265,37 @@ mod tests { kp.public.clone(), b"nonce-b".to_vec(), disc_b, - fixture_provenance(), - OwnershipChain::empty(), + &holder(), + verifier(), + [4; 16], JouleCost::measured(1), ) .unwrap(); - // Credential SAID unchanged after both presentations exist. - assert_eq!(cred.id, original_said); + // The credential's identity is unchanged after both presentations exist. + assert_eq!(cred.identity(), original_said); // Both presentations reference the same credential SAID. let body_a = presentation_from_envelope(&pres_a).unwrap(); let body_b = presentation_from_envelope(&pres_b).unwrap(); - assert_eq!(body_a.credential_said, original_said); - assert_eq!(body_b.credential_said, original_said); + assert_eq!(body_a.credential, original_said); + assert_eq!(body_b.credential, original_said); - // The presentations themselves have distinct SAIDs. - assert_ne!(pres_a.id, pres_b.id); + // The presentations are bytes of their own. + assert_ne!(pres_a.identity(), pres_b.identity()); + pres_a.verify(&keys()).unwrap(); } #[test] fn presentation_rejects_non_credential_envelope() { let kp = keygen(&mut OsRng); - let not_a_cred = Envelope::new( - fixture_provenance(), - OwnershipChain::empty(), - Cargo::Bytes(vec![1, 2, 3]), + let not_a_cred = Envelope::issue( + &issuer(), + holder().aid, + [5; 16], + Cargo::opaque(vec![1, 2, 3]), JouleCost::default(), - ) - .unwrap(); + ); let dummy_iss = issue_bbs_2023(&[b"x"], &kp.secret, &kp.public).unwrap(); let dummy_disc = create_bbs_2023_disclosure( &dummy_iss, @@ -323,8 +311,9 @@ mod tests { kp.public.clone(), b"n".to_vec(), dummy_disc, - fixture_provenance(), - OwnershipChain::empty(), + &holder(), + verifier(), + [6; 16], JouleCost::default(), ) .unwrap_err(); diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/lib.rs b/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/lib.rs index cd5958619..4d20daf20 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/lib.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-bbs/src/lib.rs @@ -52,7 +52,7 @@ pub use anoncreds_bridge::{ RevocationHandle, }; pub use cargo_bridge::{ - BBS_CREDENTIAL_CARGO_TYPE_URI, BBS_PRESENTATION_CARGO_TYPE_URI, + BBS_CREDENTIAL_CARGO_TYPE, BBS_PRESENTATION_CARGO_TYPE, BbsCredentialBody, BbsPresentationBody, bbs_credential_envelope, bbs_credential_from_envelope, presentation_envelope, presentation_from_envelope, diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/Cargo.toml b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/Cargo.toml index 345b80b49..2feeb98ac 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/Cargo.toml +++ b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/Cargo.toml @@ -10,6 +10,7 @@ publish = false [dependencies] smart-byte-core = { workspace = true } +smart-byte-keri-witness = { path = "../smart-byte-keri-witness" } smart-byte-lockstep = { workspace = true } serde = { workspace = true } serde_json = "1" diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/lib.rs b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/lib.rs index 217093d47..5a447283d 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/lib.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/lib.rs @@ -8,8 +8,10 @@ //! so CI fails if the pack and the implementation ever disagree. pub mod json; +pub mod logs; pub mod pack; pub mod verify; +pub mod world; use std::path::PathBuf; diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/logs.rs b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/logs.rs new file mode 100644 index 000000000..3c27d45e7 --- /dev/null +++ b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/logs.rs @@ -0,0 +1,787 @@ +//! `kel_vectors.json` and `envelope_vectors.json`: key-event logs replayed +//! per identity spec §4, and envelopes verified per the envelope wire format +//! against the key states those logs establish. +//! +//! Each vector is built from [`World`]: its parties' logs and bytes, or one of +//! them with a single defect. As in the rest of the pack, every expected value +//! and error class is what the reference computes, and generation fails if +//! the reference refuses a negative vector for any reason but the one named. + +use std::collections::HashMap; + +use smart_byte_core::canonical::{self, Value}; +use smart_byte_core::ownership::Transition; +use smart_byte_core::said::{self, Derived, INCEPTION_AID, SAID_FIELD}; +use smart_byte_core::{Aid, Cargo, Envelope, KeyBook, Said, wire}; +use smart_byte_keri_witness::events::{ + DelegationEvent, InceptionEvent, InteractionEvent, KeyEvent, PublicKey, RecoveryEvent, + Threshold, WitnessAid, finalize_event, +}; +use smart_byte_keri_witness::{LogVerifier, VERSION_STRING}; + +use crate::json::{J, to_json}; +use crate::world::{ + ALICE_LOG, CUSTODIAN_LOG, EventSignature, ISSUER_LOG, Key, LoggedEvent, World, inception, + interaction, rotation, wire_log, +}; + +// --------------------------------------------------------------------------- +// kel_vectors.json +// --------------------------------------------------------------------------- + +fn event_json(e: &LoggedEvent) -> J { + let mut entries = Vec::new(); + if let Ok(value) = canonical::decode(&e.bytes) { + entries.push(("event", to_json(&value))); + } + entries.push(("cbor_hex", J::Str(hex::encode(&e.bytes)))); + entries.push(( + "signatures", + J::Arr( + e.signatures + .iter() + .map(|s: &EventSignature| { + J::obj([ + ("index", J::from(u64::from(s.index))), + ("signature", J::Str(hex::encode(&s.signature))), + ( + "seed", + s.seed.map_or(J::Null, |seed| J::Str(hex::encode(seed))), + ), + ]) + }) + .collect(), + ), + )); + J::obj(entries) +} + +/// A log the reference accepts, with the state its replay establishes. +fn kel_accepted(name: &str, description: &str, log: &[LoggedEvent]) -> J { + let report = LogVerifier::new() + .verify_encoded(&wire_log(log), &HashMap::new()) + .unwrap_or_else(|r| panic!("{name} is refused at event {}: {}", r.index, r.error)); + let saids: Vec = log + .iter() + .map(|e| J::Str(e.event().said().to_said_text())) + .collect(); + J::obj([ + ("name", J::from(name)), + ("description", J::from(description)), + ("events", J::Arr(log.iter().map(event_json).collect())), + ( + "expected", + J::obj([ + ("controller", J::Str(report.controller.to_text())), + ("event_saids", J::Arr(saids)), + ( + "establishments", + J::Arr( + report + .establishments + .iter() + .map(|e| { + J::obj([ + ("sequence", J::from(e.sequence)), + ("keys", J::strs(e.keys.iter().map(|k| hex::encode(&k.0)))), + ("threshold", J::from(e.threshold.0)), + ]) + }) + .collect(), + ), + ), + ( + "next_key_digests", + J::strs(report.next_keys.iter().map(Said::to_said_text)), + ), + ("next_threshold", J::from(report.next_threshold.0)), + ]), + ), + ]) +} + +/// A log the reference refuses with `class` at event `at`. +fn kel_refused(name: &str, description: &str, log: &[LoggedEvent], class: &str, at: usize) -> J { + let refused = LogVerifier::new() + .verify_encoded(&wire_log(log), &HashMap::new()) + .err() + .unwrap_or_else(|| panic!("{name} is accepted")); + assert_eq!( + (refused.error.class(), refused.index), + (class, at), + "{name} is refused for the wrong reason: {}", + refused.error + ); + J::obj([ + ("name", J::from(name)), + ("description", J::from(description)), + ("events", J::Arr(log.iter().map(event_json).collect())), + ("error", J::from(class)), + ("error_at", J::from(at)), + ]) +} + +/// `event`'s canonical value with `edit` applied, its SAID — and an +/// inception's AID — re-derived over the result. +fn restamped(event: &KeyEvent, edit: impl FnOnce(&mut Vec<(Value, Value)>)) -> Vec { + let Value::Map(mut entries) = wire::to_value(event).unwrap() else { + unreachable!("an event is a map") + }; + edit(&mut entries); + let derived: &[Derived<'_>] = if matches!(event, KeyEvent::Inception(_)) { + std::slice::from_ref(&INCEPTION_AID) + } else { + &[] + }; + said::derive_with(&Value::Map(entries), SAID_FIELD, derived) + .unwrap() + .encoded +} + +fn set(entries: &mut Vec<(Value, Value)>, key: &str, value: Value) { + entries.retain(|(k, _)| *k != Value::text(key)); + entries.push((Value::text(key), value)); +} + +/// `bytes` with the one occurrence of `from` replaced by `to`. +fn patched(bytes: &[u8], from: &[u8], to: &[u8]) -> Vec { + let at: Vec = (0..=bytes.len() - from.len()) + .filter(|&i| bytes[i..].starts_with(from)) + .collect(); + assert_eq!(at.len(), 1, "the patch site must be unique"); + [&bytes[..at[0]], to, &bytes[at[0] + from.len()..]].concat() +} + +pub fn kel(world: &World) -> J { + let k = Key::named; + let c = |n: usize| k(&format!("custodian-{n}")); + let issuer_icp = world.issuer.log[0].event(); + let alice_icp = world.alice.log[0].event(); + let custodian_icp = world.custodian.log[0].event(); + let signed = LoggedEvent::signed; + let alice = |log: Vec| [vec![world.alice.log[0].clone()], log].concat(); + + let fixed_icp = inception(&[k("fixed-0")], 1, &[], 0); + + J::Arr(vec![ + kel_accepted( + ISSUER_LOG, + "The issuer's log: one inception, self-signed by the key it reveals (identity spec §3.5). The key is RFC 8032's first test key. `n` commits to one next key by its per-key SAID (§3.4), and both thresholds are 1. The controller AID is the inception's SAID with `B` for `E` (§3.1); while the event is hashed, `i` holds the placeholder just as `d` does.", + &world.issuer.log, + ), + kel_accepted( + ALICE_LOG, + "Alice's log: an inception, a rotation and an interaction. The rotation reveals the key the inception committed to and is signed by that key, the one it reveals (§3.5). The interaction anchors the identity of the byte Alice was issued (env-001), carries the key fields forward unchanged, and is signed with the key the rotation put in force. Two establishment events, so two key states: envelope signatures name the one whose keys made them.", + &world.alice.log, + ), + kel_accepted( + CUSTODIAN_LOG, + "The custodian's log: three keys, any two of which sign. The inception is signed by keys 0 and 1; the rotation reveals the three committed keys and is signed by keys 0 and 2, meeting both the committed next threshold and its own (§3.5). Which keys sign is up to the controller; how many is not.", + &world.custodian.log, + ), + kel_accepted( + "kel-004-no-next-keys", + "An inception that commits to no next keys: `n` is empty and `nt` is 0. The controller gives up rotating; from here only interactions are accepted (§3.3). kel-017 is a rotation after it.", + &[signed(&fixed_icp, &[(0, k("fixed-0"))])], + ), + kel_refused( + "kel-005-reserved-recovery-refused", + "The issuer's inception followed by a `rec` event, which identity spec §3.2 reserves: a v1 verifier refuses it as soon as it reads the type. This one reveals keys the issuer never committed to and is signed by them — the takeover that recovering without committed keys would allow.", + &[ + world.issuer.log[0].clone(), + signed( + &finalize_event(KeyEvent::Recovery(RecoveryEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: world.issuer.aid, + s: 1, + p: issuer_icp.said(), + kt: Threshold(1), + k: vec![k("attacker-0").public()], + nt: Threshold(1), + n: vec![k("attacker-1").digest()], + a: vec![], + })) + .unwrap(), + &[(0, k("attacker-0"))], + ), + ], + "unsupported_event_type", + 1, + ), + kel_refused( + "kel-006-delegated-inception-refused", + "A log that begins with a delegated inception (`dip`), naming the issuer as its parent. Spec v1 defines `icp`, `rot` and `ixn` (and reserves `rec`); delegation is not among them, so the event is refused.", + &[signed( + &finalize_event(KeyEvent::Delegation(DelegationEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: Aid::default(), + s: 0, + p: String::new(), + di: world.issuer.aid, + kt: Threshold(1), + k: vec![k("delegate-0").public()], + nt: Threshold(1), + n: vec![k("delegate-1").digest()], + })) + .unwrap(), + &[(0, k("delegate-0"))], + )], + "unsupported_event_type", + 0, + ), + kel_refused( + "kel-007-rotation-to-uncommitted-key", + "Alice's inception followed by a rotation to a key her inception never committed to, signed by that key. The revealed key's per-key SAID is not the committed one (§4 step 5), so the rotation is refused.", + &alice(vec![signed( + &rotation(&alice_icp, &[k("attacker-0")], 1, &[k("attacker-1")], 1), + &[(0, k("attacker-0"))], + )]), + "pre_rotation_mismatch", + 1, + ), + kel_refused( + "kel-008-rotation-signed-by-current-keys", + "Alice's rotation, revealing the right key, but signed by her current key — what an adversary who has stolen the current key can produce. A rotation is signed by the keys it reveals (§3.5), so the signature does not verify. A verifier that took rotations as signed by the previous keys would accept it, and hand the stolen key control of every later rotation.", + &alice(vec![signed( + &rotation(&alice_icp, &[k("alice-1")], 1, &[k("alice-2")], 1), + &[(0, k("alice-0"))], + )]), + "signature_invalid", + 1, + ), + kel_refused( + "kel-009-rotation-lowers-threshold", + "The custodian's rotation rewritten to set its own threshold to 1, signed by one revealed key. The inception committed `nt` = 2, and a rotation must meet the greater of the committed threshold and its own (§3.5), so one signature is not enough: the threshold cannot be lowered without the pre-rotated keys.", + &[ + world.custodian.log[0].clone(), + signed( + &rotation( + &custodian_icp, + &[c(3), c(4), c(5)], + 1, + &[c(6), c(7), c(8)], + 1, + ), + &[(0, c(3))], + ), + ], + "below_threshold", + 1, + ), + kel_refused( + "kel-010-inception-below-threshold", + "The custodian's inception signed by one of its three keys. Its own `kt` is 2, so it is refused.", + &[signed(&custodian_icp, &[(0, c(0))])], + "below_threshold", + 0, + ), + kel_refused( + "kel-011-interaction-changes-keys", + "Alice's inception followed by an interaction that writes her next key into `k`. An interaction changes no keys (§3.2); one that did would be a rotation without the pre-rotated keys. It is refused whatever signs it.", + &alice(vec![signed( + &interaction(&alice_icp, &[k("alice-1")], 1, &[k("alice-1")], 1, vec![]), + &[(0, k("alice-0"))], + )]), + "interaction_changes_keys", + 1, + ), + kel_refused( + "kel-012-zero-threshold", + "An inception with `kt` = 0 and no signatures. Zero valid signatures meet a threshold of zero, so the log would accept events no one signed. A threshold is at least 1 and at most the number of keys (§3.3).", + &[LoggedEvent::signed( + &inception(&[k("zero-0")], 0, &[k("zero-1")], 1), + &[], + )], + "invalid_threshold", + 0, + ), + kel_refused( + "kel-013-duplicate-key", + "An inception listing one key twice with `kt` = 2, signed twice by that key. Counted naively, one holder meets a two-key threshold alone. Keys in `k` are distinct (§3.3).", + &[signed( + &inception(&[k("dup-0"), k("dup-0")], 2, &[k("dup-1")], 1), + &[(0, k("dup-0")), (1, k("dup-0"))], + )], + "duplicate_key", + 0, + ), + kel_refused( + "kel-014-another-controllers-event", + "Alice's inception followed by an interaction naming the issuer's AID in `i`, signed with Alice's key. An event in a log bears that log's controller (§4 step 4).", + &alice(vec![signed( + &finalize_event(KeyEvent::Interaction(InteractionEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: world.issuer.aid, + s: 1, + p: alice_icp.said(), + kt: Threshold(1), + k: vec![k("alice-0").public()], + nt: Threshold(1), + n: vec![k("alice-1").digest()], + a: vec![], + })) + .unwrap(), + &[(0, k("alice-0"))], + )]), + "controller_mismatch", + 1, + ), + kel_refused( + "kel-015-inception-claims-another-aid", + "Alice's inception with `i` set to the issuer's AID after stamping. `i` holds the placeholder while an inception is hashed, so its SAID still checks; but `i` must be the AID that SAID names (§3.1, §4 step 1), or anyone could incept keys of their own under another controller's identifier.", + &[signed( + &{ + let mut e = alice_icp.clone(); + if let KeyEvent::Inception(icp) = &mut e { + icp.i = world.issuer.aid; + } + e + }, + &[(0, k("alice-0"))], + )], + "controller_mismatch", + 0, + ), + kel_refused( + "kel-016-changed-after-stamping", + "Alice's log with the interaction's anchor changed after its SAID was computed, and re-signed. The bytes no longer hash to the SAID they state (§4 step 1).", + &[ + world.alice.log[0].clone(), + world.alice.log[1].clone(), + signed( + &{ + let mut e = world.alice.log[2].event(); + if let KeyEvent::Interaction(ixn) = &mut e { + ixn.a = vec![Said::hash(b"another byte")]; + } + e + }, + &[(0, k("alice-1"))], + ), + ], + "said_mismatch", + 2, + ), + kel_refused( + "kel-017-rotation-after-no-next-keys", + "kel-004's inception, which committed to no next keys, followed by a rotation to a key of someone else's choosing, signed by that key. With no commitment there is nothing to reveal: the rotation is refused before its keys are compared (§4 step 5).", + &[ + signed(&fixed_icp, &[(0, k("fixed-0"))]), + signed( + &rotation(&fixed_icp, &[k("attacker-0")], 1, &[k("attacker-1")], 1), + &[(0, k("attacker-0"))], + ), + ], + "rotation_not_permitted", + 1, + ), + kel_refused( + "kel-018-sequence-gap", + "Alice's inception followed by an interaction at sequence 2. Sequence numbers rise by exactly one (§4 step 2).", + &alice(vec![signed( + &{ + let mut e = + interaction(&alice_icp, &[k("alice-0")], 1, &[k("alice-1")], 1, vec![]); + if let KeyEvent::Interaction(ixn) = &mut e { + ixn.s = 2; + } + finalize_event(e).unwrap() + }, + &[(0, k("alice-0"))], + )]), + "out_of_sequence", + 1, + ), + kel_refused( + "kel-019-prior-mismatch", + "Alice's inception followed by an interaction whose `p` names the issuer's inception. An event names the event before it in its own log (§4 step 3).", + &alice(vec![signed( + &{ + let mut e = + interaction(&alice_icp, &[k("alice-0")], 1, &[k("alice-1")], 1, vec![]); + if let KeyEvent::Interaction(ixn) = &mut e { + ixn.p = issuer_icp.said(); + } + finalize_event(e).unwrap() + }, + &[(0, k("alice-0"))], + )]), + "prior_mismatch", + 1, + ), + kel_refused( + "kel-020-non-canonical-event", + "The issuer's inception with `s` written 0x18 0x00 instead of 0x00. The bytes are not canonical, so they are refused before anything is hashed (§4 step 1, §2.6).", + &[LoggedEvent::signed_bytes( + patched( + &world.issuer.log[0].bytes, + &[0x61, b's', 0x00], + &[0x61, b's', 0x18, 0x00], + ), + &[(0, k("issuer-0"))], + )], + "non_shortest_integer", + 0, + ), + kel_refused( + "kel-021-undefined-member", + "The issuer's inception with a member `x` added and its SAID and AID re-derived, so they check. An event has exactly the members §3.3 defines.", + &[LoggedEvent::signed_bytes( + restamped(&issuer_icp, |m| set(m, "x", Value::Null)), + &[(0, k("issuer-0"))], + )], + "malformed_event", + 0, + ), + kel_refused( + "kel-022-key-written-as-integers", + "The issuer's inception with its key written as an array of 33 small integers, not a byte string, and its SAID and AID re-derived over that form. The bytes are canonical CBOR and decode to the same key, but they are not the event's encoding — a byte string is (§3.3) — so a verifier that hashes what it decoded would accept a second encoding of the event.", + &[LoggedEvent::signed_bytes( + restamped(&issuer_icp, |m| { + let key = k("issuer-0").public().0; + set( + m, + "k", + Value::Array(vec![Value::Array( + key.into_iter() + .map(|b| Value::Unsigned(u64::from(b))) + .collect(), + )]), + ) + }), + &[(0, k("issuer-0"))], + )], + "malformed_event", + 0, + ), + kel_refused( + "kel-023-unsupported-version", + "The issuer's inception with `v` = \"SBYTE20JSON\" and its SAID and AID re-derived. A v1 verifier speaks SBYTE10JSON (§3.3) and refuses any other version rather than guessing at it.", + &[LoggedEvent::signed_bytes( + restamped(&issuer_icp, |m| set(m, "v", Value::text("SBYTE20JSON"))), + &[(0, k("issuer-0"))], + )], + "unsupported_version", + 0, + ), + kel_refused( + "kel-024-reserved-algorithm", + "An inception whose key carries algorithm identifier 0x10, which identity spec §3.3 reserves for post-quantum schemes, signed with a signature carrying the same identifier. A v1 verifier verifies Ed25519 (0x01) and refuses the rest.", + &[{ + let event = finalize_event(KeyEvent::Inception(InceptionEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: Aid::default(), + s: 0, + p: String::new(), + kt: Threshold(1), + k: vec![PublicKey::new(0x10, &[0x5a; 32])], + nt: Threshold(1), + n: vec![k("reserved-1").digest()], + a: vec![], + b: vec![], + bt: Threshold(0), + })) + .unwrap(); + LoggedEvent { + bytes: event.to_cbor().unwrap(), + signatures: vec![EventSignature { + index: 0, + signature: [&[0x10][..], &[0xa5; 64]].concat(), + seed: None, + }], + } + }], + "unsupported_algorithm", + 0, + ), + kel_refused( + "kel-025-witness-pool-refused", + "An inception naming a witness pool (`b`, `bt`). Witnessing is not part of spec v1: an event has exactly the members §3.3 defines, so a v1 verifier refuses it.", + &[signed( + &finalize_event(KeyEvent::Inception(InceptionEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: Aid::default(), + s: 0, + p: String::new(), + kt: Threshold(1), + k: vec![k("witnessed-0").public()], + nt: Threshold(1), + n: vec![k("witnessed-1").digest()], + a: vec![], + b: vec![WitnessAid::of_key( + &k("witness-0").signing().verifying_key(), + )], + bt: Threshold(1), + })) + .unwrap(), + &[(0, k("witnessed-0"))], + )], + "malformed_event", + 0, + ), + ]) +} + +// --------------------------------------------------------------------------- +// envelope_vectors.json +// --------------------------------------------------------------------------- + +const ALL_LOGS: [&str; 3] = [ISSUER_LOG, ALICE_LOG, CUSTODIAN_LOG]; + +fn envelope_head(name: &str, description: &str, bytes: &[u8]) -> Vec<(&'static str, J)> { + let mut entries = vec![ + ("name", J::from(name)), + ("description", J::from(description)), + ("key_event_logs", J::strs(ALL_LOGS)), + ]; + if let Ok(value) = canonical::decode(bytes) { + entries.push(("envelope", to_json(&value))); + } + entries.push(("cbor_hex", J::Str(hex::encode(bytes)))); + entries +} + +fn envelope_accepted(name: &str, description: &str, envelope: &Envelope, keys: &KeyBook) -> J { + let bytes = envelope.to_cbor(); + let decoded = Envelope::from_cbor(&bytes).unwrap_or_else(|e| panic!("{name}: {e}")); + let v = decoded + .verify(keys) + .unwrap_or_else(|e| panic!("{name}: {e}")); + let mut entries = envelope_head(name, description, &bytes); + entries.push(( + "expected", + J::obj([ + ("identity", J::Str(v.identity.to_said_text())), + ("snapshot", J::Str(decoded.said().to_said_text())), + ("issuer", J::Str(v.issuer.to_text())), + ("owner", J::Str(v.owner.to_text())), + ("history_hash", J::Str(hex::encode(v.history_hash))), + ("transfers", J::from(decoded.ownership_chain.len())), + ]), + )); + J::obj(entries) +} + +fn envelope_refused(name: &str, description: &str, bytes: &[u8], keys: &KeyBook, class: &str) -> J { + let refused = Envelope::from_cbor(bytes) + .and_then(|e| e.verify(keys)) + .err() + .unwrap_or_else(|| panic!("{name} is accepted")); + assert_eq!( + refused.class(), + class, + "{name} is refused for the wrong reason: {refused}" + ); + let mut entries = envelope_head(name, description, bytes); + entries.push(("error", J::from(class))); + J::obj(entries) +} + +/// `envelope` with its snapshot SAID recomputed. +fn restamp(mut envelope: Envelope) -> Envelope { + envelope.d = envelope.compute_said(); + envelope +} + +fn map_member<'a>(value: &'a mut Value, key: &str) -> &'a mut Value { + let Value::Map(entries) = value else { + panic!("not a map") + }; + &mut entries + .iter_mut() + .find(|(k, _)| *k == Value::text(key)) + .unwrap_or_else(|| panic!("no member {key}")) + .1 +} + +pub fn envelopes(world: &World) -> J { + let keys = world.key_book(); + let k = Key::named; + let c = |n: usize| k(&format!("custodian-{n}")); + let issued = &world.issued; + let alice0 = world.alice.signer(0, &[(0, k("alice-0"))]); + let custodian0 = |signers: &[(u64, Key)]| world.custodian.signer(0, signers); + let to_custodian = issued.transfer(&alice0, world.custodian.aid, 10); + let accepted = |name, description, e: &Envelope| envelope_accepted(name, description, e, &keys); + let refused = |name, description, e: &Envelope, class| { + envelope_refused(name, description, &e.to_cbor(), &keys, class) + }; + + J::Arr(vec![ + accepted( + "env-001-issued", + "A USD byte of 100.00 (100000000 micros, cargo type 1) as the issuer issues it to Alice, with a measured and an estimated joule cost. The issuer signs the canonical encoding of {cargo, issuer, nonce, recipient} under its establishment event 0 (envelope wire format §3, §7). The provenance's SAID is the byte's identity; the envelope's own `d` is this snapshot's SAID (§4). With no transfers, the owner is the recipient and the history hash is the identity's digest.", + issued, + ), + accepted( + "env-002-transfers-across-a-rotation", + "env-001's byte after three transfers: Alice to the custodian at frame 10, under Alice's establishment event 0; back to Alice at frame 11, signed by two of the custodian's three keys; to the custodian again at frame 12, signed with the key Alice rotated to, under her establishment event 1. Each signature names the establishment event whose keys made it, so the chain spans the rotation unbroken (§7). The identity is env-001's; the snapshot SAID is new.", + &world.transferred, + ), + accepted( + "env-003-empty-opaque-cargo", + "An opaque byte (cargo type 0) carrying no bytes, issued to the custodian with an all-zero nonce and no joule cost: the smallest values each member takes.", + &world.opaque, + ), + refused( + "env-004-cargo-changed-identity-kept", + "env-001 with its cargo raised to 1,000,000.00 after issuance and the snapshot SAID recomputed, but the identity left as it was. The provenance no longer hashes to the identity it states.", + &restamp(Envelope { + provenance: smart_byte_core::provenance::Provenance { + cargo: Cargo::usd_micros(1_000_000_000_000), + ..issued.provenance.clone() + }, + ..issued.clone() + }), + "said_mismatch", + ), + refused( + "env-005-cargo-changed-identity-restamped", + "env-004 with the identity recomputed as well, so both SAIDs check. The issuer never signed this cargo: its signature is over the attested fields, which the cargo is one of (§3).", + &restamp(Envelope { + provenance: { + let mut p = issued.provenance.clone(); + p.cargo = Cargo::usd_micros(1_000_000_000_000); + p.d = p.compute_said(); + p + }, + ..issued.clone() + }), + "signature_invalid", + ), + refused( + "env-006-stale-snapshot-said", + "env-001 with its measured joule cost changed and its snapshot SAID left as it was. A snapshot SAID covers the whole envelope, joule cost included (§4).", + &Envelope { + joule_cost: smart_byte_core::JouleCost { + measured_microjoules: 1, + ..issued.joule_cost + }, + ..issued.clone() + }, + "said_mismatch", + ), + refused( + "env-007-transfer-by-non-owner", + "env-001, which Alice owns, transferred by the custodian — validly signed by two of its keys, but the custodian does not own the byte (§8 step 3).", + &issued.transfer(&custodian0(&[(0, c(0)), (1, c(1))]), world.issuer.aid, 10), + "not_current_owner", + ), + refused( + "env-008-grafted-transition", + "env-002's first two transfers, with the second re-signed over a previous history hash taken from the start of the chain rather than after the first transfer. Its signature verifies, but it does not continue the history it follows (§5).", + &restamp(Envelope { + ownership_chain: vec![ + world.transferred.ownership_chain[0].clone(), + Transition::sign( + &custodian0(&[(0, c(0)), (2, c(2))]), + &issued.identity(), + issued.identity().0, + world.alice.aid, + 11, + ), + ], + ..world.transferred.clone() + }), + "broken_history_chain", + ), + refused( + "env-009-two-transfers-in-one-frame", + "env-001 transferred to the custodian and straight back, both at frame 10. A byte moves at most once per frame, so frames rise strictly along the chain (§5).", + &to_custodian.transfer(&custodian0(&[(0, c(0)), (2, c(2))]), world.alice.aid, 10), + "frame_not_increasing", + ), + refused( + "env-010-names-a-non-establishment-event", + "env-001 transferred by Alice with a signature naming her event 2. That is her interaction, which establishes no keys, so no key state answers to it (§7).", + &issued.transfer( + &world.alice.signer(2, &[(0, k("alice-1"))]), + world.custodian.aid, + 10, + ), + "unknown_key_state", + ), + refused( + "env-011-old-key-under-new-establishment", + "env-001 transferred by Alice with a signature naming her establishment event 1 but made with the key event 0 put in force. A signature verifies against the key state it names.", + &issued.transfer( + &world.alice.signer(1, &[(0, k("alice-0"))]), + world.custodian.aid, + 10, + ), + "signature_invalid", + ), + refused( + "env-012-custodian-below-threshold", + "The custodian transfers the byte back to Alice with one signature; its key state needs two.", + &to_custodian.transfer(&custodian0(&[(0, c(0))]), world.alice.aid, 11), + "below_threshold", + ), + refused( + "env-013-one-key-signing-twice", + "The custodian's transfer with key 0's signature listed twice. Counted naively, one key meets the custodian's threshold of two (§7).", + &to_custodian.transfer(&custodian0(&[(0, c(0)), (0, c(0))]), world.alice.aid, 11), + "duplicate_key_index", + ), + refused( + "env-014-key-index-out-of-range", + "The custodian's transfer with a signature at key index 3 of a three-key set.", + &to_custodian.transfer(&custodian0(&[(0, c(0)), (3, c(2))]), world.alice.aid, 11), + "key_index_out_of_range", + ), + refused( + "env-015-algorithm-mismatch", + "Alice's transfer with its signature's algorithm identifier changed from 0x01 to 0x02. A signature carries its key's algorithm (§7).", + &restamp({ + let mut e = to_custodian.clone(); + e.ownership_chain[0].signature.signatures[0].signature[0] = 0x02; + e + }), + "algorithm_mismatch", + ), + envelope_refused( + "env-016-non-canonical-bytes", + "env-001's bytes with the measured joule cost, 1200, written with a four-byte argument (0x1a 0x00 0x00 0x04 0xb0) instead of two. Both SAIDs are unchanged and check against the canonical form, but these bytes are not it, so they are refused before anything is hashed.", + &patched( + &issued.to_cbor(), + &[0x19, 0x04, 0xb0], + &[0x1a, 0x00, 0x00, 0x04, 0xb0], + ), + &keys, + "non_shortest_integer", + ), + envelope_refused( + "env-017-undefined-member", + "env-001 with a top-level member `fee` added and its snapshot SAID re-derived. An envelope has exactly the members the wire format defines (§2).", + &{ + let mut value = wire::to_value(issued).unwrap(); + let Value::Map(entries) = &mut value else { + unreachable!() + }; + entries.push((Value::text("fee"), Value::Unsigned(0))); + said::derive(&value, SAID_FIELD).unwrap().encoded + }, + &keys, + "not_an_envelope", + ), + envelope_refused( + "env-018-cargo-bytes-written-as-integers", + "env-003 with its cargo's byte string written as an array of integers instead — empty either way — and both SAIDs re-derived over that form. The bytes are canonical CBOR, and a verifier that hashes what it decoded finds both SAIDs check; but they are not the envelope's encoding, in which cargo bytes are a byte string (§6). Accepting them would give the byte a second encoding.", + &{ + let mut value = wire::to_value(&world.opaque).unwrap(); + let provenance = map_member(&mut value, "provenance"); + *map_member(map_member(provenance, "cargo"), "bytes") = Value::Array(vec![]); + let restamped = said::derive(provenance, SAID_FIELD).unwrap().encoded; + *provenance = canonical::decode(&restamped).unwrap(); + said::derive(&value, SAID_FIELD).unwrap().encoded + }, + &keys, + "not_an_envelope", + ), + ]) +} diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/pack.rs b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/pack.rs index 6615329d2..a0fa9d77d 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/pack.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/pack.rs @@ -1,11 +1,12 @@ //! The v1 vector pack, built from the reference implementation. //! //! Inputs, names and descriptions are written here; every expected value is -//! computed by `smart-byte-core` (canonical CBOR, SAIDs, signatures, the -//! inbox) and `smart-byte-lockstep` (the commit rule) at generation time. -//! A negative vector also states the error class it is meant to produce, and -//! generation fails if the reference refuses it for any other reason — so a -//! vector cannot pass by accident while testing nothing. +//! computed at generation time by the reference: `smart-byte-core` (canonical +//! CBOR, SAIDs, signatures, envelopes, the inbox), `smart-byte-keri-witness` +//! (key-event logs) and `smart-byte-lockstep` (the commit rule). A negative +//! vector also states the error class it is meant to produce, and generation +//! fails if the reference refuses it for any other reason — so a vector +//! cannot pass by accident while testing nothing. use std::collections::BTreeMap; @@ -13,36 +14,38 @@ use smart_byte_core::Said; use smart_byte_core::canonical::{self, Value}; use smart_byte_core::inbox::{Delivery, Inbox}; use smart_byte_core::said::{self, SAID_FIELD, SAID_PLACEHOLDER, SaidDerivation}; -use smart_byte_core::sign::{self, SigningKey}; +use smart_byte_core::sign::{self, Signer as _, SigningKey}; +use smart_byte_core::wire; use smart_byte_lockstep::{node_view, supermajority_threshold, tally}; use crate::json::{J, pretty, to_json}; +use crate::logs; +use crate::world::{Key, World}; +pub use crate::world::{RFC8032_TEST1_SEED, RFC8032_TEST2_SEED}; /// The pack's vector files, in the order the README lists them. -pub const FILES: [&str; 5] = [ +pub const FILES: [&str; 7] = [ "canonicalisation_vectors.json", "said_vectors.json", "signature_vectors.json", + "kel_vectors.json", + "envelope_vectors.json", "lockstep_vectors.json", "gossip_vectors.json", ]; -/// The seed of RFC 8032 §7.1 test 1. -pub const RFC8032_TEST1_SEED: &str = - "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"; -/// The seed of RFC 8032 §7.1 test 2. -pub const RFC8032_TEST2_SEED: &str = - "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb"; - /// Every file of the pack: (file name, contents). pub fn generate() -> Vec<(&'static str, String)> { - let saids = SaidVectors::build(); + let world = World::build(); + let saids = SaidVectors::build(&world); vec![ (FILES[0], pretty(&canonicalisation())), (FILES[1], pretty(&saids.json)), - (FILES[2], pretty(&signatures(&saids))), - (FILES[3], pretty(&lockstep())), - (FILES[4], pretty(&gossip(&saids))), + (FILES[2], pretty(&signatures(&world))), + (FILES[3], pretty(&logs::kel(&world))), + (FILES[4], pretty(&logs::envelopes(&world))), + (FILES[5], pretty(&lockstep())), + (FILES[6], pretty(&gossip(&saids))), ] } @@ -58,10 +61,6 @@ fn uint(n: u64) -> Value { Value::Unsigned(n) } -fn seed(hex_seed: &str) -> [u8; 32] { - hex::decode(hex_seed).unwrap().try_into().unwrap() -} - // --------------------------------------------------------------------------- // canonicalisation_vectors.json // --------------------------------------------------------------------------- @@ -319,98 +318,81 @@ fn said_negative( J::obj(entries) } +/// `value` — a map with a `d` member — with `d` holding the placeholder. +fn with_placeholder(value: Value) -> Value { + let Value::Map(entries) = value else { + panic!("a SAID-addressed object is a map") + }; + Value::Map( + entries + .into_iter() + .map(|(k, v)| { + if k == Value::text(SAID_FIELD) { + (k, Value::text(SAID_PLACEHOLDER)) + } else { + (k, v) + } + }) + .collect(), + ) +} + +/// `v`'s canonical value with its SAID replaced by the placeholder. +fn object(v: &T) -> Value { + with_placeholder(wire::to_value(v).expect("the object encodes")) +} + impl SaidVectors { - pub fn build() -> Self { + pub fn build(world: &World) -> Self { let mut by_name = BTreeMap::new(); let placeholder = || text(SAID_PLACEHOLDER); - let key = |b: u8| Value::Bytes(vec![b; 32]); - let filler_signature = |b: u8| Value::Bytes(vec![b; 64]); - let joules = |measured: u64, estimated: u64| { - Value::map([ - ("measured_microjoules", uint(measured)), - ("estimated_microjoules", uint(estimated)), - ]) - }; - let usd = Value::map([ - ("type", text("usd.cargo.v1")), - ("denomination_micros", uint(100_000_000)), - ("issuer_pubkey", key(0x11)), - ]); - let provenance = Value::map([ - ("alg", uint(1)), - ("issuer_pubkey", key(0x11)), - ("issuance_frame", uint(1024)), - ("signature", filler_signature(0x22)), - ]); - let envelope = |provenance: Value, chain: Vec, cargo: Value, cost: Value| { - Value::map([ - ("d", placeholder()), - ("provenance", provenance), - ("ownership_chain", Value::Array(chain)), - ("cargo", cargo), - ("joule_cost", cost), - ]) - }; + let rotation = world.alice.log[1].event(); + let interaction = world.alice.log[2].event(); let mut vectors = vec![ said_positive( &mut by_name, - "said-001-empty-envelope", - "A nested object with an envelope's top-level members — provenance, ownership chain, cargo, joule cost — all empty or zero. It exercises the whole pipeline (placeholder, canonical CBOR, BLAKE3-256, `E` + base32) on an object with no application data.", - envelope(Value::Null, vec![], Value::Null, joules(0, 0)), + "said-001-issued-envelope", + "env-001, the USD byte as the issuer issued it to Alice, with its SAID — the snapshot SAID — replaced by the placeholder. The provenance inside carries the byte's identity (said-002) in its own `d`, which is part of what is hashed here. Nested maps, byte strings, text and integers all go through one canonical encoding.", + object(&world.issued), ), said_positive( &mut by_name, - "said-002-usd-cargo", - "The same object carrying a cargo map: a USD amount of 100.00 (100000000 micros) and a 32-byte issuer key. Members of mixed types — text, integer, byte string — all sort by their encoded keys.", - envelope(Value::Null, vec![], usd.clone(), joules(12, 15)), + "said-002-byte-identity", + "env-001's provenance — the issuer's attestation to the cargo, the recipient and the nonce, with the issuer's signature — with its `d` replaced by the placeholder. Its SAID is the byte's identity (treatise §8.2), fixed at issuance: no transfer changes it.", + object(&world.issued.provenance), ), said_positive( &mut by_name, - "said-003-custom-cargo", - "The same shape carrying a temperature reading as cargo. What a member means never affects its encoding: this cargo goes through the pipeline exactly as said-002's does.", - envelope( - Value::Null, - vec![], - Value::map([ - ("type", text("sensor.temperature.v1")), - ("sensor_id", text("rooftop-array-7")), - ("celsius_milli", uint(23475)), - ("reading_frame", uint(184236)), - ]), - joules(8, 10), - ), + "said-003-transferred-envelope", + "env-002, the same byte after three transfers. The identity inside is said-002's, unchanged; the snapshot SAID differs from said-001's because a SAID covers the whole object it is derived over, ownership chain included.", + object(&world.transferred), ), said_positive( &mut by_name, - "said-004-with-provenance", - "The same shape with a provenance map: algorithm identifier 0x01 (Ed25519), issuer key, issuance frame and signature. The SAID covers every member. The key and signature are fixed filler bytes: this vector tests SAID derivation over them, not their validity.", - envelope(provenance.clone(), vec![], usd.clone(), joules(14, 18)), + "said-004-rotation-event", + "Alice's rotation event (kel-002, event 1) with its `d` replaced by the placeholder. A rotation's `i` is the controller's AID, written out; only an inception's own `i` is placeholdered too (§3.1).", + object(&rotation), + ), + said_positive( + &mut by_name, + "said-005-interaction-event", + "Alice's interaction event (kel-002, event 2), whose anchor is said-002's SAID: the identity of the byte she was issued.", + object(&interaction), ), ]; - - // said-005 chains from said-004 (treatise §10.1): the byte's identity is - // its initial history hash, and each step is blake3(previous ‖ payload). - let origin = by_name["said-004-with-provenance"].said; - let transition = |from: u8, to: u8, frame: u64, previous: [u8; 32], signature: u8| { - Value::map([ - ("alg", uint(1)), - ("from_pubkey", key(from)), - ("to_pubkey", key(to)), - ("frame", uint(frame)), - ("previous_history_hash", Value::Bytes(previous.to_vec())), - ("signature", filler_signature(signature)), - ]) - }; - let first = transition(0x11, 0x33, 1050, origin.0, 0x44); - let after_first = Said::hash(&[origin.as_bytes().as_slice(), &cbor(&first)].concat()).0; - let second = transition(0x33, 0x55, 1080, after_first, 0x66); - vectors.push(said_positive( - &mut by_name, - "said-005-with-ownership-transitions", - "said-004's object with two ownership transitions appended and a larger joule cost: an array of maps holding byte strings. The first transition's previous_history_hash is the digest of said-004's SAID; the second is blake3 of that digest followed by the first transition's canonical encoding. The SAID differs from said-004's because a SAID covers the whole object it is derived over. A byte's identity is a different quantity — the hash of its origin attestation (treatise §8.2) — and does not change as transitions accrue.", - envelope(provenance, vec![first, second], usd, joules(47, 56)), - )); + for (name, said) in [ + ("said-001-issued-envelope", world.issued.said()), + ("said-002-byte-identity", world.issued.identity()), + ("said-003-transferred-envelope", world.transferred.said()), + ("said-004-rotation-event", rotation.said()), + ("said-005-interaction-event", interaction.said()), + ] { + assert_eq!( + by_name[name].said, said, + "{name} derives the SAID the object carries" + ); + } vectors.push(said_positive( &mut by_name, @@ -419,14 +401,11 @@ impl SaidVectors { Value::map([("d", placeholder())]), )); - let rfc_key = SigningKey::from_bytes(&seed(RFC8032_TEST1_SEED)) - .verifying_key() - .to_bytes(); - let prefixed_key = [&[0x01u8][..], &rfc_key].concat(); + let prefixed_key = Key::named("issuer-0").public().0; vectors.push(said_positive( &mut by_name, "said-007-one-field-per-key", - "The one-field case: identity spec §3.4's per-key object {d, k}, whose SAID is a next-key commitment. k is a byte string: the algorithm identifier 0x01 (Ed25519) followed by the public key of RFC 8032's first test seed (§3.3).", + "The one-field case: identity spec §3.4's per-key object {d, k}, whose SAID is a next-key commitment. k is a byte string: the algorithm identifier 0x01 (Ed25519) followed by the public key of RFC 8032's first test seed (§3.3) — the issuer's key in kel-001.", Value::map([("d", placeholder()), ("k", Value::Bytes(prefixed_key.clone()))]), )); assert_eq!( @@ -519,7 +498,7 @@ impl SaidVectors { fn signature_vector( name: &str, description: &str, - said: &Said, + message: &[u8], signing_seed: Option<[u8; 32]>, verifying_key: [u8; 32], signature: &[u8], @@ -533,7 +512,7 @@ fn signature_vector( "{name}: a seed's vector carries the key the seed derives" ); } - let result = sign::verify_said_signature(&said.to_said_text(), signature, &verifying_key); + let result = sign::verify(&verifying_key, message, signature); assert_eq!(result.is_ok(), expected_verify, "{name}"); assert_eq!( result.err().map(|e| e.class()), @@ -543,8 +522,7 @@ fn signature_vector( J::obj([ ("name", J::from(name)), ("description", J::from(description)), - ("envelope_said", J::Str(said.to_said_text())), - ("signed_message_hex", J::Str(hex::encode(said.as_bytes()))), + ("message_hex", J::Str(hex::encode(message))), ( "signing_key_seed", signing_seed @@ -589,52 +567,53 @@ fn key_of(seed: &[u8; 32]) -> [u8; 32] { SigningKey::from_bytes(seed).verifying_key().to_bytes() } -fn signatures(saids: &SaidVectors) -> J { - let empty = saids.said("said-001-empty-envelope").said; - let usd = saids.said("said-002-usd-cargo").said; - let custom = saids.said("said-003-custom-cargo").said; - let (seed1, seed2, zero) = ( - seed(RFC8032_TEST1_SEED), - seed(RFC8032_TEST2_SEED), +fn signatures(world: &World) -> J { + let (issuer, alice, zero) = ( + Key::named("issuer-0").seed, + Key::named("alice-0").seed, [0u8; 32], ); - let sig1 = sign::sign_said(&usd, &SigningKey::from_bytes(&seed1)).to_bytes(); - let sig2 = sign::sign_said(&empty, &SigningKey::from_bytes(&seed2)).to_bytes(); + let attestation = world.issued.provenance.signed_bytes(); + let inception = world.alice.log[0].bytes.clone(); + let sig1 = SigningKey::from_bytes(&issuer) + .sign(&attestation) + .to_bytes(); + let sig2 = SigningKey::from_bytes(&alice).sign(&inception).to_bytes(); J::Arr(vec![ signature_vector( - "sig-001-known-key-known-said", - "Ed25519 (RFC 8032) is deterministic: signing said-002's SAID with the key of RFC 8032's first test seed gives exactly this signature. The message is the 32 digest bytes the SAID spells (signed_message_hex), not its 53-character text.", - &usd, - Some(seed1), - key_of(&seed1), + "sig-001-issuer-attestation", + "What the issuer signs to issue env-001: the canonical encoding of {cargo, issuer, nonce, recipient} (envelope wire format §3). The key is RFC 8032's first test key, and Ed25519 is deterministic, so this is the one signature that key makes over this message — the 64 bytes after the algorithm identifier in env-001's provenance signature.", + &attestation, + Some(issuer), + key_of(&issuer), &sig1, true, None, ), signature_vector( - "sig-002-verify-positive", - "A signature over said-001's SAID by the key of RFC 8032's second test seed. It verifies under that key.", - &empty, - Some(seed2), - key_of(&seed2), + "sig-002-key-event", + "Alice's inception event (kel-002, event 0), signed by the key it reveals: RFC 8032's second test key. The message is the event's canonical encoding (identity spec §3.5).", + &inception, + Some(alice), + key_of(&alice), &sig2, true, None, ), signature_vector( - "sig-003-tampered-envelope", - "sig-002's signature presented with said-003's SAID: an adversary swaps the object and forwards the original signature. The signature is well-formed but binds another SAID, so verification MUST fail.", - &custom, - Some(seed2), - key_of(&seed2), + "sig-003-message-swapped", + "sig-002's signature presented with sig-001's message: an adversary swaps the message and forwards the original signature. The signature is well-formed but binds another message, so verification MUST fail.", + &attestation, + Some(alice), + key_of(&alice), &sig2, false, Some("signature_invalid"), ), signature_vector( "sig-004-wrong-key", - "sig-002's signature over said-001's SAID, presented with another key — the one the all-zero seed derives. The signature is valid for the key that made it, which is not this one, so verification MUST fail.", - &empty, + "sig-002's message and signature, presented with another key — the one the all-zero seed derives. The signature is valid for the key that made it, which is not this one, so verification MUST fail.", + &inception, Some(zero), key_of(&zero), &sig2, @@ -644,9 +623,9 @@ fn signatures(saids: &SaidVectors) -> J { signature_vector( "sig-005-malformed-signature", "sig-002's signature cut to 63 bytes. It is refused as malformed before any cryptography runs, and an implementation MUST NOT panic on it.", - &empty, - Some(seed2), - key_of(&seed2), + &inception, + Some(alice), + key_of(&alice), &sig2[..63], false, Some("malformed_signature"), @@ -654,9 +633,9 @@ fn signatures(saids: &SaidVectors) -> J { signature_vector( "sig-006-non-canonical-s-rejected", "sig-002's signature with its scalar S replaced by S + L: the same scalar, written outside [0, L). RFC 8032 §5.1.7 requires refusing it; accepting it would make signatures malleable, giving a second valid-looking signature for one message and key.", - &empty, - Some(seed2), - key_of(&seed2), + &inception, + Some(alice), + key_of(&alice), &with_s_plus_l(&sig2), false, Some("signature_invalid"), @@ -664,7 +643,7 @@ fn signatures(saids: &SaidVectors) -> J { signature_vector( "sig-007-non-canonical-key-rejected", "A verifying key spelled with y = p + 3: an out-of-range encoding of the valid, large-order point with y = 3. RFC 8032 §5.1.3 requires decoding to fail when y is not below p, so the key is refused as malformed before any verification runs — one key has one spelling. There is no seed: no key pair has this spelling.", - &empty, + &inception, None, KEY_Y_PLUS_P, &sig2, diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/verify.rs b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/verify.rs index a852e105e..8764d34d6 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/verify.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/verify.rs @@ -10,15 +10,16 @@ //! error class the conformance README lists must be exercised (and no other). //! A pack cannot pass by losing vectors. -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::path::{Path, PathBuf}; use serde_json::Value as S; -use smart_byte_core::Said; use smart_byte_core::canonical::{self, Value}; use smart_byte_core::inbox::{Delivery, Inbox}; use smart_byte_core::said::{self, SAID_PLACEHOLDER}; -use smart_byte_core::sign::{self, SigningKey}; +use smart_byte_core::sign::{self, Signer as _, SigningKey}; +use smart_byte_core::{Envelope, KeyBook, Said}; +use smart_byte_keri_witness::{KeyEvent, LogVerifier, SignedEvent, VerificationReport}; use smart_byte_lockstep::{node_view, supermajority_threshold, tally}; use crate::json::{from_json, parse_strict}; @@ -164,16 +165,18 @@ pub fn verify_pack(dir: &Path) -> Report { } } - let said_texts: BTreeSet = files - .get("said_vectors.json") - .into_iter() - .flatten() - .filter_map(|v| { - v.get("expected_said") - .and_then(S::as_str) - .map(str::to_owned) - }) - .collect(); + // Key-event logs first: the envelope vectors are checked against the key + // states the accepted logs establish. + let mut logs: BTreeMap = BTreeMap::new(); + for v in files.get("kel_vectors.json").into_iter().flatten() { + let name = v.get("name").and_then(S::as_str).unwrap_or("(unnamed)"); + let result = kel(v).map(|accepted| { + if let Some(r) = accepted { + logs.insert(name.to_owned(), r); + } + }); + report.record("kel_vectors.json", name, result); + } for (file, items) in &files { for v in items { @@ -181,7 +184,9 @@ pub fn verify_pack(dir: &Path) -> Report { let result = match *file { "canonicalisation_vectors.json" => canonicalisation(v), "said_vectors.json" => said_vector(v), - "signature_vectors.json" => signature(v, &said_texts), + "signature_vectors.json" => signature(v), + "kel_vectors.json" => continue, + "envelope_vectors.json" => envelope(v, &logs), "lockstep_vectors.json" => lockstep(v), "gossip_vectors.json" => gossip(v), _ => unreachable!("FILES lists exactly these"), @@ -468,19 +473,8 @@ fn lenient_decode(input: &[u8]) -> Result { Ok(v) } -fn signature(v: &S, said_texts: &BTreeSet) -> R { - let said_text = text(v, "envelope_said")?; - if !said_texts.contains(said_text) { - return Err(format!( - "envelope_said {said_text} is not the expected_said of any SAID vector" - )); - } - let said = Said::from_said_text(said_text).map_err(|e| e.to_string())?; - same( - "signed_message_hex", - hex::encode(said.as_bytes()), - text(v, "signed_message_hex")?, - )?; +fn signature(v: &S) -> R { + let message = hex_field(v, "message_hex")?; let key = match field(v, "signing_key_seed")? { S::Null => None, seed => { @@ -495,7 +489,7 @@ fn signature(v: &S, said_texts: &BTreeSet) -> R { } }; let sig = hex_field(v, "signature")?; - let result = sign::verify_said_signature(said_text, &sig, &hex_field(v, "verifying_key")?); + let result = sign::verify(&hex_field(v, "verifying_key")?, &message, &sig); match field(v, "expected_verify")?.as_bool() { Some(true) => { result.map_err(|e| format!("a positive vector fails to verify: {e}"))?; @@ -507,7 +501,7 @@ fn signature(v: &S, said_texts: &BTreeSet) -> R { let key = key.ok_or("a positive vector must carry its signing seed")?; same( "signature", - hex::encode(sign::sign_said(&said, &key).to_bytes()), + hex::encode(key.sign(&message).to_bytes()), text(v, "signature")?, ) } @@ -516,6 +510,152 @@ fn signature(v: &S, said_texts: &BTreeSet) -> R { } } +/// A vector's typed-JSON form of an object, when it gives one, must be the +/// object its bytes encode. +fn typed_matches_bytes(v: &S, typed: &str, bytes: &[u8]) -> R { + if let Some(j) = v.get(typed) { + let encoded = canonical::encode(&from_json(j)?).map_err(|e| e.to_string())?; + if encoded != bytes { + return Err(format!("`{typed}` does not encode to the vector's bytes")); + } + } + Ok(()) +} + +/// Check one key-event-log vector; an accepted log's report is returned for +/// the envelope vectors to draw key states from. +fn kel(v: &S) -> Result, String> { + let events = field(v, "events")? + .as_array() + .ok_or("events is not an array")?; + let mut log = Vec::new(); + for (i, e) in events.iter().enumerate() { + let bytes = hex_field(e, "cbor_hex")?; + typed_matches_bytes(e, "event", &bytes).map_err(|m| format!("event {i}: {m}"))?; + let mut signatures = Vec::new(); + for s in field(e, "signatures")? + .as_array() + .ok_or("signatures is not an array")? + { + let index = field(s, "index")? + .as_u64() + .and_then(|n| u32::try_from(n).ok()) + .ok_or("a signature index is not an integer")?; + let signature = hex_field(s, "signature")?; + // A signature with a seed is the one that seed's key makes: + // Ed25519 is deterministic. + if let S::String(_) = field(s, "seed")? { + let key = SigningKey::from_bytes(&hash32(&s["seed"], "seed")?); + let made = [&[0x01][..], &key.sign(&bytes).to_bytes()].concat(); + if made != signature { + return Err(format!( + "event {i}: a signature is not the one its seed makes" + )); + } + } + signatures.push((index, signature)); + } + log.push(SignedEvent { bytes, signatures }); + } + let result = LogVerifier::new().verify_encoded(&log, &HashMap::new()); + match (v.get("expected"), v.get("error")) { + (Some(expected), None) => { + let report = result.map_err(|r| { + format!( + "refused at event {} with {}: {}", + r.index, + r.error.class(), + r.error + ) + })?; + let saids: Vec = log + .iter() + .map(|e| KeyEvent::from_cbor(&e.bytes).map(|ev| ev.said().to_said_text())) + .collect::>() + .map_err(|e| e.to_string())?; + let computed = serde_json::json!({ + "controller": report.controller.to_text(), + "event_saids": saids, + "establishments": report.establishments.iter().map(|e| serde_json::json!({ + "sequence": e.sequence, + "keys": e.keys.iter().map(|k| hex::encode(&k.0)).collect::>(), + "threshold": e.threshold.0, + })).collect::>(), + "next_key_digests": report.next_keys.iter().map(Said::to_said_text).collect::>(), + "next_threshold": report.next_threshold.0, + }); + if *expected != computed { + return Err(format!("expected: reference computes {computed}")); + } + Ok(Some(report)) + } + (None, Some(_)) => { + let at = field(v, "error_at")? + .as_u64() + .ok_or("error_at is not an integer")?; + match result { + Ok(_) => Err(format!( + "the log is accepted, but the vector says {}", + text(v, "error")? + )), + Err(r) => { + refused_as("the log verifier", Err(r.error.class()), v)?; + if r.index as u64 != at { + return Err(format!( + "refused at event {}, but the vector says {at}", + r.index + )); + } + Ok(None) + } + } + } + _ => Err("a vector has exactly one of `expected` and `error`".into()), + } +} + +fn envelope(v: &S, logs: &BTreeMap) -> R { + let mut keys = KeyBook::new(); + for name in field(v, "key_event_logs")? + .as_array() + .ok_or("key_event_logs is not an array")? + { + let name = name + .as_str() + .ok_or("a key_event_logs entry is not a string")?; + logs.get(name) + .ok_or_else(|| format!("{name} is not an accepted log of kel_vectors.json"))? + .add_to(&mut keys); + } + let bytes = hex_field(v, "cbor_hex")?; + typed_matches_bytes(v, "envelope", &bytes)?; + let result = + Envelope::from_cbor(&bytes).and_then(|e| e.verify(&keys).map(|verified| (e, verified))); + match (v.get("expected"), v.get("error")) { + (Some(expected), None) => { + let (e, verified) = result.map_err(|e| format!("refused with {}: {e}", e.class()))?; + let computed = serde_json::json!({ + "identity": verified.identity.to_said_text(), + "snapshot": e.said().to_said_text(), + "issuer": verified.issuer.to_text(), + "owner": verified.owner.to_text(), + "history_hash": hex::encode(verified.history_hash), + "transfers": e.ownership_chain.len(), + }); + if *expected != computed { + return Err(format!("expected: reference computes {computed}")); + } + Ok(()) + } + (None, Some(_)) => refused_as( + "envelope verification", + result.map(drop).map_err(|e| e.class()), + v, + ), + _ => Err("a vector has exactly one of `expected` and `error`".into()), + } +} + fn lockstep(v: &S) -> R { let n = field(v, "node_count")? .as_u64() diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/world.rs b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/world.rs new file mode 100644 index 000000000..6e6270e04 --- /dev/null +++ b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/src/world.rs @@ -0,0 +1,354 @@ +//! The parties the pack is built from: three controllers, the key-event logs +//! that establish their keys, and the smart bytes they issue and transfer. +//! +//! * The **issuer** has one key and has never rotated. +//! * **Alice** has one key, has rotated once, and has anchored the byte she +//! was issued in an interaction event. +//! * The **custodian** holds three keys, any two of which sign, and has +//! rotated once. +//! +//! The issuer issues Alice a USD byte; it passes Alice → custodian → Alice → +//! custodian, and Alice's second transfer is signed with the key she rotated +//! to. Every key derives from a published seed, so every signature in the +//! pack can be reproduced: Ed25519 is deterministic. + +use std::collections::HashMap; + +use smart_byte_core::sign::Signer as _; +use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, KeyBook, Said, Signer, SigningKey}; +use smart_byte_keri_witness::events::{ + InceptionEvent, InteractionEvent, KeyEvent, PublicKey, RotationEvent, Threshold, + finalize_event, per_key_said, +}; +use smart_byte_keri_witness::{LogVerifier, SignedEvent, VERSION_STRING}; + +/// The seed of RFC 8032 §7.1 test 1: the issuer's first key. +pub const RFC8032_TEST1_SEED: &str = + "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"; +/// The seed of RFC 8032 §7.1 test 2: Alice's first key. +pub const RFC8032_TEST2_SEED: &str = + "4ccd089b28ff96da9db6c346ec114e0f5b8a319f35aba624da8cf6ed4fb8a6fb"; + +/// The names the pack gives the three controllers' logs. +pub const ISSUER_LOG: &str = "kel-001-single-key-inception"; +pub const ALICE_LOG: &str = "kel-002-rotation-then-interaction"; +pub const CUSTODIAN_LOG: &str = "kel-003-two-of-three-rotation"; + +pub fn hex32(s: &str) -> [u8; 32] { + hex::decode(s).unwrap().try_into().unwrap() +} + +/// A key the pack signs with. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Key { + pub seed: [u8; 32], +} + +impl Key { + /// The key named `label`. The issuer's and Alice's first keys are RFC + /// 8032's test keys, so an implementation can check its Ed25519 against + /// the RFC; every other seed is BLAKE3 of the label. + pub fn named(label: &str) -> Key { + let seed = match label { + "issuer-0" => hex32(RFC8032_TEST1_SEED), + "alice-0" => hex32(RFC8032_TEST2_SEED), + _ => Said::hash(format!("smart-byte/conformance/v1 key {label}").as_bytes()).0, + }; + Key { seed } + } + + pub fn signing(&self) -> SigningKey { + SigningKey::from_bytes(&self.seed) + } + + /// The key as an event writes it: `0x01` and the Ed25519 public key. + pub fn public(&self) -> PublicKey { + PublicKey::new(0x01, self.signing().verifying_key().as_bytes()) + } + + /// Its next-key commitment (identity spec §3.4). + pub fn digest(&self) -> Said { + per_key_said(&self.public()).unwrap() + } + + /// `0x01` and the Ed25519 signature over `message`. + pub fn sign(&self, message: &[u8]) -> Vec { + [&[0x01][..], &self.signing().sign(message).to_bytes()].concat() + } +} + +pub fn keys(labels: &[&str]) -> Vec { + labels.iter().map(|l| Key::named(l)).collect() +} + +/// One signature on a logged event, with the seed of the key that made it +/// (`None` for a signature no seed reproduces). +#[derive(Clone, Debug)] +pub struct EventSignature { + pub index: u32, + pub signature: Vec, + pub seed: Option<[u8; 32]>, +} + +/// An event as a log carries it: its bytes and the signatures over them. +#[derive(Clone, Debug)] +pub struct LoggedEvent { + pub bytes: Vec, + pub signatures: Vec, +} + +impl LoggedEvent { + /// `event`, signed by `signers` as `(key index, key)`. + pub fn signed(event: &KeyEvent, signers: &[(u32, Key)]) -> Self { + Self::signed_bytes(event.to_cbor().unwrap(), signers) + } + + /// `bytes` — which need not be an event — signed by `signers`. + pub fn signed_bytes(bytes: Vec, signers: &[(u32, Key)]) -> Self { + let signatures = signers + .iter() + .map(|(index, key)| EventSignature { + index: *index, + signature: key.sign(&bytes), + seed: Some(key.seed), + }) + .collect(); + Self { bytes, signatures } + } + + pub fn wire(&self) -> SignedEvent { + SignedEvent { + bytes: self.bytes.clone(), + signatures: self + .signatures + .iter() + .map(|s| (s.index, s.signature.clone())) + .collect(), + } + } + + /// The event, for one the reference decodes. + pub fn event(&self) -> KeyEvent { + KeyEvent::decode(&self.bytes, false).expect("a well-formed event") + } +} + +pub fn wire_log(log: &[LoggedEvent]) -> Vec { + log.iter().map(LoggedEvent::wire).collect() +} + +fn public_keys(k: &[Key]) -> Vec { + k.iter().map(Key::public).collect() +} + +fn digests(n: &[Key]) -> Vec { + n.iter().map(Key::digest).collect() +} + +/// An inception event, stamped. +pub fn inception(k: &[Key], kt: u64, n: &[Key], nt: u64) -> KeyEvent { + finalize_event(KeyEvent::Inception(InceptionEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: Aid::default(), + s: 0, + p: String::new(), + kt: Threshold(kt), + k: public_keys(k), + nt: Threshold(nt), + n: digests(n), + a: vec![], + b: vec![], + bt: Threshold(0), + })) + .unwrap() +} + +/// A rotation following `prior`, stamped. +pub fn rotation(prior: &KeyEvent, k: &[Key], kt: u64, n: &[Key], nt: u64) -> KeyEvent { + finalize_event(KeyEvent::Rotation(RotationEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: *prior.controller(), + s: prior.sequence() + 1, + p: prior.said(), + kt: Threshold(kt), + k: public_keys(k), + nt: Threshold(nt), + n: digests(n), + a: vec![], + br: vec![], + ba: vec![], + bt: Threshold(0), + })) + .unwrap() +} + +/// An interaction following `prior`, carrying the key fields in force. +pub fn interaction( + prior: &KeyEvent, + k: &[Key], + kt: u64, + n: &[Key], + nt: u64, + anchors: Vec, +) -> KeyEvent { + finalize_event(KeyEvent::Interaction(InteractionEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: *prior.controller(), + s: prior.sequence() + 1, + p: prior.said(), + kt: Threshold(kt), + k: public_keys(k), + nt: Threshold(nt), + n: digests(n), + a: anchors, + })) + .unwrap() +} + +/// A controller and its log. +#[derive(Clone, Debug)] +pub struct Party { + pub aid: Aid, + pub log: Vec, +} + +impl Party { + /// A signer under establishment event `establishment`, signing with + /// `keys` as `(key index, key)`. + pub fn signer(&self, establishment: u64, keys: &[(u64, Key)]) -> Signer { + Signer { + aid: self.aid, + establishment, + keys: keys.iter().map(|(i, k)| (*i, k.signing())).collect(), + } + } +} + +/// The 16-byte nonce named `label`. +pub fn nonce(label: &str) -> [u8; 16] { + Said::hash(format!("smart-byte/conformance/v1 nonce {label}").as_bytes()).0[..16] + .try_into() + .unwrap() +} + +/// The pack's parties and bytes. +#[derive(Clone, Debug)] +pub struct World { + pub issuer: Party, + pub alice: Party, + pub custodian: Party, + /// The USD byte as issued to Alice. + pub issued: Envelope, + /// The same byte after three transfers, ending with the custodian. + pub transferred: Envelope, + /// An empty opaque byte, issued to the custodian. + pub opaque: Envelope, +} + +impl World { + pub fn build() -> World { + let k = |l: &str| Key::named(l); + + // The issuer: one key, never rotated. + let issuer_icp = inception(&[k("issuer-0")], 1, &[k("issuer-1")], 1); + let issuer = Party { + aid: *issuer_icp.controller(), + log: vec![LoggedEvent::signed(&issuer_icp, &[(0, k("issuer-0"))])], + }; + + // Alice: incepted, issued a byte, rotated, then anchored the byte. + let alice_icp = inception(&[k("alice-0")], 1, &[k("alice-1")], 1); + let alice_aid = *alice_icp.controller(); + let issued = Envelope::issue( + &issuer.signer(0, &[(0, k("issuer-0"))]), + alice_aid, + nonce("env-001"), + Cargo::usd_micros(100_000_000), + JouleCost { + measured_microjoules: 1200, + estimated_microjoules: 1500, + }, + ); + let alice_rot = rotation(&alice_icp, &[k("alice-1")], 1, &[k("alice-2")], 1); + let alice_ixn = interaction( + &alice_rot, + &[k("alice-1")], + 1, + &[k("alice-2")], + 1, + vec![issued.identity()], + ); + let alice = Party { + aid: alice_aid, + log: vec![ + LoggedEvent::signed(&alice_icp, &[(0, k("alice-0"))]), + LoggedEvent::signed(&alice_rot, &[(0, k("alice-1"))]), + LoggedEvent::signed(&alice_ixn, &[(0, k("alice-1"))]), + ], + }; + + // The custodian: two of three keys sign, before and after rotating. + let c = |n: usize| k(&format!("custodian-{n}")); + let custodian_icp = inception(&[c(0), c(1), c(2)], 2, &[c(3), c(4), c(5)], 2); + let custodian_rot = rotation( + &custodian_icp, + &[c(3), c(4), c(5)], + 2, + &[c(6), c(7), c(8)], + 2, + ); + let custodian = Party { + aid: *custodian_icp.controller(), + log: vec![ + LoggedEvent::signed(&custodian_icp, &[(0, c(0)), (1, c(1))]), + LoggedEvent::signed(&custodian_rot, &[(0, c(3)), (2, c(5))]), + ], + }; + + let transferred = issued + .transfer(&alice.signer(0, &[(0, k("alice-0"))]), custodian.aid, 10) + .transfer(&custodian.signer(0, &[(0, c(0)), (2, c(2))]), alice.aid, 11) + .transfer(&alice.signer(1, &[(0, k("alice-1"))]), custodian.aid, 12); + + let opaque = Envelope::issue( + &issuer.signer(0, &[(0, k("issuer-0"))]), + custodian.aid, + [0; 16], + Cargo::opaque(Vec::new()), + JouleCost::default(), + ); + + World { + issuer, + alice, + custodian, + issued, + transferred, + opaque, + } + } + + /// The three logs, by the names the pack gives them. + pub fn logs(&self) -> [(&'static str, &Party); 3] { + [ + (ISSUER_LOG, &self.issuer), + (ALICE_LOG, &self.alice), + (CUSTODIAN_LOG, &self.custodian), + ] + } + + /// The key states the three logs establish. + pub fn key_book(&self) -> KeyBook { + let mut book = KeyBook::new(); + for (name, party) in self.logs() { + LogVerifier::new() + .verify_encoded(&wire_log(&party.log), &HashMap::new()) + .unwrap_or_else(|r| panic!("{name} is refused at event {}: {}", r.index, r.error)) + .add_to(&mut book); + } + book + } +} diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/tests/pack.rs b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/tests/pack.rs index 32a636390..b75786245 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-conformance/tests/pack.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-conformance/tests/pack.rs @@ -127,17 +127,17 @@ fn the_verifier_catches_a_planted_defect_in_every_file() { ), ( "said_vectors.json", - "said-005-with-ownership-transitions", - Box::new(|v| v["input"]["joule_cost"]["measured_microjoules"] = json!(48)), + "said-003-transferred-envelope", + Box::new(|v| v["input"]["joule_cost"]["measured_microjoules"] = json!(1201)), ), ( "signature_vectors.json", - "sig-001-known-key-known-said", + "sig-001-issuer-attestation", Box::new(move |v| v["signature"] = json!(flip_last(v["signature"].as_str().unwrap()))), ), ( "signature_vectors.json", - "sig-003-tampered-envelope", + "sig-003-message-swapped", Box::new(|v| { v["expected_verify"] = json!(true); v["error"] = Value::Null; @@ -158,6 +158,66 @@ fn the_verifier_catches_a_planted_defect_in_every_file() { json!("0300000000000000000000000000000000000000000000000000000000000000") }), ), + ( + "kel_vectors.json", + "kel-004-no-next-keys", + Box::new(|v| v["expected"]["next_threshold"] = json!(1)), + ), + ( + "kel_vectors.json", + "kel-008-rotation-signed-by-current-keys", + Box::new(|v| v["error"] = json!("pre_rotation_mismatch")), + ), + ( + "kel_vectors.json", + "kel-017-rotation-after-no-next-keys", + Box::new(|v| v["error_at"] = json!(0)), + ), + ( + "kel_vectors.json", + "kel-022-key-written-as-integers", + // The issuer's inception in its own encoding, with its own + // signature: a log the vector says must be refused, accepted. + Box::new(|v| { + v["events"] = + by_name("kel_vectors.json", "kel-001-single-key-inception")["events"].clone() + }), + ), + ( + "kel_vectors.json", + "kel-013-duplicate-key", + // A seed that did not make the signature beside it. + Box::new(|v| v["events"][0]["signatures"][1]["seed"] = json!("00".repeat(32))), + ), + ( + "envelope_vectors.json", + "env-001-issued", + Box::new(move |v| { + v["expected"]["history_hash"] = + json!(flip_last(v["expected"]["history_hash"].as_str().unwrap())) + }), + ), + ( + "envelope_vectors.json", + "env-011-old-key-under-new-establishment", + Box::new(|v| v["error"] = json!("below_threshold")), + ), + ( + "envelope_vectors.json", + "env-018-cargo-bytes-written-as-integers", + // env-003 in its own encoding: accepted, though the vector says + // it must be refused. + Box::new(|v| { + let own = by_name("envelope_vectors.json", "env-003-empty-opaque-cargo"); + v["envelope"] = own["envelope"].clone(); + v["cbor_hex"] = own["cbor_hex"].clone(); + }), + ), + ( + "envelope_vectors.json", + "env-002-transfers-across-a-rotation", + Box::new(|v| v["expected"]["owner"] = v["expected"]["issuer"].clone()), + ), ( "lockstep_vectors.json", "lockstep-002-supermajority-commit", @@ -222,6 +282,29 @@ fn the_verifier_catches_a_planted_defect_in_every_file() { } } +/// A defect in a log the envelope vectors draw key states from fails that +/// log and every envelope vector, and nothing else. +#[test] +fn a_defect_in_a_log_fails_the_envelopes_that_rely_on_it() { + let failed = failures_with(&scratch("planted/log-dependency"), |f, items| { + if f == "kel_vectors.json" { + let v = items + .iter_mut() + .find(|v| v["name"] == "kel-003-two-of-three-rotation") + .unwrap(); + // Drop the rotation's second signature: one of the two it needs. + v["events"][1]["signatures"].as_array_mut().unwrap().pop(); + } + }); + let mut expected = vec!["kel-003-two-of-three-rotation".to_string()]; + expected.extend( + vectors("envelope_vectors.json") + .iter() + .map(|v| v["name"].as_str().unwrap().to_owned()), + ); + assert_eq!(failed, expected); +} + /// Falsification: a pack cannot pass by losing vectors. #[test] fn an_incomplete_pack_fails() { @@ -363,17 +446,68 @@ fn the_spec_prints_the_worked_example_the_pack_computes() { ); } +/// The files describe one world: the SAID vectors' objects are the envelope +/// and key-event-log vectors' objects, and the signature vectors sign their +/// messages. #[test] -fn signatures_sign_saids_the_said_pack_derives() { - let saids: BTreeSet = vectors("said_vectors.json") - .into_iter() - .filter_map(|v| v["expected_said"].as_str().map(str::to_owned)) - .collect(); - for v in vectors("signature_vectors.json") { - assert!( - saids.contains(v["envelope_said"].as_str().unwrap()), - "{}", - v["name"] - ); - } +fn the_files_describe_one_world() { + let said = |name: &str| by_name("said_vectors.json", name)["expected_said"].clone(); + let issued = by_name("envelope_vectors.json", "env-001-issued"); + let transferred = by_name( + "envelope_vectors.json", + "env-002-transfers-across-a-rotation", + ); + let alice = by_name("kel_vectors.json", "kel-002-rotation-then-interaction"); + assert_eq!( + said("said-001-issued-envelope"), + issued["expected"]["snapshot"] + ); + assert_eq!( + said("said-002-byte-identity"), + issued["expected"]["identity"] + ); + assert_eq!( + said("said-003-transferred-envelope"), + transferred["expected"]["snapshot"] + ); + assert_eq!( + said("said-004-rotation-event"), + alice["expected"]["event_saids"][1] + ); + assert_eq!( + said("said-005-interaction-event"), + alice["expected"]["event_saids"][2] + ); + // Alice's interaction anchors the byte she was issued. + assert_eq!( + alice["events"][2]["event"]["a"][0], + issued["expected"]["identity"] + ); + + let sig = |name: &str| by_name("signature_vectors.json", name); + let without_algorithm = |j: &Value| j.as_str().unwrap()[2..].to_owned(); + let attestation = smart_byte_conformance::world::World::build() + .issued + .provenance + .signed_bytes(); + assert_eq!( + sig("sig-001-issuer-attestation")["message_hex"], + json!(hex::encode(attestation)) + ); + assert_eq!( + sig("sig-001-issuer-attestation")["signature"], + json!(without_algorithm( + &issued["envelope"]["provenance"]["signature"]["signatures"][0]["signature"]["$bytes"] + )) + ); + assert_eq!( + sig("sig-002-key-event")["message_hex"], + alice["events"][0]["cbor_hex"] + ); + assert_eq!( + sig("sig-002-key-event")["signature"], + json!(without_algorithm( + &alice["events"][0]["signatures"][0]["signature"] + )) + ); } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/Cargo.toml b/smart-byte/smart-byte-rs/crates/smart-byte-core/Cargo.toml index 5624c2a9d..03a76cb03 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/Cargo.toml +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/Cargo.toml @@ -18,3 +18,6 @@ thiserror = { workspace = true } uuid = { workspace = true } data-encoding = { workspace = true } rand = { workspace = true } + +[dev-dependencies] +serde_json = "1" diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/cargo.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/cargo.rs index 48b07528a..6487298e3 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/cargo.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/cargo.rs @@ -1,35 +1,119 @@ -//! Envelope cargo: the payload an envelope carries. +//! Cargo: what a byte carries (treatise §8.5). //! -//! Cargo is intentionally a small closed set in v1.0. New cargo types -//! land as additional variants in this enum once their canonical -//! encoding has been settled in the spec. +//! A cargo is a small-integer type tag and opaque bytes. The substrate +//! transports the bytes unchanged and never interprets them; the tag tells an +//! application what they mean. Tags are assigned in the registry below, which +//! `smart-byte/spec/envelope_wire_format.md` publishes. use serde::{Deserialize, Serialize}; -/// Discriminated union of supported cargo payloads. +/// The registered cargo types. +pub mod types { + /// Bytes only the application interprets. + pub const OPAQUE: u64 = 0; + /// US dollars redeemable from the issuer: canonical CBOR `{"micros": int}`, + /// fixed-point with six decimals (treatise §9.5), so $1.50 is 1500000. + pub const USD: u64 = 1; + /// A claim on energy: canonical CBOR `{"microjoules": uint}`. + pub const JOULES: u64 = 2; + /// A verifiable credential, as `smart-byte-vc` encodes it. + pub const VERIFIABLE_CREDENTIAL: u64 = 16; + /// A BBS credential, as `smart-byte-bbs` encodes it. + pub const BBS_CREDENTIAL: u64 = 17; + /// A BBS selective-disclosure presentation, as `smart-byte-bbs` encodes it. + pub const BBS_PRESENTATION: u64 = 18; + /// A CRDT document snapshot, as `smart-byte-crdt` encodes it. + pub const CRDT_SNAPSHOT: u64 = 19; + /// A CRDT delta, as `smart-byte-crdt` encodes it. + pub const CRDT_DELTA: u64 = 20; + /// A mobile-document (mdoc) presentation, as `smart-byte-mdl` encodes it. + pub const MDOC: u64 = 21; + + /// The registry's name for `tag`, or `None` if it is unassigned. + pub fn name(tag: u64) -> Option<&'static str> { + Some(match tag { + OPAQUE => "opaque", + USD => "usd", + JOULES => "joules", + VERIFIABLE_CREDENTIAL => "verifiable-credential", + BBS_CREDENTIAL => "bbs-credential", + BBS_PRESENTATION => "bbs-presentation", + CRDT_SNAPSHOT => "crdt-snapshot", + CRDT_DELTA => "crdt-delta", + MDOC => "mdoc", + _ => return None, + }) + } +} + +/// A byte's cargo: its type tag and its bytes. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "kind", content = "value")] -pub enum Cargo { - /// Opaque bytes. The application interprets these. - Bytes(Vec), - /// USD claim, denominated in minor units (cents). - Usd { minor: i64 }, - /// Joule claim, denominated in microjoules. - JouleClaim { microjoules: u64 }, - /// Application-defined cargo, identified by a URI namespace. - Custom { type_uri: String, body: Vec }, +#[serde(deny_unknown_fields)] +pub struct Cargo { + #[serde(rename = "type")] + pub type_tag: u64, + #[serde(with = "serde_bytes")] + pub bytes: Vec, +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct Usd { + micros: i64, +} + +#[derive(Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct Joules { + microjoules: u64, } impl Cargo { - /// Returns a short tag identifying the cargo kind. Useful for - /// logging and CLI output. + /// Cargo of type `type_tag` carrying `bytes`. + pub fn new(type_tag: u64, bytes: Vec) -> Self { + Self { type_tag, bytes } + } + + /// Opaque application bytes. + pub fn opaque(bytes: Vec) -> Self { + Self::new(types::OPAQUE, bytes) + } + + /// A dollar amount in micros (six decimals). + pub fn usd_micros(micros: i64) -> Self { + Self::new( + types::USD, + crate::wire::to_vec(&Usd { micros }).expect("an integer map always encodes"), + ) + } + + /// A claim on `microjoules` of energy. + pub fn joules(microjoules: u64) -> Self { + Self::new( + types::JOULES, + crate::wire::to_vec(&Joules { microjoules }).expect("an integer map always encodes"), + ) + } + + /// The dollar amount, if this is well-formed USD cargo. + pub fn as_usd_micros(&self) -> Option { + (self.type_tag == types::USD) + .then(|| crate::wire::from_slice::(&self.bytes).ok()) + .flatten() + .map(|u| u.micros) + } + + /// The joule claim, if this is well-formed joule cargo. + pub fn as_joules(&self) -> Option { + (self.type_tag == types::JOULES) + .then(|| crate::wire::from_slice::(&self.bytes).ok()) + .flatten() + .map(|j| j.microjoules) + } + + /// The registry's name for this cargo's type, or `"unregistered"`. pub fn kind(&self) -> &'static str { - match self { - Cargo::Bytes(_) => "bytes", - Cargo::Usd { .. } => "usd", - Cargo::JouleClaim { .. } => "joule_claim", - Cargo::Custom { .. } => "custom", - } + types::name(self.type_tag).unwrap_or("unregistered") } } @@ -38,28 +122,23 @@ mod tests { use super::*; #[test] - fn kinds_are_distinct() { - assert_eq!(Cargo::Bytes(vec![1, 2, 3]).kind(), "bytes"); - assert_eq!(Cargo::Usd { minor: 100 }.kind(), "usd"); - assert_eq!( - Cargo::JouleClaim { microjoules: 42 }.kind(), - "joule_claim" - ); - assert_eq!( - Cargo::Custom { - type_uri: "urn:example:foo".into(), - body: vec![] - } - .kind(), - "custom" - ); + fn registered_cargo_round_trips_its_value() { + assert_eq!(Cargo::usd_micros(1_500_000).as_usd_micros(), Some(1_500_000)); + assert_eq!(Cargo::usd_micros(-1).as_usd_micros(), Some(-1)); + assert_eq!(Cargo::joules(42).as_joules(), Some(42)); + // A value is read only under its own tag. + assert_eq!(Cargo::joules(42).as_usd_micros(), None); + assert_eq!(Cargo::usd_micros(1).kind(), "usd"); + assert_eq!(Cargo::new(9999, vec![]).kind(), "unregistered"); } #[test] - fn cbor_roundtrip() { - let c = Cargo::Usd { minor: 10_000 }; - let bytes = serde_cbor::to_vec(&c).unwrap(); - let back: Cargo = serde_cbor::from_slice(&bytes).unwrap(); - assert_eq!(c, back); + fn cargo_encodes_as_a_type_and_a_byte_string() { + let bytes = crate::wire::to_vec(&Cargo::opaque(vec![0xab])).unwrap(); + // {"type": 0, "bytes": h'ab'} in canonical order: "type" (4) before "bytes" (5). + assert_eq!( + bytes, + [&[0xa2, 0x64][..], b"type", &[0x00, 0x65], b"bytes", &[0x41, 0xab]].concat() + ); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/envelope.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/envelope.rs index e1c8a5c74..d52671943 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/envelope.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/envelope.rs @@ -1,248 +1,383 @@ -//! The Envelope is the substrate's load-bearing primitive. -//! -//! Wire layout (CBOR major-type 5, map): +//! The envelope: a smart byte on the wire (treatise §8). //! //! ```text //! { -//! "id": Said (32 bytes), -//! "provenance": Provenance, -//! "ownership": OwnershipChain, -//! "cargo": Cargo, -//! "joule_cost": JouleCost, +//! "d": SAID of this envelope as it stands now, +//! "provenance": the origin attestation; its own "d" is the byte's identity, +//! "ownership_chain": [transitions, oldest first], +//! "joule_cost": {"measured_microjoules", "estimated_microjoules"} //! } //! ``` //! -//! The SAID is computed by: -//! 1. Building the envelope with `id = Said([0; 32])`. -//! 2. Canonical-CBOR-encoding the envelope. -//! 3. BLAKE3-hashing the bytes. +//! An envelope is encoded in canonical CBOR (identity spec §2.3) and carries +//! two SAIDs. The provenance's `d` is the byte's identity — fixed at issuance +//! (treatise §8.2). The envelope's own `d` addresses this snapshot of the +//! byte — its current chain and joule cost — so the overlay can deduplicate +//! and fetch snapshots by content address (identity spec §2.6). Every +//! transfer produces a new snapshot SAID; none changes the identity. //! -//! After construction the SAID is stamped into `id`. Verifiers -//! recompute the SAID and compare. +//! [`Envelope::verify`] is treatise §10.2's procedure: the identity and the +//! issuer's signature, then every transition in order — history hash, current +//! owner, frame order, and the owner's signature — ending at the current owner +//! and history hash. Whether the issuer is registered for the cargo type +//! (§10.2 step 3) is a question about cluster state, which the caller answers. use serde::{Deserialize, Serialize}; +use crate::Blake3Hash; use crate::cargo::Cargo; use crate::joule_cost::JouleCost; -use crate::ownership::OwnershipChain; +use crate::keys::{ControllerSignatureError, KeyStates, Signer, verify_controller_signature}; +use crate::ownership::Transition; use crate::provenance::Provenance; -use crate::said::Said; +use crate::said::{self, Aid, SAID_FIELD, Said, SaidCheckError}; +use crate::wire::{self, WireError}; -/// Errors produced when building or validating an envelope. -#[derive(Debug, thiserror::Error)] +/// Why an envelope was refused. +#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum EnvelopeError { - #[error("CBOR encode/decode failed: {0}")] - Cbor(String), - #[error("envelope SAID mismatch (expected {expected}, computed {computed})")] - SaidMismatch { expected: Said, computed: Said }, - #[error("the bytes decode, but are not the envelope's own encoding")] - NotItsEncoding, + #[error("not an envelope: {0}")] + Wire(#[from] WireError), + #[error("the {part} SAID does not check: {source}")] + Said { + part: &'static str, + source: SaidCheckError, + }, + #[error("the {part} signature does not check: {source}")] + Signature { + part: String, + source: ControllerSignatureError, + }, + #[error("transition {transition} does not continue the history it follows")] + BrokenChain { transition: usize }, + #[error("transition {transition} is not signed away by the byte's current owner")] + NotOwner { transition: usize }, + #[error("transition {transition} is not in a later frame than the one before it")] + FrameOrder { transition: usize }, } -impl From for EnvelopeError { - fn from(e: serde_cbor::Error) -> Self { - EnvelopeError::Cbor(e.to_string()) +impl EnvelopeError { + /// The error class as the conformance pack names it. + pub fn class(&self) -> &'static str { + match self { + EnvelopeError::Wire(WireError::Canonical(e)) => e.class(), + EnvelopeError::Wire(WireError::Message(_) | WireError::NotItsEncoding) => "not_an_envelope", + EnvelopeError::Said { source, .. } => source.class(), + EnvelopeError::Signature { source, .. } => source.class(), + EnvelopeError::BrokenChain { .. } => "broken_history_chain", + EnvelopeError::NotOwner { .. } => "not_current_owner", + EnvelopeError::FrameOrder { .. } => "frame_not_increasing", + } } } -/// A signed, content-addressed envelope. +/// A smart byte. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Envelope { - /// Self-Addressing IDentifier: BLAKE3 over canonical CBOR with this - /// field zeroed. - pub id: Said, + /// This snapshot's SAID. + pub d: Said, pub provenance: Provenance, - pub ownership: OwnershipChain, - pub cargo: Cargo, + pub ownership_chain: Vec, pub joule_cost: JouleCost, } +/// What a successful [`Envelope::verify`] establishes. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Verified { + pub identity: Said, + pub issuer: Aid, + pub owner: Aid, + pub history_hash: Blake3Hash, +} + impl Envelope { - /// Construct an envelope and stamp its SAID. - pub fn new( - provenance: Provenance, - ownership: OwnershipChain, + /// Issue a byte: `issuer` attests to the recipient, nonce and cargo + /// (treatise §8.3), and the envelope starts with an empty chain. + pub fn issue( + issuer: &Signer, + recipient: Aid, + nonce: [u8; 16], cargo: Cargo, joule_cost: JouleCost, - ) -> Result { - let mut env = Self { - id: Said::default(), + ) -> Self { + Self::stamped( + Provenance::issue(issuer, recipient, nonce, cargo), + Vec::new(), + joule_cost, + ) + } + + /// Transfer the byte from its current owner, `owner`, to `to` at `frame`. + /// The envelope is not consulted about who owns it: [`Envelope::verify`] + /// refuses a transfer signed by anyone else. + pub fn transfer(&self, owner: &Signer, to: Aid, frame: u64) -> Self { + let mut chain = self.ownership_chain.clone(); + chain.push(Transition::sign( + owner, + &self.identity(), + self.history_hash(), + to, + frame, + )); + Self::stamped(self.provenance.clone(), chain, self.joule_cost) + } + + /// The same byte with its joule cost replaced: a new snapshot. + pub fn with_joule_cost(&self, joule_cost: JouleCost) -> Self { + Self::stamped( + self.provenance.clone(), + self.ownership_chain.clone(), + joule_cost, + ) + } + + fn stamped(provenance: Provenance, ownership_chain: Vec, joule_cost: JouleCost) -> Self { + let mut env = Envelope { + d: Said::default(), provenance, - ownership, - cargo, + ownership_chain, joule_cost, }; - env.id = env.compute_said()?; - Ok(env) + env.d = env.compute_said(); + env } - /// Compute (but do not stamp) what this envelope's SAID should be. - /// The computation zeroes `id` before hashing so it is deterministic - /// regardless of the current `id` value. - pub fn compute_said(&self) -> Result { - let mut tmp = self.clone(); - tmp.id = Said::default(); - let bytes = serde_cbor::to_vec(&tmp)?; - Ok(Said::hash(&bytes)) + /// The byte's identity (treatise §8.2): its provenance's SAID. + pub fn identity(&self) -> Said { + self.provenance.d } - /// Encode this envelope as canonical CBOR. - pub fn to_cbor(&self) -> Result, EnvelopeError> { - Ok(serde_cbor::to_vec(self)?) + /// This snapshot's SAID. + pub fn said(&self) -> Said { + self.d } - /// Decode an envelope, accepting only its own encoding: the bytes must be - /// exactly what [`Envelope::to_cbor`] writes for the decoded envelope. - /// - /// The underlying decoder tolerates extra members, indefinite lengths and - /// over-long heads. Without this check one envelope would have unboundedly - /// many byte strings, each a fresh gossip message id under the same SAID. + /// The owner after the last transition: the recipient if there is none. + pub fn current_owner(&self) -> Aid { + self.ownership_chain + .last() + .map_or(self.provenance.recipient, |t| t.to) + } + + /// The history hash after the last transition: the identity's digest if + /// there is none (treatise §10.1). This follows the chain as written; only + /// [`Envelope::verify`] establishes that it is unbroken. + pub fn history_hash(&self) -> Blake3Hash { + self.ownership_chain + .last() + .map_or(self.identity().0, Transition::next_history_hash) + } + + /// The SAID this snapshot should carry. + pub fn compute_said(&self) -> Said { + let value = wire::to_value(self).expect("an envelope always encodes"); + said::derive(&value, SAID_FIELD) + .expect("an envelope is a map with a `d` member") + .said + } + + /// The canonical encoding. + pub fn to_cbor(&self) -> Vec { + wire::to_vec(self).expect("an envelope always encodes") + } + + /// Decode an envelope, accepting only its own canonical encoding — no + /// member the envelope does not define, and none in another form — whose + /// two SAIDs, the snapshot's and the identity, both check. Signatures are + /// not checked here: that needs key states, and is [`Envelope::verify`]'s + /// job. pub fn from_cbor(bytes: &[u8]) -> Result { - let env: Self = serde_cbor::from_slice(bytes)?; - if env.to_cbor()? != bytes { - return Err(EnvelopeError::NotItsEncoding); - } + let env: Envelope = wire::from_slice(bytes)?; + env.check_saids()?; Ok(env) } - /// Verify that the stamped SAID matches a freshly-computed SAID. - pub fn verify_said(&self) -> Result<(), EnvelopeError> { - let computed = self.compute_said()?; - if computed == self.id { - Ok(()) - } else { - Err(EnvelopeError::SaidMismatch { - expected: self.id, - computed, - }) + /// Check both SAIDs: the snapshot's own, and the identity its provenance + /// states. + pub fn check_saids(&self) -> Result<(), EnvelopeError> { + let mismatch = |part: &'static str, stated: Said, computed: Said| EnvelopeError::Said { + part, + source: SaidCheckError::Mismatch { + stated: stated.to_said_text(), + computed: computed.to_said_text(), + }, + }; + let snapshot = self.compute_said(); + if snapshot != self.d { + return Err(mismatch("envelope", self.d, snapshot)); } + let identity = self.provenance.compute_said(); + if identity != self.provenance.d { + return Err(mismatch("provenance", self.provenance.d, identity)); + } + Ok(()) + } + + /// Treatise §10.2: verify the byte's identity, the issuer's signature, and + /// every transition in order, ending at the current owner and history hash. + pub fn verify(&self, keys: &impl KeyStates) -> Result { + self.check_saids()?; + let identity = self.provenance.d; + verify_controller_signature( + keys, + &self.provenance.issuer, + &self.provenance.signed_bytes(), + &self.provenance.signature, + ) + .map_err(|source| EnvelopeError::Signature { + part: "provenance".into(), + source, + })?; + + let mut owner = self.provenance.recipient; + let mut history = identity.0; + let mut last_frame: Option = None; + for (k, t) in self.ownership_chain.iter().enumerate() { + if t.previous_history_hash != history { + return Err(EnvelopeError::BrokenChain { transition: k }); + } + if t.from != owner { + return Err(EnvelopeError::NotOwner { transition: k }); + } + // One transfer per byte per frame (treatise §8.4), so frames rise. + if last_frame.is_some_and(|f| t.frame <= f) { + return Err(EnvelopeError::FrameOrder { transition: k }); + } + verify_controller_signature(keys, &t.from, &t.signed_bytes(&identity), &t.signature) + .map_err(|source| EnvelopeError::Signature { + part: format!("transition {k}"), + source, + })?; + history = t.next_history_hash(); + owner = t.to; + last_frame = Some(t.frame); + } + Ok(Verified { + identity, + issuer: self.provenance.issuer, + owner, + history_hash: history, + }) } } #[cfg(test)] mod tests { use super::*; - use crate::ownership::{OwnershipChain, Transition}; - use chrono::TimeZone; - use ed25519_dalek::{SigningKey, ed25519::signature::Signer}; - use rand::rngs::OsRng; + use crate::canonical; + use crate::keys::KeyBook; + use ed25519_dalek::SigningKey; - fn fixture_envelope() -> Envelope { - let issuer = Said::hash(b"issuer"); - let alice = Said::hash(b"alice"); - let bob = Said::hash(b"bob"); - let issued_at = chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap(); - let prov = Provenance::new(issuer, issued_at, b"auth".to_vec()); - - let mut chain = OwnershipChain::empty(); - chain.push(Transition::unsigned(issuer, alice, None)).unwrap(); - let prior = chain.transitions.last().unwrap().content_hash(); - chain - .push(Transition::unsigned(alice, bob, Some(prior))) - .unwrap(); - - Envelope::new( - prov, - chain, - Cargo::Usd { minor: 10_000 }, - JouleCost::measured(42), + fn party(n: u8) -> Signer { + Signer::single( + Aid::of_inception(Said::hash(&[n])), + 0, + SigningKey::from_bytes(&[n; 32]), ) - .unwrap() } - #[test] - fn said_is_stable() { - let a = fixture_envelope(); - let b = fixture_envelope(); - assert_eq!(a.id, b.id); - } - - #[test] - fn cbor_roundtrip_preserves_said() { - let env = fixture_envelope(); - let bytes = env.to_cbor().unwrap(); - let back = Envelope::from_cbor(&bytes).unwrap(); - assert_eq!(env, back); - back.verify_said().unwrap(); - } - - #[test] - fn an_envelope_decodes_only_from_its_own_encoding() { - let bytes = fixture_envelope().to_cbor().unwrap(); - assert_eq!(bytes[0], 0xa5, "a five-member map"); - - // An extra member the decoder would otherwise ignore. - let mut padded = bytes.clone(); - padded[0] = 0xa6; - padded.extend_from_slice(&[0x61, b'x', 0x00]); - assert!(matches!( - Envelope::from_cbor(&padded), - Err(EnvelopeError::NotItsEncoding) - )); - - // The same map written with an indefinite length. - let mut indefinite = bytes.clone(); - indefinite[0] = 0xbf; - indefinite.push(0xff); - assert!(matches!( - Envelope::from_cbor(&indefinite), - Err(EnvelopeError::NotItsEncoding) - )); - - assert!(Envelope::from_cbor(&bytes).is_ok()); - } - - #[test] - fn mutation_breaks_said() { - let mut env = fixture_envelope(); - env.cargo = Cargo::Usd { minor: 99_999 }; - let err = env.verify_said().unwrap_err(); - assert!(matches!(err, EnvelopeError::SaidMismatch { .. })); - } - - #[test] - fn sign_and_verify_happy_path() { - let env = fixture_envelope(); - let key = SigningKey::generate(&mut OsRng); - let sig = key.sign(env.id.as_bytes()); - crate::sign::verify(&env, &sig, &key.verifying_key()).unwrap(); - } - - #[test] - fn sign_and_verify_rejects_wrong_key() { - let env = fixture_envelope(); - let key = SigningKey::generate(&mut OsRng); - let other = SigningKey::generate(&mut OsRng); - let sig = key.sign(env.id.as_bytes()); - assert!(crate::sign::verify(&env, &sig, &other.verifying_key()).is_err()); - } - - #[test] - fn cargo_kinds_round_trip() { - for cargo in [ - Cargo::Bytes(vec![1, 2, 3]), - Cargo::Usd { minor: 7 }, - Cargo::JouleClaim { microjoules: 7 }, - Cargo::Custom { - type_uri: "urn:x:y".into(), - body: vec![9], - }, - ] { - let prov = Provenance::new( - Said::hash(b"i"), - chrono::Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap(), - vec![], - ); - let env = Envelope::new( - prov, - OwnershipChain::empty(), - cargo.clone(), - JouleCost::default(), - ) - .unwrap(); - let bytes = env.to_cbor().unwrap(); - let back = Envelope::from_cbor(&bytes).unwrap(); - assert_eq!(back.cargo.kind(), cargo.kind()); + fn book(parties: &[&Signer]) -> KeyBook { + let mut b = KeyBook::new(); + for p in parties { + b.insert(p.aid, p.establishment, p.key_state()); } + b + } + + #[test] + fn issue_transfer_and_verify() { + let (issuer, alice, bob) = (party(1), party(2), party(3)); + let keys = book(&[&issuer, &alice, &bob]); + let byte = Envelope::issue(&issuer, alice.aid, [7; 16], Cargo::usd_micros(1_000_000), JouleCost::measured(5)); + let v = byte.verify(&keys).unwrap(); + assert_eq!((v.owner, v.history_hash), (alice.aid, byte.identity().0)); + + let moved = byte.transfer(&alice, bob.aid, 10).transfer(&bob, alice.aid, 11); + let v = moved.verify(&keys).unwrap(); + assert_eq!(v.owner, alice.aid); + assert_eq!(v.identity, byte.identity(), "transfers never change the identity"); + assert_ne!(moved.said(), byte.said(), "each snapshot has its own SAID"); + assert_eq!(v.history_hash, moved.history_hash()); + + // The wire form round-trips, and decoding checks both SAIDs. + assert_eq!(Envelope::from_cbor(&moved.to_cbor()).unwrap(), moved); + } + + #[test] + fn each_defect_is_refused_with_its_class() { + let (issuer, alice, bob, mallory) = (party(1), party(2), party(3), party(4)); + let keys = book(&[&issuer, &alice, &bob, &mallory]); + let byte = Envelope::issue(&issuer, alice.aid, [7; 16], Cargo::joules(9), JouleCost::default()); + let class = |e: &Envelope| e.verify(&keys).unwrap_err().class(); + + // Mallory signs away a byte Alice owns. + assert_eq!(class(&byte.transfer(&mallory, bob.aid, 1)), "not_current_owner"); + + // A transition grafted from another point in history. + let mut grafted = byte.transfer(&alice, bob.aid, 1); + grafted.ownership_chain[0].previous_history_hash = [0; 32]; + let grafted = Envelope::stamped(grafted.provenance, grafted.ownership_chain, grafted.joule_cost); + assert_eq!(class(&grafted), "broken_history_chain"); + + // Two transfers in one frame. + assert_eq!(class(&byte.transfer(&alice, bob.aid, 5).transfer(&bob, alice.aid, 5)), "frame_not_increasing"); + + // The cargo changed after the issuer signed: the identity no longer checks. + let mut inflated = byte.clone(); + inflated.provenance.cargo = Cargo::joules(9_000_000); + let inflated = Envelope::stamped(inflated.provenance, vec![], inflated.joule_cost); + assert_eq!(class(&inflated), "said_mismatch"); + + // Re-stamping the identity too does not help: the issuer never signed it. + let mut forged = byte.clone(); + forged.provenance.cargo = Cargo::joules(9_000_000); + forged.provenance.d = forged.provenance.compute_said(); + let forged = Envelope::stamped(forged.provenance, vec![], forged.joule_cost); + assert_eq!(class(&forged), "signature_invalid"); + + // The snapshot's own SAID is checked on decode. + let mut stale = byte.with_joule_cost(JouleCost::measured(1)); + stale.joule_cost = JouleCost::measured(2); + assert_eq!(Envelope::from_cbor(&stale.to_cbor()).unwrap_err().class(), "said_mismatch"); + } + + #[test] + fn a_member_the_envelope_does_not_define_is_refused() { + let byte = Envelope::issue(&party(1), party(2).aid, [0; 16], Cargo::opaque(vec![]), JouleCost::default()); + let mut value = wire::to_value(&byte).unwrap(); + if let canonical::Value::Map(entries) = &mut value { + entries.push((canonical::Value::text("extra"), canonical::Value::Null)); + } + // Re-derive d so only the extra member is wrong. + let bytes = said::derive(&value, SAID_FIELD).unwrap().encoded; + assert_eq!(Envelope::from_cbor(&bytes).unwrap_err().class(), "not_an_envelope"); + } + + #[test] + fn cargo_bytes_written_as_an_array_are_refused() { + let byte = Envelope::issue(&party(1), party(2).aid, [0; 16], Cargo::opaque(vec![1, 2]), JouleCost::default()); + // Both SAIDs re-derived over the array form, so a verifier that hashes + // what it decoded, without checking each member's form, would accept. + let mut provenance = wire::to_value(&byte.provenance).unwrap(); + let canonical::Value::Map(members) = &mut provenance else { unreachable!() }; + for (k, v) in members.iter_mut() { + if *k == canonical::Value::text("cargo") { + *v = canonical::Value::map([ + ("bytes", canonical::Value::Array(vec![canonical::Value::Unsigned(1), canonical::Value::Unsigned(2)])), + ("type", canonical::Value::Unsigned(0)), + ]); + } + } + let provenance = canonical::decode(&said::derive(&provenance, SAID_FIELD).unwrap().encoded).unwrap(); + let mut envelope = wire::to_value(&byte).unwrap(); + let canonical::Value::Map(members) = &mut envelope else { unreachable!() }; + for (k, v) in members.iter_mut() { + if *k == canonical::Value::text("provenance") { + *v = provenance.clone(); + } + } + let bytes = said::derive(&envelope, SAID_FIELD).unwrap().encoded; + assert_eq!(Envelope::from_cbor(&bytes).unwrap_err().class(), "not_an_envelope"); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/joule_cost.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/joule_cost.rs index ef2865dd0..0167d3394 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/joule_cost.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/joule_cost.rs @@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize}; /// Energy cost attributed to producing or transmitting this envelope. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct JouleCost { /// Joules measured by hardware, expressed in microjoules. pub measured_microjoules: u64, diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/keys.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/keys.rs new file mode 100644 index 000000000..a109574d8 --- /dev/null +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/keys.rs @@ -0,0 +1,384 @@ +//! Controller keys and signatures. +//! +//! Parties are named by controller AIDs (identity spec §3.1), and a +//! controller's keys change as it rotates them. A signature therefore says +//! which of the controller's establishment events put the signing keys in +//! force, and carries one indexed signature per signing key; a verifier looks +//! that key set up through [`KeyStates`] — the replayed key-event log +//! (identity spec §4) — and requires the set's threshold of valid signatures. +//! A byte signed under keys since rotated away stays verifiable, because the +//! signature names the event whose keys made it. + +use std::collections::BTreeMap; + +use ed25519_dalek::{Signer as _, SigningKey, VerifyingKey}; +use serde::{Deserialize, Serialize}; + +use crate::said::Aid; +use crate::sign::SignatureError; + +/// The algorithm identifier of Ed25519 (treatise §8.3). +pub const ED25519: u8 = 0x01; + +/// A public key with its one-byte algorithm identifier in front (identity +/// spec §3.3). It serializes as a byte string. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)] +pub struct PrefixedKey(#[serde(with = "serde_bytes")] pub Vec); + +impl PrefixedKey { + /// An Ed25519 key: `0x01` followed by its 32 bytes. + pub fn ed25519(key: &VerifyingKey) -> Self { + let mut v = Vec::with_capacity(33); + v.push(ED25519); + v.extend_from_slice(key.as_bytes()); + Self(v) + } + + /// The algorithm identifier. + pub fn algorithm(&self) -> Option { + self.0.first().copied() + } + + /// The key bytes after the identifier. + pub fn key(&self) -> &[u8] { + self.0.get(1..).unwrap_or(&[]) + } +} + +impl std::fmt::Debug for PrefixedKey { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "PrefixedKey(")?; + for b in &self.0 { + write!(f, "{b:02x}")?; + } + write!(f, ")") + } +} + +/// The key set and signing threshold one establishment event put in force. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct KeyState { + pub keys: Vec, + pub threshold: u64, +} + +/// Where a verifier learns which keys a controller signed with: the key state +/// each of the controller's establishment events (inception or rotation) put +/// in force, from its replayed key-event log. +pub trait KeyStates { + /// The key state `aid`'s establishment event at sequence `establishment` + /// put in force, if the verifier holds that event. + fn key_state(&self, aid: &Aid, establishment: u64) -> Option; +} + +/// Key states held in memory. +#[derive(Clone, Debug, Default)] +pub struct KeyBook(BTreeMap<(Aid, u64), KeyState>); + +impl KeyBook { + pub fn new() -> Self { + Self::default() + } + + /// Record the key state `aid`'s establishment event `establishment` set. + pub fn insert(&mut self, aid: Aid, establishment: u64, state: KeyState) { + self.0.insert((aid, establishment), state); + } +} + +impl KeyStates for KeyBook { + fn key_state(&self, aid: &Aid, establishment: u64) -> Option { + self.0.get(&(*aid, establishment)).cloned() + } +} + +/// One key's signature: the key's index in the signing key set, and the +/// signature with the key's algorithm identifier in front. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct IndexedSignature { + pub index: u64, + #[serde(with = "serde_bytes")] + pub signature: Vec, +} + +/// A controller's signature on a message (treatise §8.3, §8.4): the sequence +/// number of the establishment event whose key set signed, and one indexed +/// signature per signing key. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct ControllerSignature { + pub establishment: u64, + pub signatures: Vec, +} + +/// Signs as a controller: its AID, the establishment event currently in force, +/// and the private keys of that event's key set, by index. +#[derive(Clone, Debug)] +pub struct Signer { + pub aid: Aid, + pub establishment: u64, + pub keys: Vec<(u64, SigningKey)>, +} + +impl Signer { + /// A controller with one Ed25519 key, at index 0 of the key set its + /// establishment event `establishment` put in force. + pub fn single(aid: Aid, establishment: u64, key: SigningKey) -> Self { + Self { + aid, + establishment, + keys: vec![(0, key)], + } + } + + /// The key state this signer's keys form, for a threshold of every key. + pub fn key_state(&self) -> KeyState { + KeyState { + keys: self + .keys + .iter() + .map(|(_, k)| PrefixedKey::ed25519(&k.verifying_key())) + .collect(), + threshold: self.keys.len() as u64, + } + } + + /// Sign `message` with every key this signer holds. + pub fn sign(&self, message: &[u8]) -> ControllerSignature { + ControllerSignature { + establishment: self.establishment, + signatures: self + .keys + .iter() + .map(|(index, key)| { + let mut signature = Vec::with_capacity(65); + signature.push(ED25519); + signature.extend_from_slice(&key.sign(message).to_bytes()); + IndexedSignature { + index: *index, + signature, + } + }) + .collect(), + } + } +} + +/// Why a controller signature was refused. +#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] +pub enum ControllerSignatureError { + #[error("no key state for {aid} at establishment event {establishment}")] + UnknownKeyState { aid: String, establishment: u64 }, + #[error("signature index {0} is outside the key set")] + IndexOutOfRange(u64), + #[error("key index {0} signed twice")] + DuplicateIndex(u64), + #[error("algorithm {0:#04x} is not supported")] + UnsupportedAlgorithm(u8), + #[error("a signature's algorithm differs from its key's")] + AlgorithmMismatch, + #[error("not an Ed25519 public key")] + MalformedVerifyingKey, + #[error("an Ed25519 signature is 64 bytes after its identifier")] + MalformedSignature, + #[error("a signature does not verify")] + SignatureInvalid, + #[error("{valid} valid signatures; the key state needs {threshold}")] + BelowThreshold { valid: u64, threshold: u64 }, +} + +impl ControllerSignatureError { + /// The error class as the conformance pack names it. + pub fn class(&self) -> &'static str { + match self { + Self::UnknownKeyState { .. } => "unknown_key_state", + Self::IndexOutOfRange(_) => "key_index_out_of_range", + Self::DuplicateIndex(_) => "duplicate_key_index", + Self::UnsupportedAlgorithm(_) => "unsupported_algorithm", + Self::AlgorithmMismatch => "algorithm_mismatch", + Self::MalformedVerifyingKey => "malformed_verifying_key", + Self::MalformedSignature => "malformed_signature", + Self::SignatureInvalid => "signature_invalid", + Self::BelowThreshold { .. } => "below_threshold", + } + } +} + +/// Verify `signature` as `aid`'s over `message`. +/// +/// The key state of the establishment event the signature names is looked up +/// in `keys`. Every listed signature must be valid — one bad signature refuses +/// the whole, so a signature set means one thing however it is relayed — each +/// key may sign once, and at least the state's threshold of keys must have +/// signed. Ed25519 is verified strictly, as in [`crate::sign`]. +pub fn verify_controller_signature( + keys: &impl KeyStates, + aid: &Aid, + message: &[u8], + signature: &ControllerSignature, +) -> Result<(), ControllerSignatureError> { + let state = keys + .key_state(aid, signature.establishment) + .ok_or_else(|| ControllerSignatureError::UnknownKeyState { + aid: aid.to_text(), + establishment: signature.establishment, + })?; + verify_against(&state, message, &signature.signatures) +} + +/// Verify indexed signatures over `message` against a known key state: every +/// listed signature valid, each key signing at most once, and at least the +/// state's threshold of keys signing. The rule [`verify_controller_signature`] +/// applies once it has looked the key state up, and the one a key-event log's +/// own events are held to. +pub fn verify_against( + state: &KeyState, + message: &[u8], + signatures: &[IndexedSignature], +) -> Result<(), ControllerSignatureError> { + let mut seen = std::collections::BTreeSet::new(); + for s in signatures { + let key = usize::try_from(s.index) + .ok() + .and_then(|i| state.keys.get(i)) + .ok_or(ControllerSignatureError::IndexOutOfRange(s.index))?; + if !seen.insert(s.index) { + return Err(ControllerSignatureError::DuplicateIndex(s.index)); + } + verify_one(key, message, &s.signature)?; + } + // At least one key signs, whatever a key state claims: a threshold of + // zero would let an empty signature list speak for the controller. + let (valid, threshold) = (seen.len() as u64, state.threshold.max(1)); + if valid < threshold { + return Err(ControllerSignatureError::BelowThreshold { valid, threshold }); + } + Ok(()) +} + +fn verify_one( + key: &PrefixedKey, + message: &[u8], + signature: &[u8], +) -> Result<(), ControllerSignatureError> { + let algorithm = key + .algorithm() + .ok_or(ControllerSignatureError::MalformedVerifyingKey)?; + if signature.first() != Some(&algorithm) { + return Err(ControllerSignatureError::AlgorithmMismatch); + } + if algorithm != ED25519 { + return Err(ControllerSignatureError::UnsupportedAlgorithm(algorithm)); + } + crate::sign::verify(key.key(), message, &signature[1..]).map_err(|e| match e { + SignatureError::MalformedVerifyingKey => ControllerSignatureError::MalformedVerifyingKey, + SignatureError::MalformedSignature(_) => ControllerSignatureError::MalformedSignature, + SignatureError::SignatureInvalid => ControllerSignatureError::SignatureInvalid, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::Said; + + fn key(n: u8) -> SigningKey { + SigningKey::from_bytes(&[n; 32]) + } + + #[test] + fn no_key_state_speaks_without_a_signature() { + // A replayed log never puts a threshold of 0 in force, but a key state + // built any other way might: an empty signature list still says nothing. + let state = KeyState { + keys: vec![PrefixedKey::ed25519(&key(1).verifying_key())], + threshold: 0, + }; + assert_eq!( + verify_against(&state, b"anything", &[]), + Err(ControllerSignatureError::BelowThreshold { + valid: 0, + threshold: 1 + }) + ); + } + + #[test] + fn a_threshold_of_distinct_keys_must_sign() { + let aid = Aid::of_inception(Said::hash(b"multi")); + let two_of_three = Signer { + aid, + establishment: 0, + keys: vec![(0, key(1)), (2, key(3))], + }; + let mut book = KeyBook::new(); + book.insert( + aid, + 0, + KeyState { + keys: [1, 2, 3] + .map(|n| PrefixedKey::ed25519(&key(n).verifying_key())) + .to_vec(), + threshold: 2, + }, + ); + let message = b"transfer"; + let sig = two_of_three.sign(message); + assert_eq!( + verify_controller_signature(&book, &aid, message, &sig), + Ok(()) + ); + + let mut one = sig.clone(); + one.signatures.pop(); + assert_eq!( + verify_controller_signature(&book, &aid, message, &one) + .unwrap_err() + .class(), + "below_threshold" + ); + + let mut twice = one.clone(); + twice.signatures.push(twice.signatures[0].clone()); + assert_eq!( + verify_controller_signature(&book, &aid, message, &twice) + .unwrap_err() + .class(), + "duplicate_key_index" + ); + + // A signature at index 1 made by key 3: one bad signature refuses all. + let mut misplaced = sig.clone(); + misplaced.signatures[1].index = 1; + assert_eq!( + verify_controller_signature(&book, &aid, message, &misplaced) + .unwrap_err() + .class(), + "signature_invalid" + ); + + let mut stale = sig.clone(); + stale.establishment = 1; + assert_eq!( + verify_controller_signature(&book, &aid, message, &stale) + .unwrap_err() + .class(), + "unknown_key_state" + ); + } + + #[test] + fn the_signature_algorithm_must_be_the_keys() { + let aid = Aid::of_inception(Said::hash(b"alg")); + let signer = Signer::single(aid, 0, key(9)); + let mut book = KeyBook::new(); + book.insert(aid, 0, signer.key_state()); + let mut sig = signer.sign(b"m"); + sig.signatures[0].signature[0] = 0x02; + assert_eq!( + verify_controller_signature(&book, &aid, b"m", &sig) + .unwrap_err() + .class(), + "algorithm_mismatch" + ); + } +} diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/lib.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/lib.rs index a220b17f8..c9a3b1fe9 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/lib.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/lib.rs @@ -1,45 +1,50 @@ //! Smart Byte core types. //! -//! This crate provides the load-bearing primitives of the Smart Byte -//! substrate's reference implementation: +//! The load-bearing primitives of the Smart Byte substrate's reference +//! implementation: //! -//! * [`Envelope`] — content-addressed signed carrier of arbitrary cargo -//! with intrinsic provenance, ownership history, and energy cost. -//! * [`Said`] — Self-Addressing IDentifier (BLAKE3 over canonical CBOR). -//! [`said::derive`] / [`said::verify`] implement identity spec §2 over -//! [`canonical`] CBOR; see the [`said`] module for which identifiers in this -//! crate do and do not follow it yet. -//! * [`sign`] / [`verify`] — Ed25519 signature over the envelope's SAID. -//! * [`Cargo`] — discriminated union of byte payload, USD claim, -//! joule claim, or custom typed body. -//! * [`JouleCost`] — measured and estimated microjoules of energy -//! attributable to producing or transmitting the envelope. -//! * [`Provenance`] — issuer SAID, issuance time, authorization blob. -//! * [`OwnershipChain`] — git-style chain of signed transitions. +//! * [`Envelope`] — a smart byte (treatise §8): its [`Provenance`] (the +//! issuer's origin attestation, whose SAID is the byte's identity), its +//! ownership chain of signed [`Transition`]s, and its [`JouleCost`]. +//! [`Envelope::verify`] is treatise §10.2's verification procedure. +//! * [`Cargo`] — a registered type tag and opaque bytes (treatise §8.5). +//! * [`Said`] and [`Aid`] — self-addressing identifiers and controller +//! identifiers (identity spec §2, §3.1), computed by [`said::derive`] over +//! [`canonical`] CBOR and spelled in the spec's text form wherever they are +//! serialized. +//! * [`keys`] — controller key states and threshold signatures: a signature +//! names the establishment event whose keys made it, so a byte signed under +//! keys since rotated stays verifiable. +//! * [`wire`] — canonical CBOR for any serde type, with strict decoding. +//! * [`inbox`] — receiving SAID-addressed objects (identity spec §2.6). //! -//! The substrate's load-bearing rule is *self-addressing*: the -//! envelope's `id` field is the BLAKE3 hash of its own canonical -//! CBOR encoding with the `id` field zeroed. Two implementers in any -//! language can therefore compute the same SAID for the same -//! semantic envelope without trusting each other. +//! Everything that travels or is hashed is canonical CBOR, so any +//! implementation in any language computes the same identifiers for the same +//! byte without trusting another. pub mod canonical; pub mod cargo; pub mod envelope; pub mod inbox; +pub mod keys; pub mod joule_cost; pub mod ownership; pub mod provenance; pub mod said; pub mod sign; +pub mod wire; pub use cargo::Cargo; -pub use envelope::{Envelope, EnvelopeError}; +pub use envelope::{Envelope, EnvelopeError, Verified}; pub use joule_cost::JouleCost; -pub use ownership::{OwnershipChain, Transition}; +pub use keys::{ + ControllerSignature, ControllerSignatureError, IndexedSignature, KeyBook, KeyState, KeyStates, + PrefixedKey, Signer, +}; +pub use ownership::Transition; pub use provenance::Provenance; -pub use said::{Said, SaidCheckError, SaidDerivation, SaidError}; -pub use sign::{SaidSignatureError, Signature, SigningKey, VerifyingKey, sign, verify}; +pub use said::{Aid, Said, SaidCheckError, SaidDerivation, SaidError}; +pub use sign::{Signature, SignatureError, SigningKey, VerifyingKey}; /// BLAKE3 32-byte hash output. Re-exported so downstream crates can /// refer to it without depending on `blake3` directly. diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/ownership.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/ownership.rs index fa69247ff..d01790bea 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/ownership.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/ownership.rs @@ -1,153 +1,103 @@ -//! Git-style ownership chain. +//! The ownership chain: signature-bound transitions (treatise §8.4, §10.1). //! -//! Every transfer of an envelope is a [`Transition`] whose `prior_hash` -//! points at the BLAKE3 of the previous transition (CBOR-encoded). The -//! genesis transition has `prior_hash = None`. Replaying the chain -//! reproduces the current owner; tampering with any transition breaks -//! the hash linkage. +//! Each transition moves the byte from its current owner to a new one at a +//! lockstep frame. The owner signs (byte identity, previous history hash, +//! from, to, frame), which scopes the signature to exactly one transition of +//! exactly one byte at one point in its history. The history hash starts as +//! the byte's identity and advances one step per transition: +//! `next = BLAKE3(previous ‖ payload)`, the payload being the transition's +//! from, to, frame and signature. use serde::{Deserialize, Serialize}; use crate::Blake3Hash; -use crate::said::Said; +use crate::keys::{ControllerSignature, Signer}; +use crate::said::{Aid, Said}; -/// Wire-format Ed25519 signature: 64 bytes. We hold the raw bytes here -/// (rather than `ed25519_dalek::Signature`) so that transitions can be -/// CBOR-round-tripped without depending on the dalek crate's -/// Serialize impl quirks. -pub type SignatureBytes = [u8; 64]; - -/// One link in the ownership chain. +/// One link in a byte's ownership chain. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Transition { - /// Outgoing owner SAID. - pub from: Said, - /// Incoming owner SAID. - pub to: Said, - /// Signature by `from` over the canonical CBOR of this transition - /// with the `signature` field zeroed. - #[serde(with = "serde_bytes_array")] - pub signature: SignatureBytes, - /// BLAKE3 hash of the previous transition's CBOR; `None` only for - /// the genesis transition. - pub prior_hash: Option, + /// The byte's history hash before this transition. + #[serde(with = "crate::wire::fixed_bytes")] + pub previous_history_hash: Blake3Hash, + /// The owner giving the byte up. + pub from: Aid, + /// The owner receiving it. + pub to: Aid, + /// The lockstep frame the transition is applied in. + pub frame: u64, + /// `from`'s signature over [`Transition::signed_bytes`]. + pub signature: ControllerSignature, +} + +#[derive(Serialize)] +struct Signed<'a> { + byte: &'a Said, + #[serde(with = "serde_bytes")] + previous_history_hash: &'a [u8], + from: &'a Aid, + to: &'a Aid, + frame: u64, +} + +#[derive(Serialize)] +struct Payload<'a> { + from: &'a Aid, + to: &'a Aid, + frame: u64, + signature: &'a ControllerSignature, } impl Transition { - /// Construct a transition whose `signature` field is the all-zero - /// placeholder. Callers fill in the signature after computing it - /// over this canonical form. - pub fn unsigned(from: Said, to: Said, prior_hash: Option) -> Self { - Self { - from, + /// A transition of byte `byte` from `owner` to `to` at `frame`, from the + /// history point `previous_history_hash`, signed by `owner`. + pub fn sign( + owner: &Signer, + byte: &Said, + previous_history_hash: Blake3Hash, + to: Aid, + frame: u64, + ) -> Self { + let mut t = Transition { + previous_history_hash, + from: owner.aid, to, - signature: [0u8; 64], - prior_hash, - } + frame, + signature: ControllerSignature { + establishment: 0, + signatures: vec![], + }, + }; + t.signature = owner.sign(&t.signed_bytes(byte)); + t } - /// BLAKE3 of this transition's canonical CBOR. - pub fn content_hash(&self) -> Blake3Hash { - let bytes = serde_cbor::to_vec(self).expect("Transition CBOR is infallible"); - *blake3::hash(&bytes).as_bytes() - } -} - -/// Append-only chain of [`Transition`]s. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct OwnershipChain { - pub transitions: Vec, -} - -impl OwnershipChain { - /// Construct an empty chain (no owners yet). - pub fn empty() -> Self { - Self::default() - } - - /// SAID of the current owner, if any. - pub fn current_owner(&self) -> Option<&Said> { - self.transitions.last().map(|t| &t.to) - } - - /// Append a transition. Returns an error if `prior_hash` does not - /// match the previous transition's content hash. - pub fn push(&mut self, t: Transition) -> Result<(), OwnershipError> { - let expected_prior = self.transitions.last().map(|p| p.content_hash()); - if t.prior_hash != expected_prior { - return Err(OwnershipError::BrokenLink); - } - self.transitions.push(t); - Ok(()) - } -} - -/// Ownership-chain errors. -#[derive(Debug, thiserror::Error)] -pub enum OwnershipError { - #[error("prior_hash does not match preceding transition")] - BrokenLink, -} - -/// Custom serde for fixed-length byte arrays. CBOR encodes a `[u8; 64]` -/// as an array-of-u8 by default, which is correct but inefficient. -/// Using `serde_bytes` produces a `bytes` major type instead. -mod serde_bytes_array { - use serde::{Deserialize, Deserializer, Serialize, Serializer}; - - pub fn serialize( - bytes: &[u8; 64], - ser: S, - ) -> Result { - serde_bytes::Bytes::new(bytes).serialize(ser) - } - - pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<[u8; 64], D::Error> { - let v: serde_bytes::ByteBuf = serde_bytes::ByteBuf::deserialize(de)?; - let b = v.into_vec(); - if b.len() != 64 { - return Err(serde::de::Error::custom("expected 64 bytes for signature")); - } - let mut out = [0u8; 64]; - out.copy_from_slice(&b); - Ok(out) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn principal(seed: u8) -> Said { - Said::hash(&[seed; 8]) - } - - #[test] - fn chain_links_through_prior_hash() { - let mut chain = OwnershipChain::empty(); - let alice = principal(1); - let bob = principal(2); - let carol = principal(3); - - let t0 = Transition::unsigned(alice, bob, None); - chain.push(t0.clone()).unwrap(); - let t1 = Transition::unsigned(bob, carol, Some(t0.content_hash())); - chain.push(t1).unwrap(); - - assert_eq!(chain.current_owner(), Some(&carol)); - } - - #[test] - fn broken_link_rejected() { - let mut chain = OwnershipChain::empty(); - let alice = principal(1); - let bob = principal(2); - let carol = principal(3); - - chain - .push(Transition::unsigned(alice, bob, None)) - .unwrap(); - let bogus = Transition::unsigned(bob, carol, Some([7u8; 32])); - assert!(matches!(chain.push(bogus), Err(OwnershipError::BrokenLink))); + /// The message `from` signs: the canonical encoding of (byte identity, + /// previous history hash, from, to, frame) (treatise §8.4). + pub fn signed_bytes(&self, byte: &Said) -> Vec { + crate::wire::to_vec(&Signed { + byte, + previous_history_hash: &self.previous_history_hash, + from: &self.from, + to: &self.to, + frame: self.frame, + }) + .expect("a transition's signed fields always encode") + } + + /// The history hash after this transition (treatise §10.1). + pub fn next_history_hash(&self) -> Blake3Hash { + let payload = crate::wire::to_vec(&Payload { + from: &self.from, + to: &self.to, + frame: self.frame, + signature: &self.signature, + }) + .expect("a transition's payload always encodes"); + let mut h = blake3::Hasher::new(); + h.update(&self.previous_history_hash); + h.update(&payload); + *h.finalize().as_bytes() } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/provenance.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/provenance.rs index 5395f6cb9..d37830cb9 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/provenance.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/provenance.rs @@ -1,29 +1,82 @@ -//! Issuer-side metadata stamped into every envelope. +//! Provenance: a byte's origin attestation (treatise §8.3). +//! +//! The issuer attests to four things — itself, the recipient, a nonce, and the +//! cargo — and signs them. The attestation's SAID, computed over all of it +//! including the signature, is the byte's identity (treatise §8.2): computed +//! once at issuance and never changed by anything that happens to the byte +//! afterwards. -use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; -use crate::said::Said; +use crate::cargo::Cargo; +use crate::keys::{ControllerSignature, Signer}; +use crate::said::{self, Aid, Said}; -/// Provenance metadata. +/// A byte's origin attestation. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Provenance { - /// The principal SAID that authored this envelope. - pub issuer: Said, - /// Wall-clock time of issuance. - pub issued_at: DateTime, - /// Opaque authorization blob. For USD bytes this typically holds a - /// regulated-issuer attestation; for joule bytes it is empty. - pub authorization: Vec, + /// The byte's identity: this attestation's SAID. + pub d: Said, + /// The issuer's controller AID. + pub issuer: Aid, + /// The first owner's controller AID. + pub recipient: Aid, + /// Sixteen bytes that keep two otherwise-identical issuances distinct. + #[serde(with = "crate::wire::fixed_bytes")] + pub nonce: [u8; 16], + pub cargo: Cargo, + /// The issuer's signature over [`Provenance::signed_bytes`]. + pub signature: ControllerSignature, +} + +/// What the issuer signs: the four attested fields (treatise §8.3). +#[derive(Serialize)] +struct Attested<'a> { + issuer: &'a Aid, + recipient: &'a Aid, + #[serde(with = "serde_bytes")] + nonce: &'a [u8], + cargo: &'a Cargo, } impl Provenance { - /// Convenience constructor. - pub fn new(issuer: Said, issued_at: DateTime, authorization: Vec) -> Self { - Self { - issuer, - issued_at, - authorization, - } + /// Issue: `issuer` signs the attested fields, and the attestation's SAID + /// is stamped into `d`. + pub fn issue(issuer: &Signer, recipient: Aid, nonce: [u8; 16], cargo: Cargo) -> Self { + let mut p = Provenance { + d: Said::default(), + issuer: issuer.aid, + recipient, + nonce, + cargo, + signature: ControllerSignature { + establishment: 0, + signatures: vec![], + }, + }; + p.signature = issuer.sign(&p.signed_bytes()); + p.d = p.compute_said(); + p + } + + /// The canonical encoding of the attested fields: the message the issuer + /// signs. + pub fn signed_bytes(&self) -> Vec { + crate::wire::to_vec(&Attested { + issuer: &self.issuer, + recipient: &self.recipient, + nonce: &self.nonce, + cargo: &self.cargo, + }) + .expect("the attested fields always encode") + } + + /// The SAID this attestation should carry. + pub fn compute_said(&self) -> Said { + let value = crate::wire::to_value(self).expect("a provenance always encodes"); + said::derive(&value, said::SAID_FIELD) + .expect("a provenance is a map with a `d` member") + .said } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/said.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/said.rs index 2177d9b09..339afdcc3 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/said.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/said.rs @@ -1,23 +1,12 @@ //! Self-Addressing IDentifiers. //! -//! A [`Said`] is a 32-byte BLAKE3 digest. It has two text forms, and they are -//! not interchangeable: -//! -//! * **The identity spec's form** (§2.2): the derivation code `E` followed by -//! the digest in *lowercase* unpadded base32 — 53 characters. -//! [`Said::to_said_text`] / [`Said::from_said_text`] produce and parse it, -//! and [`derive`] / [`verify`] implement the §2.1 procedure that yields it: -//! hash the canonical CBOR ([`crate::canonical`]) of the object with its -//! SAID field holding [`SAID_PLACEHOLDER`]. The conformance pack in -//! `smart-byte/conformance/v1` is generated from these functions. -//! * **The older form** that [`Display`](fmt::Display) and -//! [`Said::to_base32`] print: *upper-case* unpadded base32 with no -//! derivation code. It predates the identity spec. [`Envelope`] ids and the -//! key-event log in `smart-byte-keri-witness` still derive and print SAIDs -//! this way (`serde_cbor` over a zeroed id), so their identifiers are not -//! §2 SAIDs yet. -//! -//! [`Envelope`]: crate::Envelope +//! A [`Said`] is a 32-byte BLAKE3 digest. Its text form (identity spec §2.2) +//! is the derivation code `E` followed by the digest in lowercase unpadded +//! base32 — 53 characters — and that is how it displays and serializes, +//! everywhere. [`derive`] and [`verify`] implement the §2.1 procedure that +//! yields it: hash the canonical CBOR ([`crate::canonical`]) of the object with +//! its SAID field holding [`SAID_PLACEHOLDER`]. An [`Aid`] names a controller by +//! its inception event's SAID, spelled with `B` in place of `E` (§3.1). use std::fmt; @@ -50,10 +39,84 @@ pub enum SaidError { /// 32-byte self-addressing identifier. /// /// Equality is byte-equality. See the module docs for its two text forms. -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)] -#[serde(transparent)] +/// +/// Wherever a `Said` is serialized it takes its identity-spec text form +/// (`E` + 52 lowercase base32 characters), so every SAID on the wire is spelled +/// the one way the spec defines. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct Said(pub [u8; 32]); +impl Serialize for Said { + fn serialize(&self, s: S) -> Result { + s.serialize_str(&self.to_said_text()) + } +} + +impl<'de> Deserialize<'de> for Said { + fn deserialize>(d: D) -> Result { + let text = String::deserialize(d)?; + Said::from_said_text(&text).map_err(serde::de::Error::custom) + } +} + +/// A controller identifier, or AID (identity spec §3.1): the SAID of the +/// controller's inception event with the derivation code `E` replaced by `B`. +/// It names the same 32 digest bytes; its text form is `B` + 52 lowercase +/// base32 characters, and it serializes as that text. +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct Aid(pub Said); + +impl Aid { + /// The AID of the controller whose inception event has SAID `inception`. + pub fn of_inception(inception: Said) -> Self { + Aid(inception) + } + + /// The SAID of the inception event this AID names. + pub fn inception_said(&self) -> Said { + self.0 + } + + /// `B` + the inception SAID's 52 base32 characters. + pub fn to_text(&self) -> String { + let said = self.0.to_said_text(); + format!("B{}", &said[1..]) + } + + /// Parse the text form, with the same one-spelling rule as a SAID. + pub fn from_text(s: &str) -> Result { + let body = s + .strip_prefix('B') + .ok_or_else(|| SaidError::Decode(format!("an AID starts with `B`: {s:?}")))?; + Ok(Aid(Said::from_said_text(&format!("E{body}"))?)) + } +} + +impl fmt::Display for Aid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.to_text()) + } +} + +impl fmt::Debug for Aid { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Aid({})", self.to_text()) + } +} + +impl Serialize for Aid { + fn serialize(&self, s: S) -> Result { + s.serialize_str(&self.to_text()) + } +} + +impl<'de> Deserialize<'de> for Aid { + fn deserialize>(d: D) -> Result { + let text = String::deserialize(d)?; + Aid::from_text(&text).map_err(serde::de::Error::custom) + } +} + impl Said { /// Construct a SAID by hashing arbitrary bytes with BLAKE3. pub fn hash(bytes: &[u8]) -> Self { @@ -145,6 +208,8 @@ pub enum SaidCheckError { MalformedSaid(String), #[error("the object states SAID {stated} but hashes to {computed}")] Mismatch { stated: String, computed: String }, + #[error("the `{0}` field is not the value its SAID derives")] + DerivedMismatch(String), } impl SaidCheckError { @@ -156,28 +221,62 @@ impl SaidCheckError { SaidCheckError::MissingSaidField(_) => "missing_said_field", SaidCheckError::MalformedSaid(_) => "malformed_said", SaidCheckError::Mismatch { .. } => "said_mismatch", + SaidCheckError::DerivedMismatch(_) => "derived_field_mismatch", } } } +/// A member whose value is derived from the object's own SAID — an inception +/// event's controller AID (identity spec §3.1). It holds the placeholder while +/// the object is hashed, exactly as the SAID field does. +#[derive(Clone, Copy)] +pub struct Derived<'a> { + pub field: &'a str, + pub from_said: fn(&Said) -> Value, +} + +/// The `i` member of an inception event: the controller AID its SAID names. +pub const INCEPTION_AID: Derived<'static> = Derived { + field: "i", + from_said: |said| Value::Text(Aid::of_inception(*said).to_text()), +}; + /// Derive the SAID of `object`, which carries its SAID in the member named /// `field` (identity spec §2.1). Whatever that member holds now is replaced by /// the placeholder before hashing, so deriving over an already-stamped object /// gives the same SAID as deriving over a fresh one. pub fn derive(object: &Value, field: &str) -> Result { + derive_with(object, field, &[]) +} + +/// [`derive`], for an object some of whose members are themselves derived from +/// its SAID: they hold the placeholder while it is hashed, then take the value +/// their rule gives. +pub fn derive_with( + object: &Value, + field: &str, + derived: &[Derived<'_>], +) -> Result { if !matches!(object, Value::Map(_)) { return Err(SaidCheckError::NotAMap); } let mut stamped = object.clone(); - let slot = stamped - .get_mut(field) - .ok_or_else(|| SaidCheckError::MissingSaidField(field.to_owned()))?; - *slot = Value::text(SAID_PLACEHOLDER); + for name in std::iter::once(field).chain(derived.iter().map(|d| d.field)) { + let slot = stamped + .get_mut(name) + .ok_or_else(|| SaidCheckError::MissingSaidField(name.to_owned()))?; + *slot = Value::text(SAID_PLACEHOLDER); + } let hashed = canonical::encode(&stamped)?; let said = Said::hash(&hashed); if let Some(slot) = stamped.get_mut(field) { *slot = Value::Text(said.to_said_text()); } + for d in derived { + if let Some(slot) = stamped.get_mut(d.field) { + *slot = (d.from_said)(&said); + } + } let encoded = canonical::encode(&stamped)?; Ok(SaidDerivation { hashed, @@ -194,6 +293,16 @@ pub fn derive(object: &Value, field: &str) -> Result Result { + verify_with(bytes, field, &[]) +} + +/// [`verify`], for an object with members derived from its SAID: each must +/// hold exactly the value its rule gives for the SAID the object states. +pub fn verify_with( + bytes: &[u8], + field: &str, + derived: &[Derived<'_>], +) -> Result { let object = canonical::decode(bytes)?; if !matches!(object, Value::Map(_)) { return Err(SaidCheckError::NotAMap); @@ -205,13 +314,18 @@ pub fn verify(bytes: &[u8], field: &str) -> Result { } Some(_) => return Err(SaidCheckError::MalformedSaid(field.to_owned())), }; - let computed = derive(&object, field)?.said; + let computed = derive_with(&object, field, derived)?.said; if computed != stated { return Err(SaidCheckError::Mismatch { stated: stated.to_said_text(), computed: computed.to_said_text(), }); } + for d in derived { + if object.get(d.field) != Some(&(d.from_said)(&stated)) { + return Err(SaidCheckError::DerivedMismatch(d.field.to_owned())); + } + } Ok(stated) } @@ -231,13 +345,13 @@ pub fn per_key_said(prefixed_key: &[u8]) -> Said { impl fmt::Display for Said { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.to_base32()) + f.write_str(&self.to_said_text()) } } impl fmt::Debug for Said { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Said({})", self.to_base32()) + write!(f, "Said({})", self.to_said_text()) } } @@ -350,6 +464,52 @@ mod tests { assert_eq!(verify(&bad, "d").unwrap_err().class(), "malformed_said"); } + #[test] + fn an_inception_event_placeholders_its_aid_with_its_said() { + let event = Value::map([ + ("d", Value::text(SAID_PLACEHOLDER)), + ("i", Value::text(SAID_PLACEHOLDER)), + ("s", Value::int(0)), + ]); + let d = derive_with(&event, "d", &[INCEPTION_AID]).unwrap(); + let aid = Aid::of_inception(d.said); + assert_eq!(d.object.get("i"), Some(&Value::Text(aid.to_text()))); + assert_eq!( + verify_with(&d.encoded, "d", &[INCEPTION_AID]).unwrap(), + d.said + ); + // The same SAID from a stamped copy: both fields are placeholdered. + assert_eq!( + derive_with(&d.object, "d", &[INCEPTION_AID]).unwrap().said, + d.said + ); + + // An `i` naming another controller is refused even though `d` is right. + let mut wrong = d.object.clone(); + *wrong.get_mut("i").unwrap() = Value::Text(Aid::of_inception(Said::hash(b"x")).to_text()); + let bytes = canonical::encode(&wrong).unwrap(); + assert_eq!( + verify_with(&bytes, "d", &[INCEPTION_AID]) + .unwrap_err() + .class(), + "derived_field_mismatch" + ); + } + + #[test] + fn saids_and_aids_serialize_as_their_text_forms() { + let said = Said::hash(b"wire"); + assert_eq!( + serde_json::to_string(&said).unwrap(), + format!("\"{}\"", said.to_said_text()) + ); + let aid = Aid::of_inception(said); + assert!(aid.to_text().starts_with('B')); + assert_eq!(aid.to_text()[1..], said.to_said_text()[1..]); + assert_eq!(Aid::from_text(&aid.to_text()).unwrap(), aid); + assert!(Aid::from_text(&said.to_said_text()).is_err()); + } + #[test] fn per_key_said_commits_to_the_algorithm_byte() { let key = [7u8; 32]; diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/sign.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/sign.rs index c75a70ef0..cbb9845a3 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/sign.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/sign.rs @@ -1,50 +1,15 @@ -//! Ed25519 sign + verify over an envelope's SAID. +//! Ed25519, verified strictly. //! -//! Signing an envelope means signing its 32-byte SAID. Because the SAID -//! is itself a BLAKE3 commitment to the canonical CBOR of the envelope -//! (with `id` zeroed), signing the SAID is sufficient to bind the -//! signature to the entire envelope content. +//! Every Smart Byte signature — an issuer's attestation, an owner's transfer, +//! a key event — is Ed25519 over the canonical encoding of the map it signs. +//! Controller signatures — indexed, thresholded, tied to a key state — live in +//! [`crate::keys`]; this module holds the single-key rule they build on. -use ed25519_dalek::Signer; -pub use ed25519_dalek::{Signature, SigningKey, Verifier, VerifyingKey}; +pub use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; -use crate::envelope::Envelope; -use crate::said::Said; - -/// Errors returned by verification. -#[derive(Debug, thiserror::Error)] -pub enum SignError { - #[error("signature verification failed: {0}")] - Bad(String), -} - -/// Sign an envelope's SAID with the given Ed25519 signing key. -pub fn sign(envelope: &Envelope, key: &SigningKey) -> Signature { - key.sign(envelope.id.as_bytes()) -} - -/// Verify that `sig` is a valid Ed25519 signature over `envelope.id` -/// produced by the private key matching `key`. -pub fn verify( - envelope: &Envelope, - sig: &Signature, - key: &VerifyingKey, -) -> Result<(), SignError> { - key.verify(envelope.id.as_bytes(), sig) - .map_err(|e| SignError::Bad(e.to_string())) -} - -/// Sign a SAID. The message is the 32 digest bytes the SAID encodes, not its -/// 53-character text — the same bytes [`sign`] signs for an envelope. -pub fn sign_said(said: &Said, key: &SigningKey) -> Signature { - key.sign(said.as_bytes()) -} - -/// Why a signature over a SAID was refused. +/// Why an Ed25519 signature was refused. #[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)] -pub enum SaidSignatureError { - #[error("not a well-formed SAID")] - MalformedSaid, +pub enum SignatureError { #[error("an Ed25519 signature is 64 bytes; got {0}")] MalformedSignature(usize), #[error("not an Ed25519 public key")] @@ -53,20 +18,18 @@ pub enum SaidSignatureError { SignatureInvalid, } -impl SaidSignatureError { +impl SignatureError { /// The error class as the conformance pack names it. pub fn class(&self) -> &'static str { match self { - SaidSignatureError::MalformedSaid => "malformed_said", - SaidSignatureError::MalformedSignature(_) => "malformed_signature", - SaidSignatureError::MalformedVerifyingKey => "malformed_verifying_key", - SaidSignatureError::SignatureInvalid => "signature_invalid", + SignatureError::MalformedSignature(_) => "malformed_signature", + SignatureError::MalformedVerifyingKey => "malformed_verifying_key", + SignatureError::SignatureInvalid => "signature_invalid", } } } -/// Verify `signature` over the SAID whose identity-spec text form is -/// `said_text`, under `verifying_key`. +/// Verify `signature` over `message` under `verifying_key`. /// /// Verification is RFC 8032 Ed25519, strict throughout: /// @@ -78,31 +41,29 @@ impl SaidSignatureError { /// /// Structural problems are reported before any cryptography runs, so a short /// signature is `malformed_signature` rather than a verification failure. -pub fn verify_said_signature( - said_text: &str, - signature: &[u8], +pub fn verify( verifying_key: &[u8], -) -> Result<(), SaidSignatureError> { - let said = Said::from_said_text(said_text).map_err(|_| SaidSignatureError::MalformedSaid)?; + message: &[u8], + signature: &[u8], +) -> Result<(), SignatureError> { let sig: [u8; 64] = signature .try_into() - .map_err(|_| SaidSignatureError::MalformedSignature(signature.len()))?; + .map_err(|_| SignatureError::MalformedSignature(signature.len()))?; let key: [u8; 32] = verifying_key .try_into() - .map_err(|_| SaidSignatureError::MalformedVerifyingKey)?; + .map_err(|_| SignatureError::MalformedVerifyingKey)?; if !encodes_y_below_p(&key) { - return Err(SaidSignatureError::MalformedVerifyingKey); + return Err(SignatureError::MalformedVerifyingKey); } - let key = - VerifyingKey::from_bytes(&key).map_err(|_| SaidSignatureError::MalformedVerifyingKey)?; - key.verify_strict(said.as_bytes(), &Signature::from_bytes(&sig)) - .map_err(|_| SaidSignatureError::SignatureInvalid) + let key = VerifyingKey::from_bytes(&key).map_err(|_| SignatureError::MalformedVerifyingKey)?; + key.verify_strict(message, &Signature::from_bytes(&sig)) + .map_err(|_| SignatureError::SignatureInvalid) } /// Whether a compressed Edwards point's `y` (its low 255 bits) is below the /// field prime `p = 2^255 - 19`. Decoders that skip this accept nineteen /// out-of-range spellings, some of which decompress to valid points. -fn encodes_y_below_p(point: &[u8; 32]) -> bool { +pub(crate) fn encodes_y_below_p(point: &[u8; 32]) -> bool { let top = point[31] & 0x7f; !(top == 0x7f && point[1..31].iter().all(|&b| b == 0xff) && point[0] >= 0xed) } @@ -131,33 +92,30 @@ mod tests { } #[test] - fn a_signature_over_a_said_verifies_and_nothing_else_does() { + fn a_signature_verifies_over_its_message_and_nothing_else_does() { let key = SigningKey::from_bytes(&[9u8; 32]); - let said = Said::hash(b"one"); - let text = said.to_said_text(); let vk = key.verifying_key().to_bytes(); - let sig = sign_said(&said, &key).to_bytes(); + let sig = key.sign(b"one").to_bytes(); - assert_eq!(verify_said_signature(&text, &sig, &vk), Ok(())); - let other = Said::hash(b"two").to_said_text(); + assert_eq!(verify(&vk, b"one", &sig), Ok(())); assert_eq!( - verify_said_signature(&other, &sig, &vk), - Err(SaidSignatureError::SignatureInvalid) + verify(&vk, b"two", &sig), + Err(SignatureError::SignatureInvalid) ); let stranger = SigningKey::from_bytes(&[8u8; 32]) .verifying_key() .to_bytes(); assert_eq!( - verify_said_signature(&text, &sig, &stranger), - Err(SaidSignatureError::SignatureInvalid) + verify(&stranger, b"one", &sig), + Err(SignatureError::SignatureInvalid) ); assert_eq!( - verify_said_signature(&text, &sig[..63], &vk), - Err(SaidSignatureError::MalformedSignature(63)) + verify(&vk, b"one", &sig[..63]), + Err(SignatureError::MalformedSignature(63)) ); assert_eq!( - verify_said_signature("Enope", &sig, &vk), - Err(SaidSignatureError::MalformedSaid) + verify(&vk[..31], b"one", &sig), + Err(SignatureError::MalformedVerifyingKey) ); } @@ -170,10 +128,9 @@ mod tests { key[0] = 0xf0; key[31] = 0x7f; assert!(VerifyingKey::from_bytes(&key).is_ok()); - let said = Said::hash(b"k"); assert_eq!( - verify_said_signature(&said.to_said_text(), &[0u8; 64], &key), - Err(SaidSignatureError::MalformedVerifyingKey) + verify(&key, b"k", &[0u8; 64]), + Err(SignatureError::MalformedVerifyingKey) ); // The largest in-range y is accepted as an encoding (and then fails // as a key or a signature, not as a spelling). @@ -187,13 +144,12 @@ mod tests { #[test] fn the_same_scalar_spelled_s_plus_l_is_refused() { let key = SigningKey::from_bytes(&[3u8; 32]); - let said = Said::hash(b"malleable"); - let mut sig = sign_said(&said, &key).to_bytes(); + let mut sig = key.sign(b"malleable").to_bytes(); let s_plus_l = add_l(&sig[32..]); sig[32..].copy_from_slice(&s_plus_l); assert_eq!( - verify_said_signature(&said.to_said_text(), &sig, &key.verifying_key().to_bytes()), - Err(SaidSignatureError::SignatureInvalid) + verify(&key.verifying_key().to_bytes(), b"malleable", &sig), + Err(SignatureError::SignatureInvalid) ); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-core/src/wire.rs b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/wire.rs new file mode 100644 index 000000000..707e3bfc5 --- /dev/null +++ b/smart-byte/smart-byte-rs/crates/smart-byte-core/src/wire.rs @@ -0,0 +1,698 @@ +//! The wire form of any serde type: canonical CBOR. +//! +//! [`to_value`] maps a `Serialize` type onto the canonical data model and +//! [`to_vec`] encodes it canonically (identity spec §2.3); [`from_slice`] +//! decodes strictly — non-canonical bytes are refused, never re-encoded — and +//! maps the result back with [`from_value`]. Every Smart Byte object that +//! travels or is hashed goes through here, so a type gets its canonical +//! encoding from its `derive`s alone. +//! +//! [`from_slice`] also refuses bytes that are canonical CBOR but not the +//! decoded object's own encoding. Serde lets one form stand in for another — +//! an array of small integers for a byte string, a byte string for text, an +//! array for a struct — and a member omitted when empty may still be written. +//! Each would give one object a second encoding, with bytes its SAID does not +//! cover, so the object is re-encoded and anything but the input is refused. +//! +//! The mapping: +//! +//! | serde | canonical value | +//! | ----------------------------- | ----------------------------------- | +//! | `bool` | `false` / `true` | +//! | integers | unsigned / negative integer | +//! | `f32`, `f64` | refused: floats are forbidden | +//! | `char`, `str` | text | +//! | bytes (`serde_bytes`) | byte string | +//! | `None`, `()`, unit struct | `null` | +//! | `Some(x)`, newtype struct | `x` | +//! | sequence, tuple | array | +//! | map, struct | map (struct fields as text keys) | +//! | unit variant | text: the variant name | +//! | newtype/tuple/struct variant | one-entry map: variant name → value | +//! +//! A fixed-size byte array (`[u8; N]`) serializes as a tuple of integers, +//! which is an array, not a byte string. Byte fields therefore carry +//! `#[serde(with = "serde_bytes")]` or a helper from this crate. + +use serde::de::{self, DeserializeOwned, IntoDeserializer, Visitor}; +use serde::ser::{self, Serialize}; + +use crate::canonical::{self, CanonicalError, Value}; + +/// Why a value could not be mapped to or from the wire form. +#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] +pub enum WireError { + #[error(transparent)] + Canonical(#[from] CanonicalError), + #[error("{0}")] + Message(String), + /// Canonical CBOR, but not the encoding of the object it decodes to. + #[error("the bytes are not the encoding of the object they decode to")] + NotItsEncoding, +} + +impl ser::Error for WireError { + fn custom(msg: T) -> Self { + WireError::Message(msg.to_string()) + } +} + +impl de::Error for WireError { + fn custom(msg: T) -> Self { + WireError::Message(msg.to_string()) + } +} + +/// The canonical value of `value`. +pub fn to_value(value: &T) -> Result { + value.serialize(ValueSerializer) +} + +/// The canonical encoding of `value`. +pub fn to_vec(value: &T) -> Result, WireError> { + Ok(canonical::encode(&to_value(value)?)?) +} + +/// Rebuild a `T` from a canonical value. +pub fn from_value(value: Value) -> Result { + T::deserialize(value) +} + +/// Decode `bytes` into a `T`. The bytes must be canonical, and exactly the +/// encoding of the `T` they decode to. +pub fn from_slice(bytes: &[u8]) -> Result { + let decoded: T = from_value(canonical::decode(bytes)?)?; + if to_vec(&decoded)? != bytes { + return Err(WireError::NotItsEncoding); + } + Ok(decoded) +} + +/// Serde for a fixed-size byte array as a byte string of exactly that length +/// — `#[serde(with = "smart_byte_core::wire::fixed_bytes")]`. +pub mod fixed_bytes { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize( + bytes: &[u8; N], + s: S, + ) -> Result { + s.serialize_bytes(bytes) + } + + pub fn deserialize<'de, D: Deserializer<'de>, const N: usize>( + d: D, + ) -> Result<[u8; N], D::Error> { + let bytes = serde_bytes::ByteBuf::deserialize(d)?.into_vec(); + let len = bytes.len(); + bytes + .try_into() + .map_err(|_| serde::de::Error::custom(format!("expected {N} bytes, got {len}"))) + } +} + +// --------------------------------------------------------------------------- +// Serializer +// --------------------------------------------------------------------------- + +struct ValueSerializer; + +fn int(i: i128) -> Result { + if i >= 0 { + u64::try_from(i) + .map(Value::Unsigned) + .map_err(|_| WireError::Message(format!("{i} does not fit a CBOR integer"))) + } else { + u64::try_from(-1 - i) + .map(Value::Negative) + .map_err(|_| WireError::Message(format!("{i} does not fit a CBOR integer"))) + } +} + +fn variant(name: &str, value: Value) -> Value { + Value::Map(vec![(Value::text(name), value)]) +} + +impl ser::Serializer for ValueSerializer { + type Ok = Value; + type Error = WireError; + type SerializeSeq = SeqSerializer; + type SerializeTuple = SeqSerializer; + type SerializeTupleStruct = SeqSerializer; + type SerializeTupleVariant = VariantSeqSerializer; + type SerializeMap = MapSerializer; + type SerializeStruct = MapSerializer; + type SerializeStructVariant = VariantMapSerializer; + + fn serialize_bool(self, v: bool) -> Result { + Ok(Value::Bool(v)) + } + fn serialize_i8(self, v: i8) -> Result { + int(v.into()) + } + fn serialize_i16(self, v: i16) -> Result { + int(v.into()) + } + fn serialize_i32(self, v: i32) -> Result { + int(v.into()) + } + fn serialize_i64(self, v: i64) -> Result { + int(v.into()) + } + fn serialize_i128(self, v: i128) -> Result { + int(v) + } + fn serialize_u8(self, v: u8) -> Result { + Ok(Value::Unsigned(v.into())) + } + fn serialize_u16(self, v: u16) -> Result { + Ok(Value::Unsigned(v.into())) + } + fn serialize_u32(self, v: u32) -> Result { + Ok(Value::Unsigned(v.into())) + } + fn serialize_u64(self, v: u64) -> Result { + Ok(Value::Unsigned(v)) + } + fn serialize_u128(self, v: u128) -> Result { + u64::try_from(v) + .map(Value::Unsigned) + .map_err(|_| WireError::Message(format!("{v} does not fit a CBOR integer"))) + } + fn serialize_f32(self, _: f32) -> Result { + Err(CanonicalError::FloatForbidden.into()) + } + fn serialize_f64(self, _: f64) -> Result { + Err(CanonicalError::FloatForbidden.into()) + } + fn serialize_char(self, v: char) -> Result { + Ok(Value::Text(v.to_string())) + } + fn serialize_str(self, v: &str) -> Result { + Ok(Value::text(v)) + } + fn serialize_bytes(self, v: &[u8]) -> Result { + Ok(Value::Bytes(v.to_vec())) + } + fn serialize_none(self) -> Result { + Ok(Value::Null) + } + fn serialize_some(self, value: &T) -> Result { + value.serialize(self) + } + fn serialize_unit(self) -> Result { + Ok(Value::Null) + } + fn serialize_unit_struct(self, _: &'static str) -> Result { + Ok(Value::Null) + } + fn serialize_unit_variant( + self, + _: &'static str, + _: u32, + variant: &'static str, + ) -> Result { + Ok(Value::text(variant)) + } + fn serialize_newtype_struct( + self, + _: &'static str, + value: &T, + ) -> Result { + value.serialize(self) + } + fn serialize_newtype_variant( + self, + _: &'static str, + _: u32, + name: &'static str, + value: &T, + ) -> Result { + Ok(variant(name, value.serialize(ValueSerializer)?)) + } + fn serialize_seq(self, len: Option) -> Result { + Ok(SeqSerializer(Vec::with_capacity(len.unwrap_or(0)))) + } + fn serialize_tuple(self, len: usize) -> Result { + Ok(SeqSerializer(Vec::with_capacity(len))) + } + fn serialize_tuple_struct( + self, + _: &'static str, + len: usize, + ) -> Result { + Ok(SeqSerializer(Vec::with_capacity(len))) + } + fn serialize_tuple_variant( + self, + _: &'static str, + _: u32, + name: &'static str, + len: usize, + ) -> Result { + Ok(VariantSeqSerializer(name, Vec::with_capacity(len))) + } + fn serialize_map(self, _: Option) -> Result { + Ok(MapSerializer::default()) + } + fn serialize_struct(self, _: &'static str, _: usize) -> Result { + Ok(MapSerializer::default()) + } + fn serialize_struct_variant( + self, + _: &'static str, + _: u32, + name: &'static str, + _: usize, + ) -> Result { + Ok(VariantMapSerializer(name, Vec::new())) + } +} + +struct SeqSerializer(Vec); + +impl ser::SerializeSeq for SeqSerializer { + type Ok = Value; + type Error = WireError; + fn serialize_element(&mut self, value: &T) -> Result<(), WireError> { + self.0.push(value.serialize(ValueSerializer)?); + Ok(()) + } + fn end(self) -> Result { + Ok(Value::Array(self.0)) + } +} + +impl ser::SerializeTuple for SeqSerializer { + type Ok = Value; + type Error = WireError; + fn serialize_element(&mut self, value: &T) -> Result<(), WireError> { + ser::SerializeSeq::serialize_element(self, value) + } + fn end(self) -> Result { + ser::SerializeSeq::end(self) + } +} + +impl ser::SerializeTupleStruct for SeqSerializer { + type Ok = Value; + type Error = WireError; + fn serialize_field(&mut self, value: &T) -> Result<(), WireError> { + ser::SerializeSeq::serialize_element(self, value) + } + fn end(self) -> Result { + ser::SerializeSeq::end(self) + } +} + +struct VariantSeqSerializer(&'static str, Vec); + +impl ser::SerializeTupleVariant for VariantSeqSerializer { + type Ok = Value; + type Error = WireError; + fn serialize_field(&mut self, value: &T) -> Result<(), WireError> { + self.1.push(value.serialize(ValueSerializer)?); + Ok(()) + } + fn end(self) -> Result { + Ok(variant(self.0, Value::Array(self.1))) + } +} + +#[derive(Default)] +struct MapSerializer { + entries: Vec<(Value, Value)>, + key: Option, +} + +impl ser::SerializeMap for MapSerializer { + type Ok = Value; + type Error = WireError; + fn serialize_key(&mut self, key: &T) -> Result<(), WireError> { + self.key = Some(key.serialize(ValueSerializer)?); + Ok(()) + } + fn serialize_value(&mut self, value: &T) -> Result<(), WireError> { + let key = self + .key + .take() + .ok_or_else(|| WireError::Message("a map value with no key".into()))?; + self.entries.push((key, value.serialize(ValueSerializer)?)); + Ok(()) + } + fn end(self) -> Result { + Ok(Value::Map(self.entries)) + } +} + +impl ser::SerializeStruct for MapSerializer { + type Ok = Value; + type Error = WireError; + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), WireError> { + self.entries + .push((Value::text(key), value.serialize(ValueSerializer)?)); + Ok(()) + } + fn end(self) -> Result { + Ok(Value::Map(self.entries)) + } +} + +struct VariantMapSerializer(&'static str, Vec<(Value, Value)>); + +impl ser::SerializeStructVariant for VariantMapSerializer { + type Ok = Value; + type Error = WireError; + fn serialize_field( + &mut self, + key: &'static str, + value: &T, + ) -> Result<(), WireError> { + self.1 + .push((Value::text(key), value.serialize(ValueSerializer)?)); + Ok(()) + } + fn end(self) -> Result { + Ok(variant(self.0, Value::Map(self.1))) + } +} + +// --------------------------------------------------------------------------- +// Deserializer +// --------------------------------------------------------------------------- + +impl<'de> IntoDeserializer<'de, WireError> for Value { + type Deserializer = Value; + fn into_deserializer(self) -> Value { + self + } +} + +impl<'de> de::Deserializer<'de> for Value { + type Error = WireError; + + fn deserialize_any>(self, visitor: V) -> Result { + match self { + Value::Unsigned(n) => visitor.visit_u64(n), + Value::Negative(n) => match i64::try_from(n) { + Ok(n) => visitor.visit_i64(-1 - n), + Err(_) => visitor.visit_i128(-1 - i128::from(n)), + }, + Value::Bytes(b) => visitor.visit_byte_buf(b), + Value::Text(t) => visitor.visit_string(t), + Value::Array(items) => { + let len = items.len(); + let mut seq = de::value::SeqDeserializer::new(items.into_iter()); + let v = visitor.visit_seq(&mut seq)?; + seq.end().map_err(|_| { + WireError::Message(format!("an array of {len} items had items left over")) + })?; + Ok(v) + } + Value::Map(entries) => { + let mut map = de::value::MapDeserializer::new(entries.into_iter()); + let v = visitor.visit_map(&mut map)?; + map.end()?; + Ok(v) + } + Value::Bool(b) => visitor.visit_bool(b), + Value::Null => visitor.visit_unit(), + } + } + + fn deserialize_option>(self, visitor: V) -> Result { + match self { + Value::Null => visitor.visit_none(), + other => visitor.visit_some(other), + } + } + + fn deserialize_newtype_struct>( + self, + _: &'static str, + visitor: V, + ) -> Result { + visitor.visit_newtype_struct(self) + } + + fn deserialize_enum>( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result { + match self { + Value::Text(name) => visitor.visit_enum(name.into_deserializer()), + Value::Map(mut entries) if entries.len() == 1 => { + let (k, v) = entries.pop().expect("one entry"); + let Value::Text(name) = k else { + return Err(WireError::Message( + "an enum variant must be named by text".into(), + )); + }; + visitor.visit_enum(EnumAccess { name, value: v }) + } + _ => Err(WireError::Message( + "an enum is a variant name, or a map from one variant name to its value".into(), + )), + } + } + + serde::forward_to_deserialize_any! { + bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string + bytes byte_buf unit unit_struct seq tuple tuple_struct map struct + identifier ignored_any + } +} + +struct EnumAccess { + name: String, + value: Value, +} + +impl<'de> de::EnumAccess<'de> for EnumAccess { + type Error = WireError; + type Variant = VariantAccess; + fn variant_seed>( + self, + seed: V, + ) -> Result<(V::Value, VariantAccess), WireError> { + let name = seed.deserialize(Value::Text(self.name))?; + Ok((name, VariantAccess(self.value))) + } +} + +struct VariantAccess(Value); + +impl<'de> de::VariantAccess<'de> for VariantAccess { + type Error = WireError; + fn unit_variant(self) -> Result<(), WireError> { + match self.0 { + Value::Null => Ok(()), + _ => Err(WireError::Message("a unit variant carries no value".into())), + } + } + fn newtype_variant_seed>( + self, + seed: T, + ) -> Result { + seed.deserialize(self.0) + } + fn tuple_variant>(self, _: usize, visitor: V) -> Result { + de::Deserializer::deserialize_any(self.0, visitor) + } + fn struct_variant>( + self, + _: &'static [&'static str], + visitor: V, + ) -> Result { + de::Deserializer::deserialize_any(self.0, visitor) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + enum Kind { + Plain, + Wrapped(u8), + Pair(u8, i32), + Shaped { n: u64 }, + } + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + #[serde(tag = "t")] + enum Tagged { + #[serde(rename = "a")] + A { x: u8 }, + #[serde(rename = "b")] + B { y: String }, + } + + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Everything { + zed: u64, + neg: i64, + text: String, + #[serde(with = "serde_bytes")] + bytes: Vec, + list: Vec, + maybe: Option, + none: Option, + flag: bool, + tagged: Tagged, + } + + fn sample() -> Everything { + Everything { + zed: 7, + neg: -300, + text: "ünïcode".into(), + bytes: vec![0, 1, 0xff], + list: vec![ + Kind::Plain, + Kind::Wrapped(3), + Kind::Pair(1, -1), + Kind::Shaped { n: 9 }, + ], + maybe: Some(5), + none: None, + flag: true, + tagged: Tagged::B { y: "why".into() }, + } + } + + #[test] + fn a_type_round_trips_through_its_canonical_encoding() { + let bytes = to_vec(&sample()).unwrap(); + assert_eq!( + canonical::encode(&canonical::decode(&bytes).unwrap()).unwrap(), + bytes + ); + assert_eq!(from_slice::(&bytes).unwrap(), sample()); + } + + #[test] + fn struct_fields_land_in_canonical_key_order() { + #[derive(Serialize)] + struct S { + zz: u8, + a: u8, + } + // Declaration order is zz, a; canonical order is a ("a" is shorter). + assert_eq!( + to_vec(&S { zz: 1, a: 2 }).unwrap(), + vec![0xa2, 0x61, b'a', 0x02, 0x62, b'z', b'z', 0x01] + ); + } + + #[test] + fn floats_are_refused_and_non_canonical_input_never_decodes() { + assert_eq!( + to_vec(&1.5f64), + Err(WireError::Canonical(CanonicalError::FloatForbidden)) + ); + // u8 1 written with a one-byte argument. + assert_eq!( + from_slice::(&[0x18, 0x01]), + Err(WireError::Canonical(CanonicalError::NonShortestInteger)) + ); + } + + #[test] + fn a_fixed_byte_array_is_an_array_unless_marked_as_bytes() { + assert_eq!( + to_value(&[1u8, 2]).unwrap(), + Value::Array(vec![Value::Unsigned(1), Value::Unsigned(2)]) + ); + assert_eq!( + to_value(serde_bytes::Bytes::new(&[1, 2])).unwrap(), + Value::Bytes(vec![1, 2]) + ); + } + + /// `sample()`'s canonical value with one member replaced. + fn sample_with(member: &str, value: Value) -> Vec { + let Value::Map(mut entries) = to_value(&sample()).unwrap() else { + unreachable!("a struct is a map") + }; + for (k, v) in &mut entries { + if *k == Value::text(member) { + *v = value.clone(); + } + } + canonical::encode(&Value::Map(entries)).unwrap() + } + + #[test] + fn a_member_in_a_form_other_than_its_own_is_refused() { + // Each stand-in is canonical CBOR that serde would accept for the + // member, so each would give `sample()` a second encoding. + let stand_ins = [ + // An array of small integers where a byte string belongs. + ( + "bytes", + Value::Array(vec![ + Value::Unsigned(0), + Value::Unsigned(1), + Value::Unsigned(0xff), + ]), + ), + // A byte string where text belongs. + ("text", Value::Bytes("ünïcode".as_bytes().to_vec())), + // A struct written as an array of its fields. + ( + "tagged", + Value::Array(vec![Value::text("b"), Value::text("why")]), + ), + ]; + for (member, value) in stand_ins { + assert_eq!( + from_slice::(&sample_with(member, value)), + Err(WireError::NotItsEncoding), + "{member}" + ); + } + // The object's own encoding still decodes. + let own = sample_with("text", Value::text("ünïcode")); + assert_eq!(from_slice::(&own).unwrap(), sample()); + } + + #[test] + fn a_member_written_where_it_is_omitted_is_refused() { + #[derive(Debug, PartialEq, Serialize, Deserialize)] + struct Sparse { + a: u8, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + extra: Vec, + } + let written = canonical::encode(&Value::map([ + ("a", Value::Unsigned(1)), + ("extra", Value::Array(vec![])), + ])) + .unwrap(); + assert_eq!( + from_slice::(&written), + Err(WireError::NotItsEncoding) + ); + let omitted = to_vec(&Sparse { + a: 1, + extra: vec![], + }) + .unwrap(); + assert_eq!( + from_slice::(&omitted).unwrap(), + Sparse { + a: 1, + extra: vec![] + } + ); + } +} diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/cargo_bridge.rs b/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/cargo_bridge.rs index 9a37fcb1c..4de84720d 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/cargo_bridge.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/cargo_bridge.rs @@ -1,22 +1,24 @@ //! Smart Byte cargo bridge. //! -//! Wraps a [`CrdtDocument`] (full snapshot) or a batch of [`Op`]s -//! (delta) as a `Cargo::Custom` payload. The SAID over the resulting -//! envelope is stable: identical document state → identical CBOR → -//! identical SAID. +//! Wraps a [`CrdtDocument`] (full snapshot) or a batch of [`Op`]s (delta) as +//! registered cargo — [`CRDT_SNAPSHOT_CARGO_TYPE`] or [`CRDT_DELTA_CARGO_TYPE`] +//! — whose bytes are the payload's canonical CBOR. Identical document state +//! gives identical bytes on any replica in any language, and so the same byte +//! identity. use serde::{Deserialize, Serialize}; -use smart_byte_core::Cargo; +use smart_byte_core::cargo::types; +use smart_byte_core::{Cargo, wire}; use crate::document::CrdtDocument; -use crate::error::Result; +use crate::error::{CrdtError, Result}; use crate::ops::Op; -/// The canonical URI for full CRDT snapshots wrapped in `Cargo::Custom`. -pub const CRDT_SNAPSHOT_TYPE_URI: &str = "urn:smart-byte:cargo:crdt:v1"; +/// The registered cargo type of a full CRDT snapshot. +pub const CRDT_SNAPSHOT_CARGO_TYPE: u64 = types::CRDT_SNAPSHOT; -/// The canonical URI for CRDT op-batches wrapped in `Cargo::Custom`. -pub const CRDT_DELTA_TYPE_URI: &str = "urn:smart-byte:cargo:crdt-delta:v1"; +/// The registered cargo type of a CRDT op batch. +pub const CRDT_DELTA_CARGO_TYPE: u64 = types::CRDT_DELTA; /// Envelope-ready encoding of a CRDT payload. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -29,47 +31,45 @@ pub enum CrdtCargoPayload { impl CrdtCargoPayload { /// Encode as canonical CBOR. pub fn to_cbor(&self) -> Result> { - Ok(serde_cbor::to_vec(self)?) + Ok(wire::to_vec(self)?) } - /// Decode from canonical CBOR. + /// Decode from canonical CBOR, refusing any other encoding. pub fn from_cbor(bytes: &[u8]) -> Result { - Ok(serde_cbor::from_slice(bytes)?) + Ok(wire::from_slice(bytes)?) } } -/// Wrap a full snapshot as Smart Byte `Cargo::Custom`. +/// Wrap a full snapshot as cargo. pub fn snapshot_to_cargo(doc: &CrdtDocument) -> Result { let payload = CrdtCargoPayload::Snapshot(doc.clone()); - Ok(Cargo::Custom { - type_uri: CRDT_SNAPSHOT_TYPE_URI.into(), - body: payload.to_cbor()?, - }) + Ok(Cargo::new(CRDT_SNAPSHOT_CARGO_TYPE, payload.to_cbor()?)) } -/// Wrap a delta (batch of ops) as Smart Byte `Cargo::Custom`. +/// Wrap a delta (batch of ops) as cargo. pub fn delta_to_cargo(ops: &[Op]) -> Result { let payload = CrdtCargoPayload::Delta(ops.to_vec()); - Ok(Cargo::Custom { - type_uri: CRDT_DELTA_TYPE_URI.into(), - body: payload.to_cbor()?, - }) + Ok(Cargo::new(CRDT_DELTA_CARGO_TYPE, payload.to_cbor()?)) } -/// Decode a Smart Byte cargo back to a CRDT payload, validating the -/// URI matches one of the canonical strings. +/// Decode cargo back to a CRDT payload. The cargo's type must be one of the +/// two CRDT types, and the payload must be the kind that type names. pub fn cargo_to_payload(cargo: &Cargo) -> Result { - match cargo { - Cargo::Custom { type_uri, body } => { - if type_uri != CRDT_SNAPSHOT_TYPE_URI && type_uri != CRDT_DELTA_TYPE_URI { - return Err(crate::error::CrdtError::OpIntegrity(format!( - "unrecognised cargo type_uri: {type_uri}" - ))); - } - CrdtCargoPayload::from_cbor(body) + let payload = match cargo.type_tag { + CRDT_SNAPSHOT_CARGO_TYPE | CRDT_DELTA_CARGO_TYPE => CrdtCargoPayload::from_cbor(&cargo.bytes)?, + other => { + return Err(CrdtError::OpIntegrity(format!( + "cargo type {other} ({}) is not a CRDT payload", + cargo.kind() + ))); } - other => Err(crate::error::CrdtError::OpIntegrity(format!( - "expected Cargo::Custom, got {}", - other.kind() + }; + match (&payload, cargo.type_tag) { + (CrdtCargoPayload::Snapshot(_), CRDT_SNAPSHOT_CARGO_TYPE) + | (CrdtCargoPayload::Delta(_), CRDT_DELTA_CARGO_TYPE) => Ok(payload), + _ => Err(CrdtError::OpIntegrity(format!( + "a {} payload under cargo type {}", + if matches!(payload, CrdtCargoPayload::Snapshot(_)) { "snapshot" } else { "delta" }, + cargo.kind() ))), } } @@ -79,8 +79,7 @@ mod tests { use super::*; use crate::document::{CrdtDocument, DocumentId}; use crate::hlc::{HlcClock, ReplicaId}; - use chrono::TimeZone; - use smart_byte_core::{Envelope, JouleCost, OwnershipChain, Provenance, Said}; + use smart_byte_core::{Aid, Envelope, JouleCost, Said, Signer, SigningKey}; fn fixture_doc() -> CrdtDocument { let id = DocumentId::from_bytes(b"d"); @@ -103,29 +102,26 @@ mod tests { } #[test] - fn envelope_said_is_stable() { + fn envelope_identity_is_stable() { let doc = fixture_doc(); - let cargo = snapshot_to_cargo(&doc).unwrap(); - let prov = Provenance::new( - Said::hash(b"issuer"), - chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap(), - vec![], + let issuer = Signer::single( + Aid::of_inception(Said::hash(b"issuer")), + 0, + SigningKey::from_bytes(&[0x55; 32]), ); - let env1 = Envelope::new( - prov.clone(), - OwnershipChain::empty(), - cargo.clone(), - JouleCost::default(), - ) - .unwrap(); - let env2 = Envelope::new( - prov, - OwnershipChain::empty(), - cargo, - JouleCost::default(), - ) - .unwrap(); - assert_eq!(env1.id, env2.id); - env1.verify_said().unwrap(); + let to = Aid::of_inception(Said::hash(b"replica")); + let issue = || { + Envelope::issue(&issuer, to, [9; 16], snapshot_to_cargo(&doc).unwrap(), JouleCost::default()) + }; + assert_eq!(issue().identity(), issue().identity()); + assert_eq!(Envelope::from_cbor(&issue().to_cbor()).unwrap(), issue()); + } + + #[test] + fn a_payload_under_the_other_crdt_type_is_refused() { + let doc = fixture_doc(); + let mut cargo = snapshot_to_cargo(&doc).unwrap(); + cargo.type_tag = CRDT_DELTA_CARGO_TYPE; + assert!(cargo_to_payload(&cargo).is_err()); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/error.rs b/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/error.rs index 93ed36040..54c6e577e 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/error.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/error.rs @@ -51,6 +51,12 @@ impl From for CrdtError { } } +impl From for CrdtError { + fn from(e: smart_byte_core::wire::WireError) -> Self { + CrdtError::Cbor(e.to_string()) + } +} + impl From for CrdtError { fn from(e: serde_json::Error) -> Self { CrdtError::Json(e.to_string()) diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/ops.rs b/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/ops.rs index 3f7c84c67..35acd4589 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/ops.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-crdt/src/ops.rs @@ -130,7 +130,9 @@ impl Op { pub fn compute_id(&self) -> Result { let mut tmp = self.clone(); tmp.id = OpId::default(); - let bytes = serde_cbor::to_vec(&tmp)?; + // Canonical CBOR, so every replica in every language derives the + // same id for the same op. + let bytes = smart_byte_core::wire::to_vec(&tmp)?; let h = blake3::hash(&bytes); Ok(OpId(*h.as_bytes())) } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/Cargo.toml b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/Cargo.toml index 34a3972d6..9d1e357dd 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/Cargo.toml +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/Cargo.toml @@ -9,17 +9,12 @@ description = "Smart Byte KERI key-event log: full inception/rotation/interactio [dependencies] smart-byte-core = { workspace = true } -smart-byte-pq = { workspace = true } serde = { workspace = true } serde_bytes = { workspace = true } -serde_cbor = { workspace = true } -blake3 = { workspace = true } ed25519-dalek = { workspace = true } rand = { workspace = true } chrono = { workspace = true } -uuid = { workspace = true } thiserror = { workspace = true } -tracing = "0.1" tokio = { workspace = true, features = ["fs"] } async-trait = "0.1" dashmap = "6" diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/controller.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/controller.rs index 145f82d64..5050b5e56 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/controller.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/controller.rs @@ -12,9 +12,9 @@ //! * [`Controller::recover`] — catastrophic-key-loss reset. //! //! Signing keys are wrapped in [`KeyPair`], a thin newtype around -//! `ed25519_dalek::SigningKey` that also exposes the `smart_byte_pq` -//! signer trait so the substrate can transition to post-quantum or -//! hybrid keys without a controller-API break. +//! `ed25519_dalek::SigningKey`. Every key carries a one-byte algorithm +//! identifier in events, which leaves room for post-quantum keys without a +//! change to the event format. use std::collections::BTreeSet; @@ -22,23 +22,21 @@ use ed25519_dalek::{Signer as EdSignerTrait, SigningKey, VerifyingKey}; use smart_byte_core::Said; use crate::error::{KeriError, Result}; +use crate::verifier::check_establishment; use crate::events::{ - Anchor, ControllerAid, DelegationEvent, EventType, InceptionEvent, InteractionEvent, - KeyEvent, PublicKey, RecoveryEvent, RotationEvent, Threshold, WitnessAid, finalize_event, + Anchor, ControllerAid, DelegationEvent, InceptionEvent, InteractionEvent, KeyEvent, PublicKey, + RecoveryEvent, RotationEvent, Threshold, WitnessAid, finalize_event, finalize_inception, per_key_said, }; use crate::VERSION_STRING; -/// Algorithm byte used for Ed25519 signing keys (mirrors -/// `smart_byte_pq::SignatureAlgorithm::Ed25519`). +/// Algorithm byte used for Ed25519 signing keys (treatise §8.3). const ED25519_ALG: u8 = 0x01; /// A signing key plus its public counterpart. /// -/// The post-quantum migration story is handled by the wider -/// `smart_byte_pq` crate; this newtype intentionally exposes only -/// Ed25519 today and will grow algorithm variants when callers need -/// them for rotations. +/// Ed25519 only: the algorithm identifier in front of every key leaves +/// room for other schemes. #[derive(Clone)] pub struct KeyPair { /// Ed25519 signing key. @@ -120,45 +118,31 @@ impl Controller { witnesses: Vec, witness_threshold: Threshold, ) -> Result<(Self, InceptionEvent)> { - if initial_keys.is_empty() { - return Err(KeriError::Malformed("inception requires >= 1 key".into())); - } - if (threshold.0 as usize) > initial_keys.len() { - return Err(KeriError::Malformed( - "threshold exceeds initial key count".into(), - )); - } let k: Vec = initial_keys.iter().map(KeyPair::public_key).collect(); let n: Vec = next_keys .iter() .map(|kp| per_key_said(&kp.public_key())) .collect::>()?; + check_establishment(&k, threshold, &n, next_threshold)?; let witness_set: BTreeSet = witnesses.iter().cloned().collect(); - let placeholder_aid = ControllerAid(String::new()); - let placeholder_said = Said::default(); - let raw = InceptionEvent { + // `d` and `i` are placeholdered while the event is hashed, then + // stamped: `i` is the AID the event's own SAID names (spec §3.1). + let finalized = finalize_inception(InceptionEvent { v: VERSION_STRING.to_string(), - t: EventType::Inception, - d: placeholder_said, - i: placeholder_aid, + d: Said::default(), + i: ControllerAid::default(), s: 0, + p: String::new(), kt: threshold, k, nt: next_threshold, n, - c: Vec::new(), + a: Vec::new(), b: witnesses.clone(), bt: witness_threshold, - a: Vec::new(), - }; - // First pass: SAID with d zeroed and i empty - let bytes = serde_cbor::to_vec(&KeyEvent::Inception(raw.clone()))?; - let said = Said::hash(&bytes); - let aid = ControllerAid::from_inception_said(&said); - let mut finalized = raw; - finalized.d = said; - finalized.i = aid.clone(); + })?; + let aid = finalized.i; let controller = Self { aid, @@ -197,6 +181,10 @@ impl Controller { .iter() .map(|kp| per_key_said(&kp.public_key())) .collect::>()?; + if revealed.is_empty() { + return Err(KeriError::RotationNotPermitted); + } + check_establishment(&revealed, self.next_threshold, &new_n, new_next_threshold)?; // Update witness set per br/ba. for w in &remove_witnesses { @@ -209,19 +197,18 @@ impl Controller { let event = RotationEvent { v: VERSION_STRING.to_string(), - t: EventType::Rotation, d: Said::default(), - i: self.aid.clone(), + i: self.aid, s: next_sequence, p: prior_said, kt: self.next_threshold, k: revealed, nt: new_next_threshold, n: new_n, + a: Vec::new(), br: remove_witnesses, ba: add_witnesses, bt: new_witness_threshold, - a: Vec::new(), }; let finalized = finalize_event(KeyEvent::Rotation(event))?; // After commit: previous next_keys are now current; new next set is committed. @@ -244,13 +231,21 @@ impl Controller { .last() .ok_or_else(|| KeriError::Malformed("cannot interact on empty log".into()))? .clone(); + // The key fields are carried forward verbatim (spec §4 step 5). let event = InteractionEvent { v: VERSION_STRING.to_string(), - t: EventType::Interaction, d: Said::default(), - i: self.aid.clone(), + i: self.aid, s: prior.sequence() + 1, p: prior.said(), + kt: self.threshold, + k: self.current_keys.iter().map(KeyPair::public_key).collect(), + nt: self.next_threshold, + n: self + .next_keys + .iter() + .map(|kp| per_key_said(&kp.public_key())) + .collect::>()?, a: anchors, }; let finalized = finalize_event(KeyEvent::Interaction(event))?; @@ -281,20 +276,22 @@ impl Controller { .last() .ok_or_else(|| KeriError::Malformed("cannot recover on empty log".into()))? .clone(); + let k: Vec = new_keys.iter().map(KeyPair::public_key).collect(); + let n: Vec = new_next_keys + .iter() + .map(|kp| per_key_said(&kp.public_key())) + .collect::>()?; + check_establishment(&k, new_threshold, &n, new_next_threshold)?; let event = RecoveryEvent { v: VERSION_STRING.to_string(), - t: EventType::Recovery, d: Said::default(), - i: self.aid.clone(), + i: self.aid, s: prior.sequence() + 1, p: prior.said(), kt: new_threshold, - k: new_keys.iter().map(KeyPair::public_key).collect(), + k, nt: new_next_threshold, - n: new_next_keys - .iter() - .map(|kp| per_key_said(&kp.public_key())) - .collect::>()?, + n, a: anchors, }; let finalized = finalize_event(KeyEvent::Recovery(event))?; @@ -321,33 +318,28 @@ impl Controller { threshold: Threshold, next_threshold: Threshold, ) -> Result<(Self, DelegationEvent)> { - if initial_keys.is_empty() { - return Err(KeriError::Malformed("delegation requires >= 1 key".into())); - } let k: Vec = initial_keys.iter().map(KeyPair::public_key).collect(); let n: Vec = next_keys .iter() .map(|kp| per_key_said(&kp.public_key())) .collect::>()?; - let raw = DelegationEvent { + check_establishment(&k, threshold, &n, next_threshold)?; + let finalized = match finalize_event(KeyEvent::Delegation(DelegationEvent { v: VERSION_STRING.to_string(), - t: EventType::Delegation, d: Said::default(), - i: ControllerAid(String::new()), + i: ControllerAid::default(), s: 0, - p: Said::default(), - di: parent_aid.clone(), + p: String::new(), + di: parent_aid, kt: threshold, k, nt: next_threshold, n, + }))? { + KeyEvent::Delegation(e) => e, + _ => unreachable!("finalize_event keeps the event type"), }; - let bytes = serde_cbor::to_vec(&KeyEvent::Delegation(raw.clone()))?; - let said = Said::hash(&bytes); - let aid = ControllerAid::from_inception_said(&said); - let mut finalized = raw; - finalized.d = said; - finalized.i = aid.clone(); + let aid = finalized.i; let controller = Self { aid, current_keys: initial_keys, @@ -357,21 +349,50 @@ impl Controller { witnesses: BTreeSet::new(), witness_threshold: Threshold(0), key_event_log: vec![KeyEvent::Delegation(finalized.clone())], - delegated_by: Some(parent_aid), + delegated_by: Some(finalized.di), }; Ok((controller, finalized)) } - /// Sign an event with the current signing key set, returning a - /// vector of `(key_index, signature)` tuples sized to the - /// threshold. For an inception event the keys revealed *in* the - /// event itself are the signers; the caller passes those in via - /// `self.current_keys`. - pub fn sign_event(&self, event: &KeyEvent) -> Result> { + /// The sequence number of the last establishment event: the one whose + /// keys are current. + #[must_use] + pub fn establishment(&self) -> u64 { + self.key_event_log + .iter() + .rev() + .find(|e| e.event_type().is_establishment()) + .map_or(0, KeyEvent::sequence) + } + + /// A signer for envelopes and other controller-signed messages: the + /// current keys, under the establishment event that put them in force. + #[must_use] + pub fn signer(&self) -> smart_byte_core::Signer { + smart_byte_core::Signer { + aid: self.aid, + establishment: self.establishment(), + keys: self + .current_keys + .iter() + .enumerate() + .map(|(i, kp)| (i as u64, kp.signing.clone())) + .collect(), + } + } + + /// Sign an event with the current signing key set: one `(key index, + /// signature)` pair per key, each signature carrying its key's algorithm + /// identifier in front (`0x01` + 64 bytes). An inception is signed by the + /// keys it reveals; a rotation by the keys it reveals, which are current + /// once [`Controller::rotate`] returns (spec §3.5). + pub fn sign_event(&self, event: &KeyEvent) -> Result)>> { let body = event.to_cbor()?; let mut out = Vec::with_capacity(self.current_keys.len()); for (i, kp) in self.current_keys.iter().enumerate() { - let sig = kp.sign(&body); + let mut sig = Vec::with_capacity(65); + sig.push(ED25519_ALG); + sig.extend_from_slice(&kp.sign(&body).to_bytes()); out.push((i as u32, sig)); } Ok(out) diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/error.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/error.rs index 58d897c32..5486056ca 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/error.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/error.rs @@ -1,6 +1,7 @@ //! Typed error surface for the KERI + witness layer. -use smart_byte_core::Said; +use smart_byte_core::wire::WireError; +use smart_byte_core::{ControllerSignatureError, Said}; use thiserror::Error; /// Result alias used across this crate. @@ -10,6 +11,11 @@ pub type Result = std::result::Result; /// verifier, storage, and OOBI subsystems. #[derive(Debug, Error)] pub enum KeriError { + /// The bytes are not canonical CBOR, or not the encoding of the object + /// they were decoded as. + #[error("not a well-formed event: {0}")] + Wire(#[from] WireError), + /// The event's recomputed SAID did not match its asserted `d`. #[error("said mismatch: event asserts {asserted} but content hashes to {computed}")] SaidMismatch { @@ -19,6 +25,15 @@ pub enum KeriError { computed: Said, }, + /// The event's `v` names a version this crate does not speak. + #[error("unsupported version {0:?}")] + UnsupportedVersion(String), + + /// The event's `t` names a type spec v1 does not define (`rec` is + /// reserved; `dip` is an extension). + #[error("event type {0:?} is not part of spec v1")] + UnsupportedEventType(String), + /// Sequence number does not follow the prior event. #[error("sequence error: expected {expected}, got {got}")] SequenceError { @@ -28,19 +43,20 @@ pub enum KeriError { got: u64, }, - /// The event's `p` (prior-event SAID) does not match the previous event's `d`. - #[error("prior-event mismatch: expected {expected}, got {got}")] + /// The event's `p` is not the previous event's `d` (or, for an + /// inception, not the empty string). + #[error("prior-event mismatch: expected {expected:?}, got {got:?}")] PriorMismatch { - /// Expected prior SAID. - expected: Said, - /// Prior SAID actually carried in the event. - got: Said, + /// Expected `p`. + expected: String, + /// `p` as written. + got: String, }, - /// The event's `i` (controller AID) does not match the inception event's AID. + /// The event's `i` is not the controller's AID. #[error("controller AID mismatch: expected {expected}, got {got}")] ControllerAidMismatch { - /// Expected AID derived from the inception event. + /// Expected AID. expected: String, /// AID actually carried in the event. got: String, @@ -53,22 +69,50 @@ pub enum KeriError { index: usize, }, - /// Threshold of valid signatures was not met. - #[error("signature threshold not met: have {have}, need {need}")] - ThresholdNotMet { - /// Number of valid signatures collected. - have: u32, - /// Threshold required. - need: u32, + /// A rotation after an establishment event that committed to no next + /// keys: the controller gave up rotating. + #[error( + "the previous establishment event committed to no next keys; only interactions may follow" + )] + RotationNotPermitted, + + /// An interaction whose key fields differ from those in force. + #[error("an interaction must carry the key fields forward unchanged")] + KeysChanged, + + /// A threshold outside what its key list allows. + #[error( + "{field} = {threshold} with {keys} keys: a threshold is at least 1 and at most the key count, and 0 exactly when there are no keys" + )] + InvalidThreshold { + /// `kt`, `nt` or `bt`. + field: &'static str, + /// The threshold as written. + threshold: u64, + /// How many keys (or witnesses) it counts. + keys: usize, }, + /// A key, a next-key digest or a witness listed twice. + #[error("{field}[{index}] repeats an earlier entry")] + DuplicateKey { + /// `k`, `n` or `b`. + field: &'static str, + /// Position of the repeat. + index: usize, + }, + + /// The event's signatures do not satisfy its key state. + #[error("event signatures: {0}")] + Signature(#[from] ControllerSignatureError), + /// Witness threshold of receipts was not met. #[error("witness threshold not met: have {have}, need {need}")] WitnessThresholdNotMet { /// Number of valid receipts collected. - have: u32, + have: u64, /// Receipt threshold required. - need: u32, + need: u64, }, /// A witness has already issued a receipt for a different event at this sequence. @@ -78,7 +122,7 @@ pub enum KeriError { sequence: u64, }, - /// The event references an unknown algorithm byte or malformed key encoding. + /// A malformed public key, or a witness identifier that spells no key. #[error("malformed key: {0}")] MalformedKey(String), @@ -90,22 +134,10 @@ pub enum KeriError { #[error("recovery rejected: {0}")] RecoveryRejected(String), - /// A delegation event references an unknown delegating parent. - #[error("delegation parent {0} not found")] - DelegationParentMissing(String), - - /// `rec` event encountered while v1 strict mode is engaged. - #[error("recovery (rec) events disabled by strict-spec policy")] - StrictSpecRejectsRec, - /// Generic structural malformation surfaced by the verifier or storage layer. #[error("malformed event: {0}")] Malformed(String), - /// CBOR encoding/decoding failure. - #[error("cbor error: {0}")] - Cbor(String), - /// Filesystem I/O failure from `FileStorage`. #[error("io error: {0}")] Io(String), @@ -115,9 +147,39 @@ pub enum KeriError { Storage(String), } -impl From for KeriError { - fn from(e: serde_cbor::Error) -> Self { - Self::Cbor(e.to_string()) +impl KeriError { + /// The error class as the conformance pack names it. + pub fn class(&self) -> &'static str { + match self { + Self::Wire(WireError::Canonical(e)) => e.class(), + Self::Wire(WireError::Message(_) | WireError::NotItsEncoding) => "malformed_event", + Self::SaidMismatch { .. } => "said_mismatch", + Self::UnsupportedVersion(_) => "unsupported_version", + Self::UnsupportedEventType(_) => "unsupported_event_type", + Self::SequenceError { .. } => "out_of_sequence", + Self::PriorMismatch { .. } => "prior_mismatch", + Self::ControllerAidMismatch { .. } => "controller_mismatch", + Self::PreRotationMismatch { .. } => "pre_rotation_mismatch", + Self::RotationNotPermitted => "rotation_not_permitted", + Self::KeysChanged => "interaction_changes_keys", + Self::InvalidThreshold { .. } => "invalid_threshold", + Self::DuplicateKey { .. } => "duplicate_key", + Self::Signature(e) => e.class(), + Self::WitnessThresholdNotMet { .. } => "witness_threshold_not_met", + Self::DuplicityRefused { .. } => "duplicity", + Self::MalformedKey(_) => "malformed_verifying_key", + Self::BadSignature => "signature_invalid", + Self::RecoveryRejected(_) => "recovery_rejected", + Self::Malformed(_) => "malformed_event", + Self::Io(_) => "io", + Self::Storage(_) => "storage", + } + } +} + +impl From for KeriError { + fn from(e: smart_byte_core::SaidCheckError) -> Self { + Self::Malformed(e.to_string()) } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/events.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/events.rs index c7f794f74..3ec1e4946 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/events.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/events.rs @@ -1,75 +1,74 @@ //! Key-event types as defined in //! `smart-byte/spec/identity_and_key_rotation.md` §3. //! -//! Each event is a CBOR map with the canonical sorted-key field layout -//! `v`/`t`/`d`/`i`/`s`/`p`/`kt`/`k`/`nt`/`n`/`a` (plus type-specific -//! extras like `b`/`bt`/`br`/`ba`/`c`/`di`). +//! Each event is a canonical CBOR map (identity spec §2.3) holding exactly the +//! §3.3 fields `a`, `d`, `i`, `k`, `kt`, `n`, `nt`, `p`, `s`, `t`, `v`. This +//! crate's extensions — a witness pool (`b`, `bt`, `br`, `ba`), delegated +//! inception (`dip`, with `di`) and recovery (`rec`) — are outside spec v1: +//! the pool's members are written only when they carry something, so an event +//! without them is exactly the spec's form, and a v1 decoder refuses an event +//! that uses any of them ([`KeyEvent::decode`]). //! -//! The spec stipulates that the SAID (`d`) is computed by: -//! -//! 1. Replacing `d` with a 53-character `#` placeholder. -//! 2. Canonically CBOR-encoding the event with sorted map keys. -//! 3. Taking BLAKE3-256 of the bytes. -//! 4. Storing the digest back in `d`. -//! -//! Because the `smart-byte-core::Said` primitive already wraps the -//! BLAKE3-256 digest, we implement the substitution + re-hash sequence -//! locally and expose [`compute_said`] for each event type via the -//! [`SaidedEvent`] trait. +//! An event's SAID is computed per identity spec §2: its `d` holds the 53-`#` +//! placeholder while it is hashed. An inception event's `i` — the controller +//! AID, which is derived from its own SAID — holds the placeholder too +//! (§3.1). Signatures are over the canonical encoding of the finished event. use serde::{Deserialize, Serialize}; -use smart_byte_core::Said; +use smart_byte_core::canonical::{self, Value}; +use smart_byte_core::said::{self, Derived, INCEPTION_AID, SAID_FIELD}; +use smart_byte_core::{Aid, Said, VerifyingKey, wire}; use crate::error::{KeriError, Result}; /// 53-character placeholder used during SAID derivation per spec §2.1. -pub const SAID_PLACEHOLDER: &str = - "#####################################################"; +pub const SAID_PLACEHOLDER: &str = said::SAID_PLACEHOLDER; /// String form of the version field carried in every event. pub const VERSION_STRING: &str = crate::VERSION_STRING; -/// Identifier of a controller (an Autonomic IDentifier, AID). -/// -/// Per spec §3.1, an AID is the SAID of the controller's inception -/// event with the prefix `B` substituted for the `E` of an arbitrary -/// SAID. We store the substituted ASCII form directly so that -/// equality and ordering match the wire encoding. -#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] -pub struct ControllerAid(pub String); +/// Identifier of a controller (an Autonomic IDentifier, AID): its +/// inception event's SAID spelled with `B` (identity spec §3.1). +pub type ControllerAid = Aid; -impl ControllerAid { - /// Build a controller AID from its inception event's SAID by - /// substituting `B` for the leading `E` prefix character. - #[must_use] - pub fn from_inception_said(said: &Said) -> Self { - let s = said.to_base32(); - Self(format!("B{s}")) - } - - /// Recover the underlying SAID by re-substituting `E` for the `B` - /// prefix. Returns `None` if the AID does not look like a controller - /// AID. - #[must_use] - pub fn as_inception_said(&self) -> Option { - let rest = self.0.strip_prefix('B')?; - Said::from_base32(rest).ok() - } -} - -/// Identifier of a witness peer. +/// Identifier of a witness peer: `W` followed by the lowercase unpadded +/// base32 of its Ed25519 public key. A witness is named by its key, so a +/// receipt is checked against the key its witness's name spells and no one +/// can receipt under another witness's name. #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct WitnessAid(pub String); +impl WitnessAid { + /// The identifier of the witness holding `key`. + #[must_use] + pub fn of_key(key: &VerifyingKey) -> Self { + Self(format!("W{}", data_encoding::BASE32_NOPAD.encode(key.as_bytes()).to_ascii_lowercase())) + } + + /// The public key this identifier spells, if it spells one. + #[must_use] + pub fn key(&self) -> Option<[u8; 32]> { + let body = self.0.strip_prefix('W')?; + if body.bytes().any(|b| b.is_ascii_uppercase()) { + return None; + } + let key: [u8; 32] = data_encoding::BASE32_NOPAD + .decode(body.to_ascii_uppercase().as_bytes()) + .ok()? + .try_into() + .ok()?; + // One key, one spelling: refuse non-zero trailing bits. + (Self::of_key(&VerifyingKey::from_bytes(&key).ok()?) == *self).then_some(key) + } +} + /// Identifier of a watcher (third-party duplicity detector). #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] pub struct WatcherAid(pub String); -/// Public-key encoding placed in the event's `k` field. -/// -/// Each entry is the raw public key bytes prefixed with the one-byte -/// algorithm identifier defined in `smart_byte_pq::SignatureAlgorithm` -/// (`0x01` = Ed25519, `0x10-0x12` = ML-DSA, etc.). +/// Public-key encoding placed in the event's `k` field: the one-byte +/// algorithm identifier followed by the key (identity spec §3.3), as a byte +/// string. `0x01` is Ed25519; `0x10`–`0x12` are reserved for ML-DSA. #[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct PublicKey(#[serde(with = "serde_bytes")] pub Vec); @@ -96,38 +95,30 @@ impl PublicKey { } } +impl From<&PublicKey> for smart_byte_core::PrefixedKey { + fn from(k: &PublicKey) -> Self { + smart_byte_core::PrefixedKey(k.0.clone()) + } +} + /// Unsigned integer threshold (KERI's weighted thresholds reserved for a /// later revision, per spec §3.3). #[derive( Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, )] -pub struct Threshold(pub u32); +pub struct Threshold(pub u64); -/// Anchor to off-log content carried in `ixn` events' `a` field. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct Anchor { - /// SAID of the anchored object (envelope, attestation, etc.). - pub d: Said, - /// Optional sequence or position used by the caller for ordering. - #[serde(skip_serializing_if = "Option::is_none")] - pub s: Option, - /// Optional caller-defined kind tag (e.g. "envelope", "vc-issue"). - #[serde(skip_serializing_if = "Option::is_none")] - pub kind: Option, +impl Threshold { + fn is_zero(&self) -> bool { + self.0 == 0 + } } -/// Optional inception-time configuration traits per spec §3.3 `c`. -#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub enum ConfigTrait { - /// The log will never accept further rotations (terminal commitment). - NoRotate, - /// The log accepts only events with witness receipts above threshold. - EstablishOnly, - /// Implementation-defined extension trait. - Custom(String), -} +/// An anchor in an `ixn` event's `a` field: the SAID of the anchored object +/// (identity spec §3.2). +pub type Anchor = Said; -/// Event-type discriminator written to the `t` field. +/// Event-type discriminator, as written to the `t` field. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum EventType { /// Inception (`icp`). @@ -142,75 +133,70 @@ pub enum EventType { /// Recovery (`rec`). #[serde(rename = "rec")] Recovery, - /// Delegation (`dip`/`drt` collapsed into a single discriminator). + /// Delegation (`dip`). #[serde(rename = "dip")] Delegation, } -/// Per-key SAID derivation per spec §3.4. -/// -/// Each entry in the previous event's `n` array is the SAID of a tiny -/// CBOR map `{"d": "", "k": ""}`. This helper builds -/// the placeholder map, canonically encodes it, hashes, and returns -/// the resulting SAID. -pub fn per_key_said(key: &PublicKey) -> Result { - #[derive(Serialize)] - struct PerKey<'a> { - d: &'a str, - k: &'a PublicKey, +impl EventType { + /// True if the event type is one that changes the signing key set. + #[must_use] + pub fn is_establishment(self) -> bool { + matches!( + self, + Self::Inception | Self::Rotation | Self::Recovery | Self::Delegation + ) } - let placeholder = PerKey { - d: SAID_PLACEHOLDER, - k: key, - }; - let bytes = serde_cbor::to_vec(&placeholder)?; - Ok(Said::hash(&bytes)) +} + +/// Per-key SAID derivation per spec §3.4: the SAID of +/// `{"d": , "k": }`. +pub fn per_key_said(key: &PublicKey) -> Result { + Ok(said::per_key_said(&key.0)) } // ----- Event structs -------------------------------------------------------- /// The first event in a controller's log; binds the AID to its initial -/// signing keys, the pre-rotated next keys, witnesses, and any -/// inception-time anchors. See spec §3.2 / §3.3. +/// signing keys and the pre-rotated next keys. See spec §3.2 / §3.3. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct InceptionEvent { - /// Version string (e.g. `SBYTE10JSON`). + /// Version string (`SBYTE10JSON`). pub v: String, - /// Event-type discriminator (always [`EventType::Inception`]). - pub t: EventType, /// SAID of this event. pub d: Said, - /// Controller AID (this event's SAID with `B` substituted for `E`). + /// Controller AID: this event's SAID spelled with `B`. pub i: ControllerAid, /// Sequence number (always 0). pub s: u64, + /// The prior event's SAID: the empty string, as an inception has none. + pub p: String, /// Signing-key threshold. pub kt: Threshold, /// Signing keys revealed at inception. pub k: Vec, /// Next-key threshold (committed forward). pub nt: Threshold, - /// SAIDs of pre-rotated next keys. + /// Per-key SAIDs of the pre-rotated next keys. pub n: Vec, - /// Inception-time configuration traits. - pub c: Vec, - /// Initial witness set. - pub b: Vec, - /// Witness-receipt threshold. - pub bt: Threshold, - /// Inception-time anchors. + /// Anchors: empty for an inception (spec §4 step 5). pub a: Vec, + /// Extension: initial witness set. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub b: Vec, + /// Extension: witness-receipt threshold. + #[serde(default, skip_serializing_if = "Threshold::is_zero")] + pub bt: Threshold, } -/// Rotation event: replaces the current signing keys with the keys -/// committed by the previous event's `n` array and commits a fresh -/// pre-rotation set. See spec §3.2 (`rot`). +/// Rotation event: reveals the keys the previous establishment event +/// committed to and commits a fresh pre-rotation set. See spec §3.2 (`rot`). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RotationEvent { /// Version string. pub v: String, - /// Event-type discriminator (always [`EventType::Rotation`]). - pub t: EventType, /// SAID of this event. pub d: Said, /// Controller AID. @@ -221,30 +207,32 @@ pub struct RotationEvent { pub p: Said, /// New signing-key threshold. pub kt: Threshold, - /// Revealed signing keys (must match previous `n` per spec §3.4). + /// Revealed signing keys (must match the previous `n` per spec §3.4). pub k: Vec, /// New next-key threshold. pub nt: Threshold, - /// SAIDs of new pre-rotated keys. + /// Per-key SAIDs of the new pre-rotated keys. pub n: Vec, - /// Witnesses removed at this rotation. - pub br: Vec, - /// Witnesses added at this rotation. - pub ba: Vec, - /// New witness-receipt threshold. - pub bt: Threshold, - /// Anchors carried in this rotation. + /// Anchors: empty for a rotation (spec §3.3). pub a: Vec, + /// Extension: witnesses removed at this rotation. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub br: Vec, + /// Extension: witnesses added at this rotation. + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ba: Vec, + /// Extension: new witness-receipt threshold. + #[serde(default, skip_serializing_if = "Threshold::is_zero")] + pub bt: Threshold, } -/// Interaction event: a signed anchor to off-log content with no key -/// change. See spec §3.2 (`ixn`). +/// Interaction event: a signed anchor to off-log content, with the key +/// fields carried forward unchanged (spec §3.2, §4 step 5). #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct InteractionEvent { /// Version string. pub v: String, - /// Event-type discriminator (always [`EventType::Interaction`]). - pub t: EventType, /// SAID of this event. pub d: Said, /// Controller AID. @@ -253,31 +241,32 @@ pub struct InteractionEvent { pub s: u64, /// Prior-event SAID. pub p: Said, - /// Anchors (must be non-empty in practice). + /// Signing-key threshold, as the previous event set it. + pub kt: Threshold, + /// Signing keys, as the previous event set them. + pub k: Vec, + /// Next-key threshold, as the previous event set it. + pub nt: Threshold, + /// Next-key commitments, as the previous event set them. + pub n: Vec, + /// SAIDs of the anchored objects. pub a: Vec, } -/// Recovery event: catastrophic-key-loss reset that re-establishes -/// signing key set and pre-rotation commitment when the controller has -/// lost the pre-images for the previous `n` field. +/// Recovery event: catastrophic-key-loss reset that re-establishes the +/// signing key set and pre-rotation commitment when the controller has lost +/// the pre-images for the previous `n` field. /// -/// The spec §3.2 reserves `rec`; this crate ingests the semantics: -/// -/// * a recovery event MUST be signed by an out-of-band recovery key -/// (controller-supplied at construction time); -/// * a recovery event resets the verifier's view of the controller's -/// signing keys to the new `k` field; -/// * post-recovery, the chain continues from the recovery event's `s`. -/// -/// Strict-spec verifiers may still reject `rec` events outright; -/// callers opt in by setting [`crate::verifier::LogVerifier`] to -/// non-strict mode. +/// Spec v1 reserves `rec`, and a v1 verifier rejects it; this crate's +/// recovery semantics are an extension, accepted only by a verifier that +/// opts in ([`crate::verifier::LogVerifier::strict`] set to `false`), and +/// only with the receipts of the log's witnesses: the keys it reveals were +/// never committed to, so nothing else speaks for the controller. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct RecoveryEvent { /// Version string. pub v: String, - /// Event-type discriminator (always [`EventType::Recovery`]). - pub t: EventType, /// SAID of this event. pub d: Said, /// Controller AID. @@ -292,32 +281,33 @@ pub struct RecoveryEvent { pub k: Vec, /// New next-key threshold. pub nt: Threshold, - /// SAIDs of fresh pre-rotated keys. + /// Per-key SAIDs of fresh pre-rotated keys. pub n: Vec, /// Anchors (typically attestations from witnesses agreeing to recovery). pub a: Vec, } -/// Delegation event: a parent controller delegates authority to a -/// child controller. The child's events anchor to the parent's log. -/// -/// Modeled after KERI `dip` (delegated-inception). v1 here covers -/// delegated inception only; delegated rotation (`drt`) reuses -/// [`RotationEvent`] with anchors back to the parent. +/// Delegated inception (an extension): a parent controller delegates +/// authority to a child controller, whose events anchor to the parent's log. +/// Modeled on KERI's `dip`: it is the child log's first event, and its `i` is +/// derived from its own SAID exactly as an inception's is. The parent's +/// anchoring of it lives in the parent's log, which the child's verifier does +/// not hold; [`crate::verifier::VerificationReport::delegator`] names the +/// parent so the caller can check it. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct DelegationEvent { /// Version string. pub v: String, - /// Event-type discriminator (always [`EventType::Delegation`]). - pub t: EventType, /// SAID of this event. pub d: Said, /// Child controller AID. pub i: ControllerAid, - /// Sequence number (0 for a delegated inception). + /// Sequence number: always 0. pub s: u64, - /// Prior-event SAID; empty SAID for a delegated inception. - pub p: Said, + /// The prior event's SAID: the empty string, as a delegated inception + /// has none. + pub p: String, /// Delegating parent controller AID. pub di: ControllerAid, /// Signing-key threshold. @@ -326,11 +316,11 @@ pub struct DelegationEvent { pub k: Vec, /// Next-key threshold. pub nt: Threshold, - /// SAIDs of pre-rotated keys. + /// Per-key SAIDs of pre-rotated keys. pub n: Vec, } -/// Discriminated union of every event type in a key-event log. +/// Discriminated union of every event type in a key-event log, tagged by `t`. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(tag = "t")] pub enum KeyEvent { @@ -388,16 +378,16 @@ impl KeyEvent { } } - /// Prior-event SAID, or [`None`] for an inception/delegated-inception. + /// The prior event's SAID as written in `p`: the empty string for an + /// inception or a delegated inception. #[must_use] - pub fn prior(&self) -> Option { + pub fn prior_text(&self) -> String { match self { - Self::Inception(_) => None, - Self::Delegation(e) if e.s == 0 => None, - Self::Rotation(e) => Some(e.p), - Self::Interaction(e) => Some(e.p), - Self::Recovery(e) => Some(e.p), - Self::Delegation(e) => Some(e.p), + Self::Inception(e) => e.p.clone(), + Self::Delegation(e) => e.p.clone(), + Self::Rotation(e) => e.p.to_said_text(), + Self::Interaction(e) => e.p.to_said_text(), + Self::Recovery(e) => e.p.to_said_text(), } } @@ -413,41 +403,98 @@ impl KeyEvent { } } - /// Canonical CBOR encoding of the event body. Used as the message - /// over which signatures are produced and verified. - pub fn to_cbor(&self) -> Result> { - Ok(serde_cbor::to_vec(self)?) + /// Whether this event derives its `i` from its own SAID: an inception, or + /// a delegated inception. + fn derives_aid(&self) -> bool { + matches!(self, Self::Inception(_) | Self::Delegation(_)) } - /// Recompute the SAID from the event body with the `d` field - /// substituted for the placeholder, per spec §2. For inception - /// and delegated-inception events the `i` field is also reset to - /// the empty AID because it is derived from `d` itself. - pub fn recompute_said(&self) -> Result { - let mut clone = self.clone(); - let placeholder = Said::default(); - let empty_aid = ControllerAid(String::new()); - match &mut clone { - Self::Inception(e) => { - e.d = placeholder; - e.i = empty_aid; - } - Self::Delegation(e) if e.s == 0 => { - e.d = placeholder; - e.i = empty_aid; - } - Self::Rotation(e) => e.d = placeholder, - Self::Interaction(e) => e.d = placeholder, - Self::Recovery(e) => e.d = placeholder, - Self::Delegation(e) => e.d = placeholder, + /// Whether the event uses anything outside spec v1: a `dip` or `rec` + /// event, or a witness pool. + #[must_use] + pub fn uses_extensions(&self) -> bool { + match self { + Self::Inception(e) => !e.b.is_empty() || !e.bt.is_zero(), + Self::Rotation(e) => !e.br.is_empty() || !e.ba.is_empty() || !e.bt.is_zero(), + Self::Interaction(_) => false, + Self::Recovery(_) | Self::Delegation(_) => true, } - let bytes = serde_cbor::to_vec(&clone)?; - Ok(Said::hash(&bytes)) } - /// Convenience constructor: validate this event's asserted SAID - /// against its body, returning [`KeriError::SaidMismatch`] on - /// disagreement. + fn derived(&self) -> &'static [Derived<'static>] { + if self.derives_aid() { + std::slice::from_ref(&INCEPTION_AID) + } else { + &[] + } + } + + /// Canonical CBOR encoding of the event: the message signatures are made + /// and verified over (spec §3.5). + pub fn to_cbor(&self) -> Result> { + Ok(wire::to_vec(self)?) + } + + /// Decode a spec-v1 event: [`KeyEvent::decode`] with no extensions. + pub fn from_cbor(bytes: &[u8]) -> Result { + Self::decode(bytes, true) + } + + /// Decode an event and check its SAID — identity spec §4 step 1, with the + /// decoding it presupposes: + /// + /// 1. the bytes are canonical CBOR (§2.3); + /// 2. `t` names an event type: under `strict`, one spec v1 defines, so + /// `rec` and `dip` are refused before anything else is read; + /// 3. the bytes are exactly the event's own encoding: no member the event + /// does not define, none of another type, none written where it is + /// omitted; + /// 4. under `strict`, no witness-pool member; + /// 5. `v` is the version this crate speaks; + /// 6. the SAID checks, and an inception's `i` is the AID it names. + pub fn decode(bytes: &[u8], strict: bool) -> Result { + let value = canonical::decode(bytes).map_err(wire::WireError::from)?; + let t = match value.get("t") { + Some(Value::Text(t)) => t.as_str(), + _ => return Err(KeriError::Malformed("an event is a map with a text `t`".into())), + }; + if strict && matches!(t, "rec" | "dip") { + return Err(KeriError::UnsupportedEventType(t.to_owned())); + } + let event: KeyEvent = wire::from_slice(bytes)?; + if strict && event.uses_extensions() { + return Err(KeriError::Malformed( + "a witness pool is an extension spec v1 does not define".into(), + )); + } + if event.version() != VERSION_STRING { + return Err(KeriError::UnsupportedVersion(event.version().to_owned())); + } + event.validate_said()?; + Ok(event) + } + + /// The version string `v`. + #[must_use] + pub fn version(&self) -> &str { + match self { + Self::Inception(e) => &e.v, + Self::Rotation(e) => &e.v, + Self::Interaction(e) => &e.v, + Self::Recovery(e) => &e.v, + Self::Delegation(e) => &e.v, + } + } + + /// Recompute the SAID from the event body with `d` — and, for an + /// inception, `i` — holding the placeholder, per spec §2 and §3.1. + pub fn recompute_said(&self) -> Result { + let value = wire::to_value(self)?; + Ok(said::derive_with(&value, SAID_FIELD, self.derived())?.said) + } + + /// Check this event's asserted SAID against its body, and an inception's + /// AID against its SAID. pub fn validate_said(&self) -> Result<()> { let computed = self.recompute_said()?; if computed != self.said() { @@ -456,72 +503,144 @@ impl KeyEvent { computed, }); } + if self.derives_aid() && *self.controller() != Aid::of_inception(computed) { + return Err(KeriError::ControllerAidMismatch { + expected: Aid::of_inception(computed).to_text(), + got: self.controller().to_text(), + }); + } Ok(()) } } -/// Helper: build the canonical inception event SAID from its body -/// (with `d` and `i` set to placeholders), then patch `d` and derive -/// the AID. Used by [`crate::controller::Controller::incept`]. -pub fn finalize_inception(mut event: InceptionEvent) -> Result { - event.d = Said::default(); - event.i = ControllerAid(String::new()); - let bytes = serde_cbor::to_vec(&KeyEvent::Inception(event.clone()))?; - let said = Said::hash(&bytes); - event.d = said; - event.i = ControllerAid::from_inception_said(&said); - // Re-hash with the controller AID populated; for spec-equivalence - // we anchor the controller AID via the `i` field, which depends on - // the SAID. To keep the SAID stable we re-derive after the second - // population by *only* re-encoding with the AID set, then patch - // back to a stable SAID computed over the body-with-placeholder - // form. We model this as a fixed-point: the SAID covers the body - // with `d` zeroed and `i` empty, both of which derive from the - // SAID itself. - Ok(event) -} - -/// Helper: patch a non-inception event so its `d` field reflects the -/// SAID of its body with `d` placeholdered. -pub fn finalize_event(event: KeyEvent) -> Result { - let mut clone = event; - let placeholder = Said::default(); - match &mut clone { - KeyEvent::Inception(e) => e.d = placeholder, - KeyEvent::Rotation(e) => e.d = placeholder, - KeyEvent::Interaction(e) => e.d = placeholder, - KeyEvent::Recovery(e) => e.d = placeholder, - KeyEvent::Delegation(e) => e.d = placeholder, - } - let bytes = serde_cbor::to_vec(&clone)?; - let said = Said::hash(&bytes); - match &mut clone { - KeyEvent::Inception(e) => e.d = said, +/// Stamp an event's SAID — and, for an inception, its AID — from its body. +pub fn finalize_event(mut event: KeyEvent) -> Result { + let said = event.recompute_said()?; + let aid = Aid::of_inception(said); + let derives = event.derives_aid(); + match &mut event { + KeyEvent::Inception(e) => { + e.d = said; + e.i = aid; + } KeyEvent::Rotation(e) => e.d = said, KeyEvent::Interaction(e) => e.d = said, KeyEvent::Recovery(e) => e.d = said, - KeyEvent::Delegation(e) => e.d = said, + KeyEvent::Delegation(e) => { + e.d = said; + e.i = aid; + } + } + debug_assert!(derives == matches!(event, KeyEvent::Inception(_) | KeyEvent::Delegation(_))); + Ok(event) +} + +/// Stamp an inception event's SAID and AID. +pub fn finalize_inception(event: InceptionEvent) -> Result { + match finalize_event(KeyEvent::Inception(event))? { + KeyEvent::Inception(e) => Ok(e), + _ => unreachable!("finalize_event keeps the event type"), } - Ok(clone) } #[cfg(test)] mod tests { use super::*; - #[test] - fn per_key_said_is_stable() { - let k = PublicKey::new(0x01, &[7u8; 32]); - let a = per_key_said(&k).expect("said"); - let b = per_key_said(&k).expect("said"); - assert_eq!(a, b); + fn inception() -> InceptionEvent { + finalize_inception(InceptionEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: Aid::default(), + s: 0, + p: String::new(), + kt: Threshold(1), + k: vec![PublicKey::new(0x01, &[7u8; 32])], + nt: Threshold(1), + n: vec![per_key_said(&PublicKey::new(0x01, &[8u8; 32])).unwrap()], + a: vec![], + b: vec![], + bt: Threshold(0), + }) + .unwrap() } #[test] - fn controller_aid_round_trip() { - let said = Said::hash(b"some-inception"); - let aid = ControllerAid::from_inception_said(&said); - let back = aid.as_inception_said().expect("inverse"); - assert_eq!(back, said); + fn per_key_said_is_the_cores() { + let k = PublicKey::new(0x01, &[7u8; 32]); + assert_eq!(per_key_said(&k).unwrap(), said::per_key_said(&k.0)); + } + + #[test] + fn an_inception_names_its_controller_by_its_own_said() { + let icp = inception(); + assert_eq!(icp.i, Aid::of_inception(icp.d)); + let event = KeyEvent::Inception(icp); + let bytes = event.to_cbor().unwrap(); + assert_eq!(KeyEvent::from_cbor(&bytes).unwrap(), event); + } + + #[test] + fn a_plain_inception_has_exactly_the_spec_fields() { + let value = wire::to_value(&KeyEvent::Inception(inception())).unwrap(); + let smart_byte_core::canonical::Value::Map(entries) = value else { + panic!("an event is a map") + }; + let mut keys: Vec = entries + .into_iter() + .map(|(k, _)| match k { + smart_byte_core::canonical::Value::Text(t) => t, + other => panic!("{other:?}"), + }) + .collect(); + keys.sort(); + assert_eq!(keys, ["a", "d", "i", "k", "kt", "n", "nt", "p", "s", "t", "v"]); + } + + #[test] + fn an_inception_naming_another_controller_is_refused() { + let mut icp = inception(); + icp.i = Aid::of_inception(Said::hash(b"someone else")); + assert!(KeyEvent::Inception(icp).validate_said().is_err()); + } + + /// `event`'s canonical value with `member` set to `value` and its SAID + /// (and AID) re-derived, so only the change is wrong. + fn restamped(event: &KeyEvent, member: &str, value: Value) -> Vec { + let Value::Map(mut entries) = wire::to_value(event).unwrap() else { unreachable!() }; + entries.retain(|(k, _)| *k != Value::text(member)); + entries.push((Value::text(member), value)); + said::derive_with(&Value::Map(entries), SAID_FIELD, event.derived()).unwrap().encoded + } + + #[test] + fn decoding_refuses_what_spec_v1_does_not_define() { + let icp = KeyEvent::Inception(inception()); + let class = |bytes: &[u8]| KeyEvent::from_cbor(bytes).unwrap_err().class(); + assert_eq!(KeyEvent::from_cbor(&icp.to_cbor().unwrap()).unwrap(), icp); + assert_eq!(class(&restamped(&icp, "x", Value::Null)), "malformed_event"); + assert_eq!(class(&restamped(&icp, "c", Value::Array(vec![]))), "malformed_event"); + assert_eq!(class(&restamped(&icp, "v", Value::text("SBYTE20JSON"))), "unsupported_version"); + assert_eq!(class(&restamped(&icp, "t", Value::text("rec"))), "unsupported_event_type"); + assert_eq!(class(&restamped(&icp, "t", Value::text("dip"))), "unsupported_event_type"); + assert_eq!(class(&restamped(&icp, "t", Value::text("zap"))), "malformed_event"); + // A witness pool decodes only with extensions on. + let pooled = restamped(&icp, "b", Value::Array(vec![Value::text("W")])); + assert_eq!(class(&pooled), "malformed_event"); + assert!(KeyEvent::decode(&pooled, false).is_ok()); + // Keys written as arrays of integers: canonical, but not the event's + // own encoding. + let k = Value::Array(vec![Value::Array((0..33).map(Value::Unsigned).collect())]); + assert_eq!(class(&restamped(&icp, "k", k)), "malformed_event"); + } + + #[test] + fn a_witness_is_named_by_its_key() { + let key = smart_byte_core::SigningKey::from_bytes(&[5; 32]).verifying_key(); + let aid = WitnessAid::of_key(&key); + assert_eq!((aid.0.len(), &aid.0[..1]), (53, "W")); + assert_eq!(aid.key(), Some(key.to_bytes())); + assert_eq!(WitnessAid(aid.0.to_ascii_uppercase()).key(), None); + assert_eq!(WitnessAid("w1".into()).key(), None); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/lib.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/lib.rs index bcec74f41..b94a8a4c0 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/lib.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/lib.rs @@ -16,17 +16,21 @@ //! controller cannot rewrite their own history (see [`witness`]); //! * **watchers** — third-party observers who detect duplicity by //! collecting receipts and comparing logs (see [`watcher`]); -//! * **recovery** semantics — the reserved `rec` event type elevated -//! to first-class status here, gated to catastrophic key loss; +//! * **recovery** semantics — the reserved `rec` event type, accepted +//! only with its witnesses' receipts, for catastrophic key loss; //! * **delegated identifiers** so an organisation can delegate to a //! department whose events anchor to the parent's log; +//! +//! Spec v1 defines inception, rotation and interaction, and nothing else: +//! the default [`LogVerifier`] refuses the witness pool, delegation and +//! recovery, which are this crate's extensions. //! * **OOBI** (Out-Of-Band Introduction) for bootstrapping "I claim //! to be controller X, here are my witnesses" (see [`oobi`]). //! -//! All events are encoded using deterministic CBOR per the spec; SAIDs -//! are 32-byte BLAKE3 digests via [`smart_byte_core::Said`]. Signing -//! supports both classical Ed25519 and hybrid (Ed25519 + ML-DSA) via -//! [`smart_byte_pq`]. +//! All events are canonical CBOR per identity spec §2.3, and SAIDs are +//! BLAKE3-256 digests via [`smart_byte_core::Said`]. Keys and signatures are +//! Ed25519; the algorithm byte in front of each reserves room for +//! post-quantum schemes. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -43,13 +47,15 @@ pub mod witness; pub use controller::{Controller, KeyPair}; pub use error::{KeriError, Result}; pub use events::{ - Anchor, ConfigTrait, ControllerAid, DelegationEvent, EventType, InceptionEvent, - InteractionEvent, KeyEvent, PublicKey, RecoveryEvent, RotationEvent, Threshold, WatcherAid, - WitnessAid, + Anchor, ControllerAid, DelegationEvent, EventType, InceptionEvent, InteractionEvent, KeyEvent, + PublicKey, RecoveryEvent, RotationEvent, Threshold, WatcherAid, WitnessAid, }; pub use oobi::Oobi; pub use storage::{EventLogStorage, FileStorage, MemoryStorage}; -pub use verifier::{DuplicityEvidence, LogVerifier, VerificationReport}; +pub use verifier::{ + DuplicityEvidence, Establishment, LogRejection, LogVerifier, Replay, SignedEvent, + VerificationReport, +}; pub use watcher::{DuplicityDetected, Watcher}; pub use witness::{Witness, WitnessReceipt}; diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/oobi.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/oobi.rs index e78bd74ec..87312cc39 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/oobi.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/oobi.rs @@ -1,17 +1,21 @@ //! Out-Of-Band Introduction (OOBI). //! -//! An OOBI is a signed envelope distributed off-band that says "I claim -//! to be controller X, here are my witnesses, here's how to reach them." -//! Verifiers use OOBIs to bootstrap a controller's identity: from an -//! OOBI plus a witness reachable per the OOBI, a verifier can fetch -//! the full key-event log and replay it. +//! An OOBI is distributed off-band and says "I am controller X, here are my +//! witnesses, here's how to reach them." Verifiers use OOBIs to bootstrap a +//! controller's identity: from an OOBI plus a witness reachable per the OOBI, +//! a verifier can fetch the full key-event log and replay it. +//! +//! The controller signs its OOBI with the keys its log puts in force, so an +//! OOBI verifies only against that log's key states ([`Oobi::verify`]): +//! before the log is in hand it is a hint, and after, it is the controller's +//! own statement of where it can be reached. use chrono::{DateTime, Utc}; -use ed25519_dalek::{Signature, Signer as EdSigner, Verifier, VerifyingKey}; use serde::{Deserialize, Serialize}; -use smart_byte_core::Said; +use smart_byte_core::keys::verify_controller_signature; +use smart_byte_core::{ControllerSignature, KeyStates, Said, wire}; -use crate::controller::KeyPair; +use crate::controller::Controller; use crate::error::{KeriError, Result}; use crate::events::{ControllerAid, WitnessAid}; @@ -33,98 +37,140 @@ pub struct OobiEndpoint { pub struct Oobi { /// Controller AID being introduced. pub controller: ControllerAid, - /// SAID of the controller's inception event — pinning the - /// introduction to one specific identity epoch. + /// SAID of the controller's inception event: the AID names it, so the + /// introduction is pinned to one identity. pub inception_said: Said, /// Endpoints for the controller's witnesses. pub endpoints: Vec, /// When the OOBI was minted. pub issued_at: DateTime, - /// Issuer's Ed25519 verifying key, base32-encoded. - pub issuer_pubkey_b32: String, - /// Ed25519 signature over the canonical CBOR of the OOBI with the - /// signature field cleared, base32-encoded. - pub signature_b32: String, + /// The controller's signature over [`Oobi::signed_bytes`]. + pub signature: ControllerSignature, +} + +/// What the controller signs: everything but the signature. +#[derive(Serialize)] +struct OobiBody<'a> { + controller: &'a ControllerAid, + endpoints: &'a [OobiEndpoint], + inception_said: &'a Said, + issued_at: &'a DateTime, } impl Oobi { - /// Mint a fresh OOBI signed by `signer`. - pub fn issue( - controller: ControllerAid, - inception_said: Said, - endpoints: Vec, - signer: &KeyPair, - ) -> Result { + /// Mint an OOBI for `controller`, signed with its current keys. + pub fn issue(controller: &Controller, endpoints: Vec) -> Result { + let signer = controller.signer(); let mut oobi = Self { - controller, - inception_said, + controller: controller.aid, + inception_said: controller.aid.inception_said(), endpoints, issued_at: Utc::now(), - issuer_pubkey_b32: data_encoding_b32(signer.verifying.as_bytes()), - signature_b32: String::new(), + signature: ControllerSignature { + establishment: signer.establishment, + signatures: Vec::new(), + }, }; - let body = serde_cbor::to_vec(&oobi)?; - let sig: Signature = signer.signing.sign(&body); - oobi.signature_b32 = data_encoding_b32(&sig.to_bytes()); + oobi.signature = signer.sign(&oobi.signed_bytes()?); Ok(oobi) } - /// Verify the OOBI's signature against the embedded issuer public key. - pub fn verify(&self) -> Result<()> { - let pk_bytes = decode_b32_fixed::<32>(&self.issuer_pubkey_b32)?; - let vk = VerifyingKey::from_bytes(&pk_bytes) - .map_err(|e| KeriError::MalformedKey(e.to_string()))?; - let sig_bytes = decode_b32_fixed::<64>(&self.signature_b32)?; - let sig = Signature::from_bytes(&sig_bytes); - - let mut stripped = self.clone(); - stripped.signature_b32 = String::new(); - let body = serde_cbor::to_vec(&stripped)?; - vk.verify(&body, &sig).map_err(|_| KeriError::BadSignature) + /// The message the controller signs. + pub fn signed_bytes(&self) -> Result> { + Ok(wire::to_vec(&OobiBody { + controller: &self.controller, + endpoints: &self.endpoints, + inception_said: &self.inception_said, + issued_at: &self.issued_at, + })?) } -} -fn data_encoding_b32(bytes: &[u8]) -> String { - data_encoding::BASE32_NOPAD.encode(bytes) -} - -fn decode_b32_fixed(s: &str) -> Result<[u8; N]> { - let upper = s.to_ascii_uppercase(); - let bytes = data_encoding::BASE32_NOPAD - .decode(upper.as_bytes()) - .map_err(|e| KeriError::MalformedKey(e.to_string()))?; - if bytes.len() != N { - return Err(KeriError::MalformedKey(format!( - "expected {N}-byte base32 payload, got {}", - bytes.len() - ))); + /// Verify the OOBI against the controller's key states — those its + /// replayed log establishes ([`crate::VerificationReport::add_to`]). + pub fn verify(&self, keys: &impl KeyStates) -> Result<()> { + if self.controller != ControllerAid::of_inception(self.inception_said) { + return Err(KeriError::ControllerAidMismatch { + expected: ControllerAid::of_inception(self.inception_said).to_text(), + got: self.controller.to_text(), + }); + } + Ok(verify_controller_signature( + keys, + &self.controller, + &self.signed_bytes()?, + &self.signature, + )?) } - let mut out = [0u8; N]; - out.copy_from_slice(&bytes); - Ok(out) } #[cfg(test)] mod tests { + use std::collections::HashMap; + use super::*; - use rand::rngs::OsRng; + use crate::controller::KeyPair; + use crate::events::{KeyEvent, Threshold}; + use crate::verifier::LogVerifier; + use smart_byte_core::KeyBook; #[test] - fn oobi_round_trip() { - let mut rng = OsRng; - let kp = KeyPair::generate(&mut rng); - let said = Said::hash(b"inception"); - let aid = ControllerAid::from_inception_said(&said); + fn an_oobi_verifies_against_its_controllers_log_only() { + let key = |n: u8| KeyPair::from_signing(smart_byte_core::SigningKey::from_bytes(&[n; 32])); + let (ctrl, icp) = Controller::incept( + vec![key(1)], + vec![key(2)], + Threshold(1), + Threshold(1), + vec![], + Threshold(0), + ) + .expect("incept"); + let witness = WitnessAid::of_key(&key(9).verifying); let oobi = Oobi::issue( - aid, - said, + &ctrl, vec![OobiEndpoint { - witness: WitnessAid("W1".into()), + witness, url: "https://example.test/witness1".into(), }], - &kp, ) .expect("issue"); - oobi.verify().expect("verify"); + + let icp = KeyEvent::Inception(icp); + let sigs = HashMap::from([(icp.said(), ctrl.sign_event(&icp).unwrap())]); + let report = LogVerifier::new() + .verify(&[icp], &sigs, &HashMap::new()) + .expect("log"); + let mut keys = KeyBook::new(); + report.add_to(&mut keys); + oobi.verify(&keys).expect("verify"); + + // Without the log, nothing vouches for it. + assert_eq!( + oobi.verify(&KeyBook::new()).unwrap_err().class(), + "unknown_key_state" + ); + // An endpoint swapped in after signing. + let mut redirected = oobi.clone(); + redirected.endpoints[0].url = "https://attacker.test".into(); + assert_eq!( + redirected.verify(&keys).unwrap_err().class(), + "signature_invalid" + ); + // Someone else's controller signing in this one's name. + let (other, _) = Controller::incept( + vec![key(3)], + vec![key(4)], + Threshold(1), + Threshold(1), + vec![], + Threshold(0), + ) + .expect("incept"); + let mut forged = oobi.clone(); + forged.signature = other.signer().sign(&forged.signed_bytes().unwrap()); + assert_eq!( + forged.verify(&keys).unwrap_err().class(), + "signature_invalid" + ); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/storage.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/storage.rs index 6112ebe27..e1be07b69 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/storage.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/storage.rs @@ -103,7 +103,7 @@ impl FileStorage { } fn log_path(&self, controller: &ControllerAid) -> PathBuf { - self.logs_dir().join(format!("{}.cbor", controller.0)) + self.logs_dir().join(format!("{}.cbor", controller.to_text())) } fn receipt_path(&self, said: &Said) -> PathBuf { @@ -118,12 +118,12 @@ impl EventLogStorage for FileStorage { let path = self.log_path(controller); let mut existing: Vec = if path.exists() { let bytes = fs::read(&path).await?; - serde_cbor::from_slice(&bytes)? + smart_byte_core::wire::from_slice(&bytes)? } else { Vec::new() }; existing.push(event); - let bytes = serde_cbor::to_vec(&existing)?; + let bytes = smart_byte_core::wire::to_vec(&existing)?; fs::write(&path, bytes).await?; Ok(()) } @@ -133,12 +133,12 @@ impl EventLogStorage for FileStorage { let path = self.receipt_path(&event_said); let mut existing: Vec = if path.exists() { let bytes = fs::read(&path).await?; - serde_cbor::from_slice(&bytes)? + smart_byte_core::wire::from_slice(&bytes)? } else { Vec::new() }; existing.push(receipt); - let bytes = serde_cbor::to_vec(&existing)?; + let bytes = smart_byte_core::wire::to_vec(&existing)?; fs::write(&path, bytes).await?; Ok(()) } @@ -149,7 +149,7 @@ impl EventLogStorage for FileStorage { return Ok(Vec::new()); } let bytes = fs::read(&path).await?; - Ok(serde_cbor::from_slice(&bytes)?) + Ok(smart_byte_core::wire::from_slice(&bytes)?) } async fn fetch_receipts(&self, event_said: Said) -> Result> { @@ -158,7 +158,7 @@ impl EventLogStorage for FileStorage { return Ok(Vec::new()); } let bytes = fs::read(&path).await?; - Ok(serde_cbor::from_slice(&bytes)?) + Ok(smart_byte_core::wire::from_slice(&bytes)?) } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/verifier.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/verifier.rs index 25c5784b4..daa8edec0 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/verifier.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/verifier.rs @@ -1,19 +1,26 @@ -//! Replay verifier per spec §4. +//! Replay verifier per identity spec §4. //! -//! Given a sequence of events and (optionally) witness receipts, the -//! verifier accepts the log iff every spec-mandated check passes for -//! every event in sequence. +//! A [`Replay`] takes a controller's events one at a time, in log order, and +//! accepts each only if every check of §4 passes; the first failure is +//! terminal. [`LogVerifier`] runs a whole log through one, from decoded +//! events ([`LogVerifier::verify`]) or from their wire form +//! ([`LogVerifier::verify_encoded`], which also says which event was refused). +//! +//! A [`LogVerifier`] is a spec-v1 verifier by default. With +//! [`LogVerifier::strict`] off it also accepts this crate's extensions — a +//! witness pool, delegated inception and witnessed recovery — and with +//! [`LogVerifier::require_witness_quorum`] on it requires every event to carry +//! receipts from the pool's threshold of witnesses. -use std::collections::{BTreeMap, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; -use ed25519_dalek::{Signature, Verifier as EdVerifier, VerifyingKey}; use serde::{Deserialize, Serialize}; -use smart_byte_core::Said; +use smart_byte_core::keys::{IndexedSignature, KeyState, verify_against}; +use smart_byte_core::{KeyBook, PrefixedKey, Said}; +use crate::VERSION_STRING; use crate::error::{KeriError, Result}; -use crate::events::{ - ControllerAid, EventType, KeyEvent, PublicKey, Threshold, per_key_said, -}; +use crate::events::{ControllerAid, KeyEvent, PublicKey, Threshold, WitnessAid, per_key_said}; use crate::witness::WitnessReceipt; /// Evidence record for a detected duplicity event. @@ -25,30 +32,63 @@ pub struct DuplicityEvidence { pub fork: (Said, Said), } +/// The key state one establishment event put in force. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Establishment { + /// The establishment event's sequence number. + pub sequence: u64, + /// The signing keys it put in force. + pub keys: Vec, + /// The signing threshold it put in force. + pub threshold: Threshold, +} + /// Outcome of a full-log verification. #[derive(Clone, Debug)] pub struct VerificationReport { + /// The controller whose log this is. + pub controller: ControllerAid, + /// Every establishment event's key state, in log order. A signature that + /// names an establishment event (an envelope's, say) is checked against + /// these, so it stays verifiable after the keys rotate away. + pub establishments: Vec, /// Signing keys in force after the last accepted event. pub current_keys: Vec, /// Threshold in force after the last accepted event. pub current_threshold: Threshold, + /// Next-key digests committed by the last establishment event. + pub next_keys: Vec, + /// Next-key threshold committed by the last establishment event. + pub next_threshold: Threshold, /// Highest sequence number accepted. pub last_sequence: u64, - /// Whether the witness-receipt threshold was met for every event. + /// SAID of the last accepted event. + pub last_said: Said, + /// Extension: the witness pool in force. + pub witnesses: BTreeSet, + /// Extension: the witness-receipt threshold in force. + pub witness_threshold: Threshold, + /// Whether every event carried receipts from the threshold of its + /// witnesses (vacuously, with no witness threshold). pub witness_quorum_met: bool, /// How many recovery events the log contains. pub recovery_count: u32, - /// Duplicity evidence collected from receipts (if any). + /// Extension: the parent a delegated inception names. The parent's log + /// must anchor the child's inception; the caller checks that there. + pub delegator: Option, + /// Duplicity evidence: verified receipts, by the log's witnesses, for two + /// different events at one sequence number. pub duplicity: Vec, } /// Replay verifier. +#[derive(Clone, Copy, Debug)] pub struct LogVerifier { - /// `true` (the default): reject `rec` events outright. Identity spec §3.2 - /// reserves `rec`, and a v1 verifier MUST reject it as malformed. `false` - /// accepts recoveries verified against the keys they reveal — an - /// extension outside spec v1, for exercising recovery before the spec - /// defines it. + /// `true` (the default): spec v1 only. `rec` and `dip` events and witness + /// pools are refused — identity spec §3.2 reserves `rec`, and v1 defines + /// neither delegation nor witnesses. `false` accepts this crate's + /// extensions: a witness pool, delegated inception, and recovery, which + /// counts only with its witnesses' receipts. pub strict: bool, /// When `true`, also require the witness-receipt threshold be met /// for every event in the log. @@ -64,16 +104,34 @@ impl Default for LogVerifier { } } +/// One event as it travels: its bytes, and the controller's signatures over +/// them — `(key index, algorithm byte ‖ signature)` pairs (spec §3.5). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignedEvent { + /// The event's canonical encoding. + pub bytes: Vec, + /// The signatures over `bytes`. + pub signatures: Vec<(u32, Vec)>, +} + +/// A refused log: which event was refused, and why. +#[derive(Debug)] +pub struct LogRejection { + /// Position of the refused event in the log, from 0. + pub index: usize, + /// Why it was refused. + pub error: KeriError, +} + impl LogVerifier { - /// Build a spec-v1 verifier: rejects `rec`, and does not require witness - /// quorum. + /// Build a spec-v1 verifier: no extensions, no witness quorum. #[must_use] pub fn new() -> Self { Self::default() } - /// `true` (the default) rejects `rec` per spec v1; `false` opts into the - /// recovery extension. + /// `true` (the default) accepts spec v1 only; `false` opts into the + /// extensions. #[must_use] pub fn strict(mut self, on: bool) -> Self { self.strict = on; @@ -87,330 +145,630 @@ impl LogVerifier { self } - /// Replay the supplied log and produce a [`VerificationReport`]. + /// Replay decoded events, with their signatures keyed by event SAID. pub fn verify( &self, log: &[KeyEvent], signatures: &HashMap)>>, receipts: &HashMap>, ) -> Result { - if log.is_empty() { - return Err(KeriError::Malformed("empty log".into())); + let mut replay = Replay::new(*self); + for event in log { + let sigs = signatures.get(&event.said()).map_or(&[][..], Vec::as_slice); + replay.apply(event, sigs, receipts)?; } - // First event must be inception or delegated-inception. - let first = &log[0]; - let (mut current_keys, mut current_threshold, mut next_keys_committed, mut next_threshold, mut witness_threshold) = - match first { - KeyEvent::Inception(icp) => { - if icp.s != 0 { - return Err(KeriError::SequenceError { - expected: 0, - got: icp.s, - }); - } - ( - icp.k.clone(), - icp.kt, - icp.n.clone(), - icp.nt, - icp.bt, - ) - } - KeyEvent::Delegation(dip) if dip.s == 0 => ( - dip.k.clone(), - dip.kt, - dip.n.clone(), - dip.nt, - Threshold(0), + replay.finish(receipts) + } + + /// Replay events from their wire form, decoding each per spec §4 step 1 + /// ([`KeyEvent::decode`]) as it comes. + pub fn verify_encoded( + &self, + log: &[SignedEvent], + receipts: &HashMap>, + ) -> std::result::Result { + let mut replay = Replay::new(*self); + for (index, signed) in log.iter().enumerate() { + KeyEvent::decode(&signed.bytes, self.strict) + .and_then(|event| replay.apply(&event, &signed.signatures, receipts)) + .map_err(|error| LogRejection { index, error })?; + } + replay + .finish(receipts) + .map_err(|error| LogRejection { index: 0, error }) + } +} + +/// The state replay has established so far. +#[derive(Clone, Debug)] +struct State { + controller: ControllerAid, + current_keys: Vec, + current_threshold: Threshold, + next_keys: Vec, + next_threshold: Threshold, + last_sequence: u64, + last_said: Said, + witnesses: BTreeSet, + witness_threshold: Threshold, +} + +/// Incremental replay of one controller's log (spec §4). +#[derive(Clone, Debug)] +pub struct Replay { + verifier: LogVerifier, + state: Option, + establishments: Vec, + /// Every witness that has been in the pool: whose receipts count as + /// evidence of duplicity. + ever_witnesses: BTreeSet, + recovery_count: u32, + delegator: Option, + all_witnessed: bool, +} + +impl Replay { + /// An empty replay. + #[must_use] + pub fn new(verifier: LogVerifier) -> Self { + Self { + verifier, + state: None, + establishments: Vec::new(), + ever_witnesses: BTreeSet::new(), + recovery_count: 0, + delegator: None, + all_witnessed: true, + } + } + + /// Accept the next event, or refuse it. A refusal leaves the replay as it + /// was. + pub fn apply( + &mut self, + event: &KeyEvent, + signatures: &[(u32, Vec)], + receipts: &HashMap>, + ) -> Result<()> { + let strict = self.verifier.strict; + // What decoding checks, for events handed over already decoded. + if strict && event.uses_extensions() { + return Err(match event { + KeyEvent::Recovery(_) => KeriError::UnsupportedEventType("rec".into()), + KeyEvent::Delegation(_) => KeriError::UnsupportedEventType("dip".into()), + _ => KeriError::Malformed( + "a witness pool is an extension spec v1 does not define".into(), ), - _ => { + }); + } + if event.version() != VERSION_STRING { + return Err(KeriError::UnsupportedVersion(event.version().to_owned())); + } + // 1. SAID, and an inception's AID. + event.validate_said()?; + + let first = self.state.is_none(); + let begins = matches!(event, KeyEvent::Inception(_) | KeyEvent::Delegation(_)); + if first != begins { + return Err(KeriError::Malformed(if first { + "a log begins with an inception".into() + } else { + "an inception appears only at sequence 0".into() + })); + } + // 2. Sequence. + let expected = self.state.as_ref().map_or(0, |s| s.last_sequence + 1); + if event.sequence() != expected { + return Err(KeriError::SequenceError { + expected, + got: event.sequence(), + }); + } + // 3. Prior event. + let expected = self + .state + .as_ref() + .map_or_else(String::new, |s| s.last_said.to_said_text()); + if event.prior_text() != expected { + return Err(KeriError::PriorMismatch { + expected, + got: event.prior_text(), + }); + } + // 4. Controller (an inception's own AID was checked in step 1). + if let Some(s) = &self.state + && event.controller() != &s.controller + { + return Err(KeriError::ControllerAidMismatch { + expected: s.controller.to_text(), + got: event.controller().to_text(), + }); + } + + // 5 and 6, by type. Each arm computes the state the event puts in + // force; nothing is committed until every check has passed. + let mut next = self.state.clone(); + let mut establishes = true; + match event { + KeyEvent::Inception(icp) => { + if !icp.a.is_empty() { return Err(KeriError::Malformed( - "log must begin with inception or delegated-inception".into(), - )) - } - }; - let controller_aid = first.controller().clone(); - let mut last_said = first.said(); - let mut last_sequence = first.sequence(); - let mut recovery_count = 0u32; - let mut witness_quorum_met = true; - let mut duplicity = Vec::new(); - - // Verify the inception itself. - self.verify_event( - first, - ¤t_keys, - current_threshold, - None, - &controller_aid, - signatures, - )?; - self.check_witness_quorum( - first, - witness_threshold, - receipts, - &mut witness_quorum_met, - )?; - - for (idx, event) in log.iter().enumerate().skip(1) { - // 1. SAID consistency. - event.validate_said()?; - // 2. Sequence. - if event.sequence() != last_sequence + 1 { - return Err(KeriError::SequenceError { - expected: last_sequence + 1, - got: event.sequence(), - }); - } - // 3. Prior linkage. - if event.prior() != Some(last_said) { - return Err(KeriError::PriorMismatch { - expected: last_said, - got: event.prior().unwrap_or_default(), - }); - } - // 4. Controller AID. - if event.controller() != &controller_aid { - return Err(KeriError::ControllerAidMismatch { - expected: controller_aid.0.clone(), - got: event.controller().0.clone(), - }); - } - - // 5. Type-specific structural check. - match event { - KeyEvent::Rotation(rot) => { - if rot.k.len() != next_keys_committed.len() { - return Err(KeriError::PreRotationMismatch { index: 0 }); - } - for (i, key) in rot.k.iter().enumerate() { - let derived = per_key_said(key)?; - if derived != next_keys_committed[i] { - return Err(KeriError::PreRotationMismatch { index: i }); - } - } - // Verify signatures against the NEWLY-revealed keys - // (rotation is signed by the keys it reveals). - self.verify_event( - event, - &rot.k, - rot.kt, - Some(idx as u64), - &controller_aid, - signatures, - )?; - self.check_witness_quorum( - event, - rot.bt, - receipts, - &mut witness_quorum_met, - )?; - current_keys = rot.k.clone(); - current_threshold = rot.kt; - next_keys_committed = rot.n.clone(); - next_threshold = rot.nt; - witness_threshold = rot.bt; - } - KeyEvent::Interaction(_) => { - self.verify_event( - event, - ¤t_keys, - current_threshold, - Some(idx as u64), - &controller_aid, - signatures, - )?; - self.check_witness_quorum( - event, - witness_threshold, - receipts, - &mut witness_quorum_met, - )?; - } - KeyEvent::Recovery(rec) => { - if self.strict { - return Err(KeriError::StrictSpecRejectsRec); - } - // Recovery is verified against the keys it reveals. - self.verify_event( - event, - &rec.k, - rec.kt, - Some(idx as u64), - &controller_aid, - signatures, - )?; - self.check_witness_quorum( - event, - witness_threshold, - receipts, - &mut witness_quorum_met, - )?; - current_keys = rec.k.clone(); - current_threshold = rec.kt; - next_keys_committed = rec.n.clone(); - next_threshold = rec.nt; - recovery_count += 1; - } - KeyEvent::Delegation(dlg) => { - // Delegated rotation: signed by revealed keys. - self.verify_event( - event, - &dlg.k, - dlg.kt, - Some(idx as u64), - &controller_aid, - signatures, - )?; - current_keys = dlg.k.clone(); - current_threshold = dlg.kt; - next_keys_committed = dlg.n.clone(); - next_threshold = dlg.nt; - } - KeyEvent::Inception(_) => { - return Err(KeriError::Malformed( - "inception event appeared after sequence 0".into(), + "an inception carries no anchors".into(), )); } - } - - last_said = event.said(); - last_sequence = event.sequence(); - } - - // Duplicity: inspect receipts for forked SAIDs at the same sequence. - // Said does not implement Ord so we key by sequence and dedupe with - // a Vec compared via byte-equality. - let mut by_seq: BTreeMap> = BTreeMap::new(); - for (said, rs) in receipts { - for r in rs { - if r.controller == controller_aid { - let bucket = by_seq.entry(r.sequence).or_default(); - if !bucket.contains(said) { - bucket.push(*said); - } - } - } - } - for (seq, saids) in by_seq { - if saids.len() > 1 { - let a = saids[0]; - let b = saids[1]; - duplicity.push(DuplicityEvidence { - sequence: seq, - fork: (a, b), + check_establishment(&icp.k, icp.kt, &icp.n, icp.nt)?; + let witnesses = pool(&icp.b)?; + check_witness_threshold(icp.bt, &witnesses)?; + verify_event_signatures(event, &icp.k, icp.kt, signatures)?; + next = Some(State { + controller: icp.i, + current_keys: icp.k.clone(), + current_threshold: icp.kt, + next_keys: icp.n.clone(), + next_threshold: icp.nt, + last_sequence: 0, + last_said: icp.d, + witnesses, + witness_threshold: icp.bt, }); } + KeyEvent::Delegation(dip) => { + check_establishment(&dip.k, dip.kt, &dip.n, dip.nt)?; + verify_event_signatures(event, &dip.k, dip.kt, signatures)?; + next = Some(State { + controller: dip.i, + current_keys: dip.k.clone(), + current_threshold: dip.kt, + next_keys: dip.n.clone(), + next_threshold: dip.nt, + last_sequence: 0, + last_said: dip.d, + witnesses: BTreeSet::new(), + witness_threshold: Threshold(0), + }); + } + KeyEvent::Rotation(rot) => { + let s = next.as_mut().expect("not the first event"); + if s.next_keys.is_empty() { + return Err(KeriError::RotationNotPermitted); + } + if rot.k.len() != s.next_keys.len() { + return Err(KeriError::PreRotationMismatch { + index: rot.k.len().min(s.next_keys.len()), + }); + } + for (i, key) in rot.k.iter().enumerate() { + if per_key_said(key)? != s.next_keys[i] { + return Err(KeriError::PreRotationMismatch { index: i }); + } + } + if !rot.a.is_empty() { + return Err(KeriError::Malformed("a rotation carries no anchors".into())); + } + check_establishment(&rot.k, rot.kt, &rot.n, rot.nt)?; + let witnesses = rotate_pool(&s.witnesses, &rot.br, &rot.ba)?; + check_witness_threshold(rot.bt, &witnesses)?; + // Signed by the keys it reveals, under both the threshold + // committed before — so it cannot be lowered without the + // pre-rotated keys — and its own (spec §3.5). + verify_event_signatures(event, &rot.k, s.next_threshold.max(rot.kt), signatures)?; + s.current_keys = rot.k.clone(); + s.current_threshold = rot.kt; + s.next_keys = rot.n.clone(); + s.next_threshold = rot.nt; + s.witnesses = witnesses; + s.witness_threshold = rot.bt; + } + KeyEvent::Interaction(ixn) => { + let s = next.as_ref().expect("not the first event"); + if ixn.k != s.current_keys + || ixn.kt != s.current_threshold + || ixn.n != s.next_keys + || ixn.nt != s.next_threshold + { + return Err(KeriError::KeysChanged); + } + verify_event_signatures(event, &s.current_keys, s.current_threshold, signatures)?; + establishes = false; + } + KeyEvent::Recovery(rec) => { + let s = next.as_mut().expect("not the first event"); + check_establishment(&rec.k, rec.kt, &rec.n, rec.nt)?; + verify_event_signatures(event, &rec.k, rec.kt, signatures)?; + // The keys a recovery reveals were never committed to, so + // only the log's witnesses can speak for it. + if s.witness_threshold.0 == 0 { + return Err(KeriError::RecoveryRejected( + "a recovery counts only with witness receipts, and the log has no witness threshold".into(), + )); + } + check_witness_quorum(event, s, receipts)?; + s.current_keys = rec.k.clone(); + s.current_threshold = rec.kt; + s.next_keys = rec.n.clone(); + s.next_threshold = rec.nt; + } } - let _ = next_threshold; // kept in report-implicit state + let s = next.as_mut().expect("every arm leaves a state"); + let witnessed = check_witness_quorum(event, s, receipts); + if self.verifier.require_witness_quorum { + witnessed?; + } else if witnessed.is_err() { + self.all_witnessed = false; + } + s.last_sequence = event.sequence(); + s.last_said = event.said(); + if establishes { + self.establishments.push(Establishment { + sequence: event.sequence(), + keys: s.current_keys.clone(), + threshold: s.current_threshold, + }); + } + self.ever_witnesses.extend(s.witnesses.iter().cloned()); + if let KeyEvent::Recovery(_) = event { + self.recovery_count += 1; + } + if let KeyEvent::Delegation(dip) = event { + self.delegator = Some(dip.di); + } + self.state = next; + Ok(()) + } + + /// The report for the events accepted so far. + pub fn finish( + &self, + receipts: &HashMap>, + ) -> Result { + let s = self + .state + .as_ref() + .ok_or_else(|| KeriError::Malformed("empty log".into()))?; + // Duplicity: verified receipts, by witnesses of this log, for two + // different events at one sequence number. + let mut by_sequence: BTreeMap> = BTreeMap::new(); + for r in receipts.values().flatten() { + if r.controller == s.controller + && self.ever_witnesses.contains(&r.witness) + && r.verify_signature().is_ok() + { + by_sequence + .entry(r.sequence) + .or_default() + .insert(r.signed_event_said); + } + } + let duplicity = by_sequence + .into_iter() + .filter(|(_, saids)| saids.len() > 1) + .map(|(sequence, saids)| { + let mut it = saids.into_iter(); + let a = it.next().expect("two or more"); + let b = it.next().expect("two or more"); + DuplicityEvidence { + sequence, + fork: (a, b), + } + }) + .collect(); Ok(VerificationReport { - current_keys, - current_threshold, - last_sequence, - witness_quorum_met, - recovery_count, + controller: s.controller, + establishments: self.establishments.clone(), + current_keys: s.current_keys.clone(), + current_threshold: s.current_threshold, + next_keys: s.next_keys.clone(), + next_threshold: s.next_threshold, + last_sequence: s.last_sequence, + last_said: s.last_said, + witnesses: s.witnesses.clone(), + witness_threshold: s.witness_threshold, + witness_quorum_met: self.all_witnessed, + recovery_count: self.recovery_count, + delegator: self.delegator, duplicity, }) } +} - fn verify_event( - &self, - event: &KeyEvent, - keys: &[PublicKey], - threshold: Threshold, - _idx: Option, - controller_aid: &ControllerAid, - signatures: &HashMap)>>, - ) -> Result<()> { - // SAID consistency. - event.validate_said()?; - // Controller AID consistency. - if event.controller() != controller_aid { - return Err(KeriError::ControllerAidMismatch { - expected: controller_aid.0.clone(), - got: event.controller().0.clone(), - }); - } - // Inception structural check. - if let KeyEvent::Inception(icp) = event - && icp.s != 0 - { - return Err(KeriError::SequenceError { expected: 0, got: icp.s }); - } - // Signature threshold. - let sigs = signatures.get(&event.said()).cloned().unwrap_or_default(); - let body = event.to_cbor()?; - let mut valid = std::collections::BTreeSet::new(); - for (i, sig_bytes) in sigs { - let pk = keys.get(i as usize).ok_or_else(|| { - KeriError::Malformed(format!("signature references unknown key index {i}")) - })?; - if pk.algorithm() != Some(0x01) || pk.key_bytes().len() != 32 { - continue; - } - let mut pk_arr = [0u8; 32]; - pk_arr.copy_from_slice(pk.key_bytes()); - let vk = match VerifyingKey::from_bytes(&pk_arr) { - Ok(v) => v, - Err(_) => continue, - }; - if sig_bytes.len() != 64 { - continue; - } - let mut sig_arr = [0u8; 64]; - sig_arr.copy_from_slice(&sig_bytes); - let sig = Signature::from_bytes(&sig_arr); - if vk.verify(&body, &sig).is_ok() { - valid.insert(i); - } - } - if (valid.len() as u32) < threshold.0 { - return Err(KeriError::ThresholdNotMet { - have: valid.len() as u32, - need: threshold.0, - }); - } - Ok(()) +/// Spec §4 step 5 for an establishment event's key fields: at least one +/// signing key, no key or next-key digest listed twice, `1 <= kt <= |k|`, and +/// `nt` 0 when `n` is empty and otherwise `1 <= nt <= |n|`. +pub(crate) fn check_establishment( + k: &[PublicKey], + kt: Threshold, + n: &[Said], + nt: Threshold, +) -> Result<()> { + if let Some(index) = first_repeat(k) { + return Err(KeriError::DuplicateKey { field: "k", index }); } + if kt.0 == 0 || kt.0 as usize > k.len() { + return Err(KeriError::InvalidThreshold { + field: "kt", + threshold: kt.0, + keys: k.len(), + }); + } + if let Some(index) = first_repeat(n) { + return Err(KeriError::DuplicateKey { field: "n", index }); + } + if (n.is_empty() && nt.0 != 0) || (!n.is_empty() && (nt.0 == 0 || nt.0 as usize > n.len())) { + return Err(KeriError::InvalidThreshold { + field: "nt", + threshold: nt.0, + keys: n.len(), + }); + } + Ok(()) +} - fn check_witness_quorum( - &self, - event: &KeyEvent, - bt: Threshold, - receipts: &HashMap>, - witness_quorum_met: &mut bool, - ) -> Result<()> { - if !self.require_witness_quorum { - return Ok(()); +fn first_repeat(items: &[T]) -> Option { + (1..items.len()).find(|&i| items[..i].contains(&items[i])) +} + +/// An inception's witness pool: distinct witnesses, each named by its key. +fn pool(b: &[WitnessAid]) -> Result> { + if let Some(index) = first_repeat(b) { + return Err(KeriError::DuplicateKey { field: "b", index }); + } + for w in b { + if w.key().is_none() { + return Err(KeriError::MalformedKey(format!( + "witness {:?} names no key", + w.0 + ))); } - let rs = receipts.get(&event.said()).cloned().unwrap_or_default(); - let mut distinct_valid = std::collections::BTreeSet::new(); - for r in rs { - if r.verify_signature().is_ok() && r.sequence == event.sequence() { - distinct_valid.insert(r.witness.0.clone()); - } + } + Ok(b.iter().cloned().collect()) +} + +/// The pool after a rotation removes `br` and adds `ba`: every removed +/// witness in the pool, every added one new and named by its key. +fn rotate_pool( + current: &BTreeSet, + br: &[WitnessAid], + ba: &[WitnessAid], +) -> Result> { + let mut out = current.clone(); + for w in br { + if !out.remove(w) { + return Err(KeriError::Malformed(format!( + "br removes {:?}, which is not in the pool", + w.0 + ))); } - if (distinct_valid.len() as u32) < bt.0 { - *witness_quorum_met = false; - return Err(KeriError::WitnessThresholdNotMet { - have: distinct_valid.len() as u32, - need: bt.0, - }); + } + for w in &pool(ba)? { + if !out.insert(w.clone()) { + return Err(KeriError::Malformed(format!( + "ba adds {:?}, which is already in the pool", + w.0 + ))); + } + } + Ok(out) +} + +fn check_witness_threshold(bt: Threshold, witnesses: &BTreeSet) -> Result<()> { + if bt.0 as usize > witnesses.len() { + return Err(KeriError::InvalidThreshold { + field: "bt", + threshold: bt.0, + keys: witnesses.len(), + }); + } + Ok(()) +} + +/// At least `bt` distinct witnesses of the pool have receipted this event — +/// receipts that verify, for this controller, sequence and SAID. +fn check_witness_quorum( + event: &KeyEvent, + state: &State, + receipts: &HashMap>, +) -> Result<()> { + let receipted: BTreeSet<&WitnessAid> = receipts + .get(&event.said()) + .into_iter() + .flatten() + .filter(|r| { + state.witnesses.contains(&r.witness) + && r.controller == *event.controller() + && r.sequence == event.sequence() + && r.signed_event_said == event.said() + && r.verify_signature().is_ok() + }) + .map(|r| &r.witness) + .collect(); + let (have, need) = (receipted.len() as u64, state.witness_threshold.0); + if have < need { + return Err(KeriError::WitnessThresholdNotMet { have, need }); + } + Ok(()) +} + +/// Spec §4 step 6, by the rule every Smart Byte signature follows: each +/// listed `(key index, algorithm byte ‖ signature)` valid over the event's +/// canonical encoding, each key signing once, and at least `threshold` keys +/// signing. +pub(crate) fn verify_event_signatures( + event: &KeyEvent, + keys: &[PublicKey], + threshold: Threshold, + signatures: &[(u32, Vec)], +) -> Result<()> { + let state = KeyState { + keys: keys.iter().map(PrefixedKey::from).collect(), + threshold: threshold.0, + }; + let sigs: Vec = signatures + .iter() + .map(|(index, signature)| IndexedSignature { + index: u64::from(*index), + signature: signature.clone(), + }) + .collect(); + Ok(verify_against(&state, &event.to_cbor()?, &sigs)?) +} + +impl VerificationReport { + /// Record this controller's establishment key states in `book`, so + /// envelope signatures that name them can be verified. + pub fn add_to(&self, book: &mut KeyBook) { + for e in &self.establishments { + book.insert( + self.controller, + e.sequence, + KeyState { + keys: e.keys.iter().map(PrefixedKey::from).collect(), + threshold: e.threshold.0, + }, + ); } - // Use _ on event-type-prefix discriminator - let _ = event.event_type(); - Ok(()) } } -impl EventType { - /// True if the event type is one that changes the signing key set. - #[must_use] - pub fn is_establishment(self) -> bool { - matches!( - self, - Self::Inception | Self::Rotation | Self::Recovery | Self::Delegation +#[cfg(test)] +mod tests { + use super::*; + use crate::controller::{Controller, KeyPair}; + + fn key(n: u8) -> KeyPair { + KeyPair::from_signing(smart_byte_core::SigningKey::from_bytes(&[n; 32])) + } + + fn signed(ctrl: &Controller, event: &KeyEvent) -> SignedEvent { + SignedEvent { + bytes: event.to_cbor().unwrap(), + signatures: ctrl.sign_event(event).unwrap(), + } + } + + #[test] + fn a_log_from_its_wire_form_verifies_and_a_refusal_names_the_event() { + let (mut ctrl, icp) = Controller::incept( + vec![key(1)], + vec![key(2)], + Threshold(1), + Threshold(1), + vec![], + Threshold(0), ) + .unwrap(); + let icp = KeyEvent::Inception(icp); + let mut log = vec![signed(&ctrl, &icp)]; + let rot = KeyEvent::Rotation( + ctrl.rotate(vec![key(3)], Threshold(1), vec![], vec![], Threshold(0)) + .unwrap(), + ); + log.push(signed(&ctrl, &rot)); + let ixn = KeyEvent::Interaction(ctrl.interact(vec![Said::hash(b"anchored")]).unwrap()); + log.push(signed(&ctrl, &ixn)); + + let report = LogVerifier::new() + .verify_encoded(&log, &HashMap::new()) + .unwrap(); + assert_eq!(report.establishments.len(), 2); + assert_eq!((report.last_sequence, report.last_said), (2, ixn.said())); + + // The interaction signed by the pre-rotation key instead. + log[2].signatures = Controller::incept( + vec![key(1)], + vec![], + Threshold(1), + Threshold(0), + vec![], + Threshold(0), + ) + .unwrap() + .0 + .sign_event(&ixn) + .unwrap(); + let refused = LogVerifier::new() + .verify_encoded(&log, &HashMap::new()) + .unwrap_err(); + assert_eq!( + (refused.index, refused.error.class()), + (2, "signature_invalid") + ); + } + + #[test] + fn nothing_rotates_after_an_empty_next_commitment() { + let (mut ctrl, icp) = Controller::incept( + vec![key(1)], + vec![], + Threshold(1), + Threshold(0), + vec![], + Threshold(0), + ) + .unwrap(); + // The controller refuses to build it, so build it by hand: a + // rotation to a key of the attacker's choosing, signed by that key. + assert_eq!( + ctrl.rotate(vec![key(3)], Threshold(1), vec![], vec![], Threshold(0)) + .unwrap_err() + .class(), + "rotation_not_permitted" + ); + let rot = crate::events::finalize_event(KeyEvent::Rotation(crate::events::RotationEvent { + v: VERSION_STRING.into(), + d: Said::default(), + i: icp.i, + s: 1, + p: icp.d, + kt: Threshold(1), + k: vec![key(3).public_key()], + nt: Threshold(0), + n: vec![], + a: vec![], + br: vec![], + ba: vec![], + bt: Threshold(0), + })) + .unwrap(); + let attacker = Controller::incept( + vec![key(3)], + vec![], + Threshold(1), + Threshold(0), + vec![], + Threshold(0), + ) + .unwrap() + .0; + let log = [ + signed(&ctrl, &KeyEvent::Inception(icp)), + signed(&attacker, &rot), + ]; + let refused = LogVerifier::new() + .verify_encoded(&log, &HashMap::new()) + .unwrap_err(); + assert_eq!( + (refused.index, refused.error.class()), + (1, "rotation_not_permitted") + ); + } + + #[test] + fn establishment_key_fields_are_checked() { + let k = |n: u8| key(n).public_key(); + let d = |n: u8| per_key_said(&key(n).public_key()).unwrap(); + let class = |k: &[PublicKey], kt: u64, n: &[Said], nt: u64| { + check_establishment(k, Threshold(kt), n, Threshold(nt)).map_err(|e| e.class()) + }; + assert_eq!(class(&[k(1)], 1, &[d(2)], 1), Ok(())); + assert_eq!(class(&[k(1)], 1, &[], 0), Ok(())); + assert_eq!(class(&[], 0, &[], 0), Err("invalid_threshold")); + assert_eq!(class(&[k(1)], 0, &[d(2)], 1), Err("invalid_threshold")); + assert_eq!(class(&[k(1)], 2, &[d(2)], 1), Err("invalid_threshold")); + assert_eq!(class(&[k(1), k(1)], 2, &[d(2)], 1), Err("duplicate_key")); + assert_eq!(class(&[k(1)], 1, &[d(2), d(2)], 1), Err("duplicate_key")); + assert_eq!(class(&[k(1)], 1, &[], 1), Err("invalid_threshold")); + assert_eq!(class(&[k(1)], 1, &[d(2)], 0), Err("invalid_threshold")); + assert_eq!(class(&[k(1)], 1, &[d(2)], 2), Err("invalid_threshold")); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/watcher.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/watcher.rs index 27e1b286a..4faea9db3 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/watcher.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/watcher.rs @@ -88,7 +88,10 @@ impl Watcher { // Verify every supplied receipt before letting it influence // duplicity state. for r in witness_receipts { - if r.signed_event_said != event.said() || r.sequence != event.sequence() { + if r.signed_event_said != event.said() + || r.sequence != event.sequence() + || r.controller != *event.controller() + { return Err(KeriError::Malformed( "receipt does not match supplied event".into(), )); diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/witness.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/witness.rs index 75bb34af2..229233e20 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/witness.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/src/witness.rs @@ -5,21 +5,21 @@ //! //! 1. Receives an event plus the controller's threshold-met signatures. //! 2. Verifies the controller signatures against the keys in force. -//! 3. If valid, records the event and emits a [`WitnessReceipt`] over -//! the event's SAID. +//! 3. If valid, records the event and emits a [`WitnessReceipt`]: its +//! signature over the controller, the sequence number and the event's +//! SAID. //! 4. Refuses to issue a contradicting receipt for the same //! `(controller, sequence)` — duplicity defence. //! -//! Witness signatures are themselves Ed25519. A future revision may -//! upgrade witnesses to hybrid (Ed25519 + ML-DSA) via the same -//! `smart_byte_pq::Signer` plumbing used elsewhere in the substrate. +//! A witness is named by its Ed25519 key ([`WitnessAid::of_key`]), and a +//! receipt verifies only under the key its witness's name spells, so no one +//! can receipt in another witness's name. use async_trait::async_trait; use chrono::{DateTime, Utc}; use dashmap::DashMap; -use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use serde::{Deserialize, Serialize}; -use smart_byte_core::Said; +use smart_byte_core::{Said, wire}; use crate::controller::KeyPair; use crate::error::{KeriError, Result}; @@ -29,56 +29,58 @@ use crate::events::{ControllerAid, KeyEvent, PublicKey, Threshold, WitnessAid}; /// given event at the given sequence. #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct WitnessReceipt { - /// Witness AID that emitted this receipt. + /// The witness that emitted this receipt, named by its key. pub witness: WitnessAid, /// Controller AID the receipted event belongs to. pub controller: ControllerAid, /// Sequence number of the receipted event. pub sequence: u64, - /// SAID of the receipted event body. + /// SAID of the receipted event. pub signed_event_said: Said, - /// Wall-clock timestamp at which the receipt was emitted. + /// When the receipt was emitted. Not signed: a witness attests to what + /// it saw, not to when. pub timestamp: DateTime, - /// Witness's Ed25519 verifying key (so verifiers can validate the receipt). - #[serde(with = "serde_bytes")] - pub witness_pubkey: Vec, - /// Witness's Ed25519 signature over the SAID bytes. + /// The witness's Ed25519 signature over [`WitnessReceipt::signed_bytes`]. #[serde(with = "serde_bytes")] pub signature: Vec, } +/// What a witness signs: the canonical encoding of the controller, the +/// sequence number and the event's SAID. +#[derive(Serialize)] +struct ReceiptBody<'a> { + controller: &'a ControllerAid, + event: &'a Said, + sequence: u64, +} + impl WitnessReceipt { - /// Cryptographically verify the witness's signature against the - /// receipted event SAID. + /// The message the witness signs. + pub fn signed_bytes(&self) -> Vec { + wire::to_vec(&ReceiptBody { + controller: &self.controller, + event: &self.signed_event_said, + sequence: self.sequence, + }) + .expect("a receipt body always encodes") + } + + /// Verify the witness's signature under the key its name spells. pub fn verify_signature(&self) -> Result<()> { - let mut pk_arr = [0u8; 32]; - if self.witness_pubkey.len() != 32 { - return Err(KeriError::MalformedKey(format!( - "witness pubkey must be 32 bytes, got {}", - self.witness_pubkey.len() - ))); - } - pk_arr.copy_from_slice(&self.witness_pubkey); - let vk = VerifyingKey::from_bytes(&pk_arr) - .map_err(|e| KeriError::MalformedKey(e.to_string()))?; - if self.signature.len() != 64 { - return Err(KeriError::MalformedKey(format!( - "witness signature must be 64 bytes, got {}", - self.signature.len() - ))); - } - let mut sig_arr = [0u8; 64]; - sig_arr.copy_from_slice(&self.signature); - let sig = Signature::from_bytes(&sig_arr); - vk.verify(self.signed_event_said.as_bytes(), &sig) - .map_err(|_| KeriError::BadSignature) + let key = self + .witness + .key() + .ok_or_else(|| KeriError::MalformedKey(format!("witness {:?} names no key", self.witness.0)))?; + smart_byte_core::sign::verify(&key, &self.signed_bytes(), &self.signature).map_err(|e| match e { + smart_byte_core::SignatureError::SignatureInvalid => KeriError::BadSignature, + other => KeriError::MalformedKey(other.to_string()), + }) } } /// A witness peer. pub struct Witness { - /// Witness AID — typically the base32 of the witness pubkey - /// prefixed with `W`, but treated opaquely by this crate. + /// Witness AID: its key's name ([`WitnessAid::of_key`]). pub aid: WitnessAid, /// Witness signing key. pub key: KeyPair, @@ -88,12 +90,11 @@ pub struct Witness { } impl Witness { - /// Construct a fresh witness wrapping the given AID and Ed25519 - /// key pair. + /// A witness holding `key`, named by it. #[must_use] - pub fn new(aid: WitnessAid, key: KeyPair) -> Self { + pub fn new(key: KeyPair) -> Self { Self { - aid, + aid: WitnessAid::of_key(&key.verifying), key, observed_logs: DashMap::new(), } @@ -107,54 +108,27 @@ impl Witness { /// the time the event was authored (the inception event's own /// `k` for inception, or the most recent establishment event's /// `k` for everything else). `threshold` is the matching `kt`. - /// - /// Witness signatures are NOT computed over an algorithm byte; - /// they are computed over the raw 32-byte SAID, mirroring the - /// behaviour of `smart_byte_core::sign`. pub async fn receive( &self, event: KeyEvent, - controller_signatures: &[(u32, Signature)], + controller_signatures: &[(u32, Vec)], controller_keys_in_force: &[PublicKey], threshold: Threshold, ) -> Result { // 1. SAID consistency. event.validate_said()?; - // 2. Controller-signature threshold. - let body = event.to_cbor()?; - let mut valid_distinct = std::collections::BTreeSet::new(); - for (idx, sig) in controller_signatures { - let pk = controller_keys_in_force.get(*idx as usize).ok_or_else(|| { - KeriError::Malformed(format!("signature references unknown key index {idx}")) - })?; - // v1 only validates Ed25519-prefix keys at the witness layer; - // PQ keys would be routed through smart_byte_pq here. - if pk.algorithm() != Some(0x01) { - continue; - } - let pk_bytes = pk.key_bytes(); - if pk_bytes.len() != 32 { - continue; - } - let mut pk_arr = [0u8; 32]; - pk_arr.copy_from_slice(pk_bytes); - let vk = VerifyingKey::from_bytes(&pk_arr) - .map_err(|e| KeriError::MalformedKey(e.to_string()))?; - if vk.verify(&body, sig).is_ok() { - valid_distinct.insert(*idx); - } - } - if (valid_distinct.len() as u32) < threshold.0 { - return Err(KeriError::ThresholdNotMet { - have: valid_distinct.len() as u32, - need: threshold.0, - }); - } + // 2. Controller signatures, by the verifier's rule. + crate::verifier::verify_event_signatures( + &event, + controller_keys_in_force, + threshold, + controller_signatures, + )?; // 3. Duplicity defence — refuse to receipt a different event at // the same (controller, sequence). - let controller = event.controller().clone(); + let controller = *event.controller(); let sequence = event.sequence(); let said = event.said(); if let Some(existing) = self.observed_logs.get(&controller) @@ -165,18 +139,18 @@ impl Witness { } // 4. Record + sign. - self.observed_logs.entry(controller.clone()).or_default().push(event); + self.observed_logs.entry(controller).or_default().push(event); - let sig = self.key.sign(said.as_bytes()); - Ok(WitnessReceipt { + let mut receipt = WitnessReceipt { witness: self.aid.clone(), controller, sequence, signed_event_said: said, timestamp: Utc::now(), - witness_pubkey: self.key.verifying.as_bytes().to_vec(), - signature: sig.to_bytes().to_vec(), - }) + signature: Vec::new(), + }; + receipt.signature = self.key.sign(&receipt.signed_bytes()).to_bytes().to_vec(); + Ok(receipt) } } @@ -189,7 +163,7 @@ pub trait WitnessClient: Send + Sync { async fn push( &self, event: KeyEvent, - controller_signatures: &[(u32, Signature)], + controller_signatures: &[(u32, Vec)], controller_keys_in_force: &[PublicKey], threshold: Threshold, ) -> Result; @@ -206,7 +180,7 @@ impl WitnessClient for InProcessWitnessClient { async fn push( &self, event: KeyEvent, - controller_signatures: &[(u32, Signature)], + controller_signatures: &[(u32, Vec)], controller_keys_in_force: &[PublicKey], threshold: Threshold, ) -> Result { @@ -225,8 +199,8 @@ mod tests { async fn witness_signs_well_formed_event() { let mut rng = OsRng; let kp = KeyPair::generate(&mut rng); - let w_aid = WitnessAid("W1".into()); - let w = Witness::new(w_aid.clone(), KeyPair::generate(&mut rng)); + let w = Witness::new(KeyPair::generate(&mut rng)); + let w_aid = w.aid.clone(); let (mut ctrl, icp) = crate::controller::Controller::incept( vec![kp.clone()], @@ -247,5 +221,17 @@ mod tests { .expect("receipt"); receipt.verify_signature().expect("receipt verify"); let _ = &mut ctrl; // silence unused-mut + + // Every field but the timestamp is signed. + let mut moved = receipt.clone(); + moved.sequence += 1; + assert!(moved.verify_signature().is_err()); + // A receipt in another witness's name does not verify. + let mut impostor = receipt.clone(); + impostor.witness = WitnessAid::of_key(&KeyPair::generate(&mut rng).verifying); + assert!(impostor.verify_signature().is_err()); + let mut later = receipt; + later.timestamp = Utc::now(); + later.verify_signature().expect("the timestamp is not attested"); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/tests/integration.rs b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/tests/integration.rs index d73798bf8..98af2d978 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/tests/integration.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-keri-witness/tests/integration.rs @@ -8,12 +8,13 @@ use proptest::prelude::*; use rand::rngs::OsRng; use smart_byte_core::Said; -use smart_byte_keri_witness::{ - Anchor, Controller, ControllerAid, KeyEvent, KeyPair, LogVerifier, MemoryStorage, Threshold, - Watcher, Witness, WitnessAid, WitnessReceipt, -}; +use smart_byte_keri_witness::events::finalize_event; use smart_byte_keri_witness::storage::EventLogStorage; use smart_byte_keri_witness::WatcherAid; +use smart_byte_keri_witness::{ + Controller, KeyEvent, KeyPair, LogVerifier, MemoryStorage, Threshold, Watcher, Witness, + WitnessReceipt, +}; fn sign_all_into_map( controller: &Controller, @@ -23,7 +24,7 @@ fn sign_all_into_map( let sigs = controller.sign_event(event).expect("sign"); let entry = signatures_out.entry(event.said()).or_default(); for (i, sig) in sigs { - entry.push((i, sig.to_bytes().to_vec())); + entry.push((i, sig)); } } @@ -75,11 +76,7 @@ async fn inception_three_rotations_five_interactions_verify() { let mut interactions = Vec::new(); for i in 0..5 { - let anchor = Anchor { - d: Said::hash(format!("payload-{i}").as_bytes()), - s: Some(i as u64), - kind: Some("test".into()), - }; + let anchor = Said::hash(format!("payload-{i}").as_bytes()); let ixn = ctrl.interact(vec![anchor]).expect("interact"); let ev = KeyEvent::Interaction(ixn); sign_all_into_map(&ctrl, &ev, &mut sigs); @@ -122,35 +119,28 @@ async fn pre_rotation_mismatch_is_rejected() { .expect("rot"); let mut rot_event = KeyEvent::Rotation(rot); - // Tamper: swap `k` to a fresh key the previous `n` did not commit to. + // Tamper: swap `k` to a fresh key the previous `n` did not commit to, + // and re-stamp the SAID so only the pre-rotation check can catch it. + let evil = KeyPair::generate(&mut rng); if let KeyEvent::Rotation(ref mut r) = rot_event { - let evil = KeyPair::generate(&mut rng); r.k = vec![evil.public_key()]; - // Recompute SAID so SAID consistency passes — what we want to - // demonstrate is that the *pre-rotation* check fires. - let placeholder = Said::default(); - r.d = placeholder; - let bytes = serde_cbor::to_vec(&KeyEvent::Rotation(r.clone())).expect("cbor"); - r.d = Said::hash(&bytes); } - // Replace prior signature entry: still won't matter — pre-rotation - // check fires before signatures. - sign_all_into_map(&ctrl, &rot_event, &mut sigs); + let rot_event = finalize_event(rot_event).expect("restamp"); + let evil_ctrl = Controller::incept(vec![evil], vec![], Threshold(1), Threshold(0), vec![], Threshold(0)) + .expect("signer") + .0; + sign_all_into_map(&evil_ctrl, &rot_event, &mut sigs); let log = vec![icp_event, rot_event]; let receipts: HashMap> = HashMap::new(); let err = LogVerifier::new().verify(&log, &sigs, &receipts).unwrap_err(); - matches!( - err, - smart_byte_keri_witness::KeriError::PreRotationMismatch { .. } - ); + assert_eq!(err.class(), "pre_rotation_mismatch", "{err}"); } #[tokio::test] async fn witness_signs_and_verifier_accepts_quorum() { let mut rng = OsRng; - let w_key = KeyPair::generate(&mut rng); - let witness = Witness::new(WitnessAid("W1".into()), w_key); + let witness = Witness::new(KeyPair::generate(&mut rng)); let k0 = KeyPair::generate(&mut rng); let k1 = KeyPair::generate(&mut rng); @@ -175,25 +165,43 @@ async fn witness_signs_and_verifier_accepts_quorum() { let mut sigs: HashMap)>> = HashMap::new(); let entry = sigs.entry(event.said()).or_default(); - for (i, s) in raw_sigs { - entry.push((i, s.to_bytes().to_vec())); + for (i, s) in raw_sigs.clone() { + entry.push((i, s)); } let mut receipts: HashMap> = HashMap::new(); receipts.insert(event.said(), vec![receipt]); let log = vec![event]; let report = LogVerifier::new() + .strict(false) .require_witness_quorum(true) .verify(&log, &sigs, &receipts) .expect("verify ok with witness quorum"); assert!(report.witness_quorum_met); + + // A witness pool is an extension: a spec-v1 verifier refuses it. + let refused = LogVerifier::new().verify(&log, &sigs, &receipts).unwrap_err(); + assert_eq!(refused.class(), "malformed_event"); + + // A receipt from a witness outside the pool does not count. + let outsider = Witness::new(KeyPair::generate(&mut rng)); + let stray = outsider + .receive(log[0].clone(), &raw_sigs, &keys_in_force, Threshold(1)) + .await + .expect("an outsider can sign"); + receipts.insert(log[0].said(), vec![stray]); + let refused = LogVerifier::new() + .strict(false) + .require_witness_quorum(true) + .verify(&log, &sigs, &receipts) + .unwrap_err(); + assert_eq!(refused.class(), "witness_threshold_not_met"); } #[tokio::test] async fn witness_refuses_contradicting_receipt_and_watcher_detects() { let mut rng = OsRng; - let w_key = KeyPair::generate(&mut rng); - let witness = Witness::new(WitnessAid("W1".into()), w_key); + let witness = Witness::new(KeyPair::generate(&mut rng)); let k0 = KeyPair::generate(&mut rng); let k1a = KeyPair::generate(&mut rng); @@ -290,7 +298,7 @@ async fn witness_refuses_contradicting_receipt_and_watcher_detects() { .expect("observe-a"); // Forge a watcher-side receipt for event_b so the watcher's // receipt-verify check passes; we model a colluding witness here. - let forged_witness = Witness::new(WitnessAid("W-COLLUDE".into()), KeyPair::generate(&mut rng)); + let forged_witness = Witness::new(KeyPair::generate(&mut rng)); let receipt_b = forged_witness .receive(event_b.clone(), &sigs_b, &rot_b.k, Threshold(1)) .await @@ -322,6 +330,7 @@ fn clone_controller(src: &Controller) -> Controller { #[tokio::test] async fn recovery_resets_key_set() { let mut rng = OsRng; + let witness = Witness::new(KeyPair::generate(&mut rng)); let k0 = KeyPair::generate(&mut rng); let k1 = KeyPair::generate(&mut rng); let (mut ctrl, icp) = Controller::incept( @@ -329,8 +338,8 @@ async fn recovery_resets_key_set() { vec![k1.clone()], Threshold(1), Threshold(1), - vec![], - Threshold(0), + vec![witness.aid.clone()], + Threshold(1), ) .expect("incept"); @@ -350,27 +359,62 @@ async fn recovery_resets_key_set() { vec![], ) .expect("recover"); - let rec_event = KeyEvent::Recovery(rec); + let rec_event = KeyEvent::Recovery(rec.clone()); sign_all_into_map(&ctrl, &rec_event, &mut sigs); - let log = ctrl.key_event_log.clone(); - let receipts: HashMap> = HashMap::new(); + + // Unwitnessed, the recovery is anyone's: the keys it reveals were never + // committed to. + let mut receipts: HashMap> = HashMap::new(); + let refused = LogVerifier::new() + .strict(false) + .verify(&log, &sigs, &receipts) + .unwrap_err(); + assert_eq!(refused.class(), "witness_threshold_not_met"); + + let receipt = witness + .receive(rec_event.clone(), &sigs[&rec_event.said()], &rec.k, Threshold(1)) + .await + .expect("the witness receipts the recovery"); + receipts.insert(rec_event.said(), vec![receipt]); let report = LogVerifier::new() .strict(false) .verify(&log, &sigs, &receipts) .expect("recovery accepted"); assert_eq!(report.recovery_count, 1); assert_eq!(report.last_sequence, 1); +} - // Strict mode rejects. - let strict_err = LogVerifier::new() - .strict(true) - .verify(&log, &sigs, &receipts) +#[tokio::test] +async fn a_log_without_witnesses_cannot_recover() { + let mut rng = OsRng; + let (mut ctrl, icp) = Controller::incept( + vec![KeyPair::generate(&mut rng)], + vec![KeyPair::generate(&mut rng)], + Threshold(1), + Threshold(1), + vec![], + Threshold(0), + ) + .expect("incept"); + let mut sigs: HashMap)>> = HashMap::new(); + sign_all_into_map(&ctrl, &KeyEvent::Inception(icp), &mut sigs); + // An attacker with keys of their own "recovers" the identity. + let rec = ctrl + .recover(vec![KeyPair::generate(&mut rng)], vec![KeyPair::generate(&mut rng)], Threshold(1), Threshold(1), vec![]) + .expect("recover"); + sign_all_into_map(&ctrl, &KeyEvent::Recovery(rec), &mut sigs); + let refused = LogVerifier::new() + .strict(false) + .verify(&ctrl.key_event_log, &sigs, &HashMap::new()) .unwrap_err(); - assert!(matches!( - strict_err, - smart_byte_keri_witness::KeriError::StrictSpecRejectsRec - )); + assert_eq!(refused.class(), "recovery_rejected"); + + // Spec v1 reserves `rec`. + let refused = LogVerifier::new() + .verify(&ctrl.key_event_log, &sigs, &HashMap::new()) + .unwrap_err(); + assert_eq!(refused.class(), "unsupported_event_type"); } #[tokio::test] @@ -406,9 +450,15 @@ async fn delegation_child_anchors_to_parent() { let log = child.key_event_log.clone(); let receipts: HashMap> = HashMap::new(); let report = LogVerifier::new() + .strict(false) .verify(&log, &sigs, &receipts) .expect("delegated verify ok"); assert_eq!(report.last_sequence, 0); + assert_eq!(report.delegator, Some(parent.aid)); + + // Delegation is an extension: a spec-v1 verifier refuses it. + let refused = LogVerifier::new().verify(&log, &sigs, &receipts).unwrap_err(); + assert_eq!(refused.class(), "unsupported_event_type"); } #[tokio::test] @@ -433,7 +483,6 @@ async fn memory_storage_round_trip() { assert_eq!(fetched.len(), 1); assert_eq!(fetched[0].said(), event.said()); - let _ = ControllerAid("ignored".into()); // suppress unused-import warnings } proptest! { @@ -464,11 +513,7 @@ proptest! { sign_all_into_map(&ctrl, &ev, &mut sigs); } for i in 0..extra_interactions { - let ixn = ctrl.interact(vec![Anchor { - d: Said::hash(format!("a{i}").as_bytes()), - s: None, - kind: None, - }]).unwrap(); + let ixn = ctrl.interact(vec![Said::hash(format!("a{i}").as_bytes())]).unwrap(); let ev = KeyEvent::Interaction(ixn); sign_all_into_map(&ctrl, &ev, &mut sigs); } @@ -494,15 +539,11 @@ proptest! { let mut sigs: HashMap)>> = HashMap::new(); let icp_event = KeyEvent::Inception(icp); sign_all_into_map(&ctrl, &icp_event, &mut sigs); - let ixn = ctrl.interact(vec![Anchor { - d: Said::hash(format!("seed{seed}").as_bytes()), - s: None, - kind: None, - }]).unwrap(); + let ixn = ctrl.interact(vec![Said::hash(format!("seed{seed}").as_bytes())]).unwrap(); let mut ev = KeyEvent::Interaction(ixn); sign_all_into_map(&ctrl, &ev, &mut sigs); if let KeyEvent::Interaction(ref mut i) = ev { - i.a = vec![Anchor { d: Said::hash(b"tampered"), s: None, kind: None }]; + i.a = vec![Said::hash(b"tampered")]; } let mut log = ctrl.key_event_log.clone(); let last_idx = log.len() - 1; diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/src/codec.rs b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/src/codec.rs index d89995dcb..5ba9cf6e2 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/src/codec.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/src/codec.rs @@ -88,7 +88,7 @@ impl Codec for EnvelopeCodec { T: AsyncRead + Unpin + Send, { let bytes = read_frame(io).await?; - serde_cbor::from_slice(&bytes) + smart_byte_core::wire::from_slice(&bytes) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) } @@ -101,7 +101,7 @@ impl Codec for EnvelopeCodec { T: AsyncRead + Unpin + Send, { let bytes = read_frame(io).await?; - serde_cbor::from_slice(&bytes) + smart_byte_core::wire::from_slice(&bytes) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string())) } @@ -114,7 +114,7 @@ impl Codec for EnvelopeCodec { where T: AsyncWrite + Unpin + Send, { - let bytes = serde_cbor::to_vec(&req) + let bytes = smart_byte_core::wire::to_vec(&req) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; write_frame(io, &bytes).await } @@ -128,7 +128,7 @@ impl Codec for EnvelopeCodec { where T: AsyncWrite + Unpin + Send, { - let bytes = serde_cbor::to_vec(&resp) + let bytes = smart_byte_core::wire::to_vec(&resp) .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?; write_frame(io, &bytes).await } @@ -137,21 +137,22 @@ impl Codec for EnvelopeCodec { #[cfg(test)] mod tests { use super::*; - use chrono::TimeZone; use futures::io::Cursor; - use smart_byte_core::{Cargo, JouleCost, OwnershipChain, Provenance}; + use smart_byte_core::{Aid, Cargo, JouleCost, Signer, SigningKey}; fn fixture_envelope() -> Envelope { - let issuer = Said::hash(b"issuer-codec"); - let issued_at = chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap(); - let prov = Provenance::new(issuer, issued_at, vec![]); - Envelope::new( - prov, - OwnershipChain::empty(), - Cargo::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]), + let issuer = Signer::single( + Aid::of_inception(Said::hash(b"issuer-codec")), + 0, + SigningKey::from_bytes(&[0x77; 32]), + ); + Envelope::issue( + &issuer, + Aid::of_inception(Said::hash(b"holder-codec")), + [7; 16], + Cargo::opaque(vec![0xDE, 0xAD, 0xBE, 0xEF]), JouleCost::measured(7), ) - .expect("envelope") } #[tokio::test] @@ -159,7 +160,7 @@ mod tests { let proto = StreamProtocol::new("/smart-byte/envelope/1.0.0"); let mut codec = EnvelopeCodec; let env = fixture_envelope(); - let req = EnvelopeRequest { said: env.id }; + let req = EnvelopeRequest { said: env.said() }; let mut buf = Vec::new(); { @@ -195,7 +196,7 @@ mod tests { let got = codec.read_response(&proto, &mut r).await.expect("read"); assert_eq!(got, resp); let back = got.envelope.expect("envelope"); - back.verify_said().expect("said still valid"); + back.check_saids().expect("SAIDs still check"); } #[tokio::test] diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/src/transport.rs b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/src/transport.rs index baad08ba8..2f9a2bf5e 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/src/transport.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/src/transport.rs @@ -39,15 +39,15 @@ use crate::codec::{EnvelopeRequest, EnvelopeResponse}; use crate::error::{Error, Result}; use crate::node::Libp2pNode; -/// Decode an envelope received from the network, accepting it only if the -/// bytes are its own encoding and its content hashes to the id it states -/// (treatise §10.2 step 1). +/// Decode an envelope received from the network: canonical bytes, no member +/// the envelope does not define, and both SAIDs — the snapshot's and the +/// byte's identity — checking (identity spec §2.6; treatise §10.2 step 1). +/// Signatures need the controllers' key states; the application checks them +/// with [`Envelope::verify`]. pub(crate) fn accept_envelope( bytes: &[u8], ) -> std::result::Result { - let env = Envelope::from_cbor(bytes)?; - env.verify_said()?; - Ok(env) + Envelope::from_cbor(bytes) } /// Rule on one received gossip message. @@ -98,18 +98,18 @@ pub(crate) fn settle_if_gossip( } } -/// Accept a fetch's answer only if it is the envelope that was asked for: it -/// must state the requested SAID, and its content must hash to it. Otherwise a +/// Accept a fetch's answer only if it is the snapshot that was asked for: it +/// must state the requested SAID, and both its SAIDs must check. Otherwise a /// peer could answer any request with any envelope it liked. fn check_served(peer: PeerId, requested: Said, envelope: Envelope) -> Result { - if envelope.id != requested { + if envelope.said() != requested { return Err(Error::Substituted { peer, requested, - served: envelope.id, + served: envelope.said(), }); } - envelope.verify_said()?; + envelope.check_saids()?; Ok(envelope) } @@ -129,7 +129,7 @@ pub async fn publish_envelope( .subscribe(&topic_obj) .map_err(|e| Error::Gossipsub(e.to_string()))?; - let bytes = env.to_cbor().map_err(Error::Envelope)?; + let bytes = env.to_cbor(); let id = node .swarm .behaviour_mut() @@ -400,58 +400,60 @@ async fn find_provider(node: &mut Libp2pNode, said: Said) -> Result { #[cfg(test)] mod tests { use super::*; - use chrono::TimeZone; - use smart_byte_core::{Cargo, EnvelopeError, JouleCost, OwnershipChain, Provenance}; + use smart_byte_core::{Aid, Cargo, EnvelopeError, JouleCost, Signer, SigningKey}; - fn envelope(seed: &[u8]) -> Envelope { - let issued_at = chrono::Utc.with_ymd_and_hms(2026, 9, 30, 0, 0, 0).unwrap(); - Envelope::new( - Provenance::new(Said::hash(seed), issued_at, vec![]), - OwnershipChain::empty(), - Cargo::Usd { minor: 100 }, + fn envelope(seed: u8) -> Envelope { + let issuer = Signer::single( + Aid::of_inception(Said::hash(&[seed])), + 0, + SigningKey::from_bytes(&[seed; 32]), + ); + Envelope::issue( + &issuer, + Aid::of_inception(Said::hash(b"holder")), + [seed; 16], + Cargo::usd_micros(100), JouleCost::measured(1), ) - .unwrap() } #[test] - fn a_received_envelope_must_hash_to_its_own_id() { - let genuine = envelope(b"issuer"); - assert_eq!( - accept_envelope(&genuine.to_cbor().unwrap()).unwrap(), - genuine - ); + fn a_received_envelope_must_check_both_its_saids() { + let genuine = envelope(1); + assert_eq!(accept_envelope(&genuine.to_cbor()).unwrap(), genuine); - // Same id, different content: refused rather than delivered under it. + // The cargo changed after issuance, the snapshot SAID re-stamped to + // match: the identity no longer checks. let mut forged = genuine.clone(); - forged.cargo = Cargo::Usd { minor: 1_000_000 }; + forged.provenance.cargo = Cargo::usd_micros(1_000_000); + forged.d = forged.compute_said(); assert!(matches!( - accept_envelope(&forged.to_cbor().unwrap()), - Err(EnvelopeError::SaidMismatch { .. }) + accept_envelope(&forged.to_cbor()), + Err(EnvelopeError::Said { part: "provenance", .. }) )); } #[test] fn a_fetch_answered_with_another_envelope_is_refused() { let peer = PeerId::random(); - let wanted = envelope(b"wanted"); - let other = envelope(b"other"); + let wanted = envelope(1); + let other = envelope(2); assert_eq!( - check_served(peer, wanted.id, wanted.clone()).unwrap(), + check_served(peer, wanted.said(), wanted.clone()).unwrap(), wanted ); assert!(matches!( - check_served(peer, wanted.id, other.clone()), + check_served(peer, wanted.said(), other.clone()), Err(Error::Substituted { requested, served, .. }) - if requested == wanted.id && served == other.id + if requested == wanted.said() && served == other.said() )); - // Stating the requested id is not enough: the content must hash to it. + // Stating the requested SAID is not enough: it must check. let mut relabelled = other; - relabelled.id = wanted.id; + relabelled.d = wanted.said(); assert!(matches!( - check_served(peer, wanted.id, relabelled), - Err(Error::Envelope(EnvelopeError::SaidMismatch { .. })) + check_served(peer, wanted.said(), relabelled), + Err(Error::Envelope(EnvelopeError::Said { part: "envelope", .. })) )); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/gossip_validation.rs b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/gossip_validation.rs index 826496741..3b5c945c2 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/gossip_validation.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/gossip_validation.rs @@ -8,11 +8,10 @@ use std::time::Duration; -use chrono::TimeZone; use libp2p::futures::StreamExt; use libp2p::gossipsub::{self, IdentTopic}; use libp2p::swarm::SwarmEvent; -use smart_byte_core::{Cargo, Envelope, JouleCost, OwnershipChain, Provenance, Said}; +use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, Said, Signer, SigningKey}; use smart_byte_libp2p::behaviour::{SmartByteBehaviourEvent, default_gossipsub_config}; use smart_byte_libp2p::transport::{settle_gossip, subscribe_topic}; use smart_byte_libp2p::{Libp2pNode, NodeConfig, NodeEvent, run}; @@ -83,20 +82,30 @@ async fn a_forged_envelope_is_not_relayed() { "A and C must not be directly connected" ); - let issued = chrono::Utc.with_ymd_and_hms(2026, 9, 30, 0, 0, 0).unwrap(); - let genuine = Envelope::new( - Provenance::new(Said::hash(b"issuer"), issued, vec![]), - OwnershipChain::empty(), - Cargo::Usd { minor: 100 }, + let issuer = Signer::single( + Aid::of_inception(Said::hash(b"issuer")), + 0, + SigningKey::from_bytes(&[0x24; 32]), + ); + let genuine = Envelope::issue( + &issuer, + Aid::of_inception(Said::hash(b"holder")), + [1; 16], + Cargo::usd_micros(100), JouleCost::measured(1), - ) - .unwrap(); + ); + // The cargo inflated after issuance, the snapshot SAID re-stamped so only + // the identity gives it away. let mut forged = genuine.clone(); - forged.cargo = Cargo::Usd { minor: 1_000_000 }; - let forged = forged.to_cbor().unwrap(); - let genuine_bytes = genuine.to_cbor().unwrap(); + forged.provenance.cargo = Cargo::usd_micros(1_000_000); + forged.d = forged.compute_said(); + let forged = forged.to_cbor(); + let genuine_bytes = genuine.to_cbor(); + // The genuine envelope with an extra member appended: a map of five + // members where the envelope defines four. let mut padded = genuine_bytes.clone(); - padded[0] = 0xa6; + assert_eq!(padded[0], 0xa4, "a four-member map"); + padded[0] = 0xa5; padded.extend_from_slice(&[0x61, b'x', 0x00]); let gossip = &mut a.swarm.behaviour_mut().gossipsub; diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/req_resp_fetch.rs b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/req_resp_fetch.rs index 6e0c5e18b..949637f68 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/req_resp_fetch.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/req_resp_fetch.rs @@ -3,27 +3,28 @@ use std::time::Duration; -use chrono::TimeZone; use libp2p::futures::StreamExt; use libp2p::request_response::{self, Message}; use libp2p::swarm::SwarmEvent; -use smart_byte_core::{Cargo, Envelope, JouleCost, OwnershipChain, Provenance, Said}; +use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, Said, Signer, SigningKey}; use smart_byte_libp2p::{ EnvelopeResponse, Libp2pNode, NodeConfig, SmartByteBehaviourEvent, behaviour::default_gossipsub_config, }; fn fixture_envelope() -> Envelope { - let issuer = Said::hash(b"req-resp"); - let issued_at = chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap(); - let prov = Provenance::new(issuer, issued_at, vec![]); - Envelope::new( - prov, - OwnershipChain::empty(), - Cargo::Bytes(b"reqresp-payload".to_vec()), + let issuer = Signer::single( + Aid::of_inception(Said::hash(b"req-resp")), + 0, + SigningKey::from_bytes(&[0x42; 32]), + ); + Envelope::issue( + &issuer, + Aid::of_inception(Said::hash(b"holder")), + [1; 16], + Cargo::opaque(b"reqresp-payload".to_vec()), JouleCost::measured(3), ) - .expect("envelope") } async fn spawn_node() -> Libp2pNode { @@ -70,7 +71,7 @@ async fn request_response_fetch() { // - drive A's swarm and read the response. // - drive B's swarm and serve the response when the inbound // request arrives. - let req = smart_byte_libp2p::EnvelopeRequest { said: env.id }; + let req = smart_byte_libp2p::EnvelopeRequest { said: env.said() }; let request_id = node_a .swarm .behaviour_mut() @@ -112,6 +113,6 @@ async fn request_response_fetch() { .expect("not timed out"); let env_back = result.envelope.expect("envelope returned"); - assert_eq!(env_back.id, env.id); - env_back.verify_said().expect("said valid"); + assert_eq!(env_back.said(), env.said()); + env_back.check_saids().expect("SAIDs check"); } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/two_node_pubsub.rs b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/two_node_pubsub.rs index d5866f595..18ae6bcad 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/two_node_pubsub.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-libp2p/tests/two_node_pubsub.rs @@ -2,10 +2,9 @@ use std::time::Duration; -use chrono::TimeZone; use libp2p::futures::StreamExt; use libp2p::swarm::SwarmEvent; -use smart_byte_core::{Cargo, Envelope, JouleCost, OwnershipChain, Provenance, Said}; +use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, Said, Signer, SigningKey}; use smart_byte_libp2p::{ Libp2pNode, NodeConfig, behaviour::default_gossipsub_config, @@ -13,16 +12,18 @@ use smart_byte_libp2p::{ }; fn fixture_envelope(seed: &str) -> Envelope { - let issuer = Said::hash(seed.as_bytes()); - let issued_at = chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap(); - let prov = Provenance::new(issuer, issued_at, vec![]); - Envelope::new( - prov, - OwnershipChain::empty(), - Cargo::Bytes(seed.as_bytes().to_vec()), + let issuer = Signer::single( + Aid::of_inception(Said::hash(seed.as_bytes())), + 0, + SigningKey::from_bytes(&[0x42; 32]), + ); + Envelope::issue( + &issuer, + Aid::of_inception(Said::hash(b"holder")), + [1; 16], + Cargo::opaque(seed.as_bytes().to_vec()), JouleCost::measured(11), ) - .expect("envelope") } async fn spawn_node() -> Libp2pNode { @@ -91,6 +92,6 @@ async fn two_nodes_pubsub_envelope() { .expect("not timed out") .expect("received envelope"); let (received_env, from_peer) = received; - assert_eq!(received_env.id, env.id); + assert_eq!(received_env.said(), env.said()); assert_eq!(from_peer, node_a.local_peer_id); } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/Cargo.toml b/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/Cargo.toml index 12423d672..31eb12d11 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/Cargo.toml +++ b/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/Cargo.toml @@ -12,5 +12,4 @@ smart-byte-core = { workspace = true } tokio = { workspace = true } blake3 = { workspace = true } serde = { workspace = true } -serde_cbor = { workspace = true } thiserror = { workspace = true } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/src/lib.rs b/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/src/lib.rs index 9719cb2a4..f02d3d2a7 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/src/lib.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-lockstep/src/lib.rs @@ -19,7 +19,7 @@ use std::collections::{BTreeMap, HashSet}; use serde::{Deserialize, Serialize}; -use smart_byte_core::{Blake3Hash, Said, ownership::Transition}; +use smart_byte_core::{Aid, Blake3Hash, Said, Signer, SigningKey, ownership::Transition}; /// Errors during a frame step. #[derive(Debug, thiserror::Error)] @@ -255,7 +255,7 @@ impl Node { hasher.update(&self.state); hasher.update(&frame.frame_number.to_le_bytes()); for t in &frame.ordered_transitions { - let bytes = serde_cbor::to_vec(t).expect("Transition CBOR is infallible"); + let bytes = smart_byte_core::wire::to_vec(t).expect("a transition always encodes"); hasher.update(&bytes); } let honest = *hasher.finalize().as_bytes(); @@ -365,14 +365,15 @@ impl Cluster { pub fn synthetic_transitions(frame_index: u64, count: usize) -> Vec { (0..count) .map(|i| { - let from = Said::hash(format!("from-{frame_index}-{i}").as_bytes()); - let to = Said::hash(format!("to-{frame_index}-{i}").as_bytes()); - let prior = if i == 0 { - None - } else { - Some(Said::hash(format!("prior-{frame_index}-{i}").as_bytes()).0) - }; - Transition::unsigned(from, to, prior) + let seed = *blake3::hash(format!("from-{frame_index}-{i}").as_bytes()).as_bytes(); + let owner = Signer::single( + Aid::of_inception(Said::hash(&seed)), + 0, + SigningKey::from_bytes(&seed), + ); + let to = Aid::of_inception(Said::hash(format!("to-{frame_index}-{i}").as_bytes())); + let byte = Said::hash(format!("byte-{frame_index}-{i}").as_bytes()); + Transition::sign(&owner, &byte, byte.0, to, frame_index) }) .collect() } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/cargo_bridge.rs b/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/cargo_bridge.rs index e89caa5af..67fca258b 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/cargo_bridge.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/cargo_bridge.rs @@ -1,70 +1,53 @@ //! Smart Byte cargo bridge for mDL / mDOC presentations. //! -//! An mdoc presentation is packaged as `Cargo::Custom { type_uri, -//! body }` where: -//! -//! * `type_uri` is the constant [`MDL_CARGO_TYPE_URI`]. -//! * `body` is the canonical CBOR encoding of a [`MobileDoc`]. -//! -//! Because the envelope's SAID is BLAKE3 over the canonical CBOR of the -//! envelope (with the body an opaque byte string), the SAID is stable -//! across any round trip of the mdoc that preserves its byte image — -//! including the selective-disclosure projection (the projected mdoc -//! has a *different* SAID, because items were removed; an unprojected -//! round-trip preserves it). +//! An mdoc travels as cargo of the registered type [`MDL_CARGO_TYPE`] +//! (treatise §8.5), whose bytes are the mdoc's own CBOR, exactly as its +//! issuer signed it. The byte's identity covers those bytes, so it is stable +//! across any round trip that preserves the mdoc's byte image. A +//! selective-disclosure projection removes items and so is a different byte. -use smart_byte_core::{ - Cargo, Envelope, JouleCost, OwnershipChain, Provenance, Said, -}; +use smart_byte_core::cargo::types; +use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, Said, Signer}; use crate::error::MdlError; use crate::mdoc::MobileDoc; -/// Cargo `type_uri` constant for an mDL/mDOC payload. -pub const MDL_CARGO_TYPE_URI: &str = "urn:smart-byte:cargo:mdoc:v1"; +/// The registered cargo type of an mdoc. +pub const MDL_CARGO_TYPE: u64 = types::MDOC; -/// Pack an mdoc into a Smart Byte envelope. +/// Issue an mdoc as a smart byte: `issuer` attests to it for `recipient`. pub fn mdoc_envelope( doc: &MobileDoc, - provenance: Provenance, - ownership: OwnershipChain, + issuer: &Signer, + recipient: Aid, + nonce: [u8; 16], joule_cost: JouleCost, ) -> Result { - let body = doc.to_cbor()?; - let cargo = Cargo::Custom { - type_uri: MDL_CARGO_TYPE_URI.into(), - body, - }; - Envelope::new(provenance, ownership, cargo, joule_cost) - .map_err(|e| MdlError::Bridge(e.to_string())) + let cargo = Cargo::new(MDL_CARGO_TYPE, doc.to_cbor()?); + Ok(Envelope::issue(issuer, recipient, nonce, cargo, joule_cost)) } -/// Extract an mdoc from an envelope. +/// Extract the mdoc from a byte whose cargo is an mdoc. pub fn mdoc_from_envelope(env: &Envelope) -> Result { - match &env.cargo { - Cargo::Custom { type_uri, body } if type_uri == MDL_CARGO_TYPE_URI => { - MobileDoc::from_cbor(body) - } - Cargo::Custom { type_uri, .. } => Err(MdlError::Bridge(format!( - "unexpected cargo type_uri: {type_uri}" - ))), - other => Err(MdlError::Bridge(format!( - "cargo kind {} is not an mdoc payload", - other.kind() - ))), + let cargo = &env.provenance.cargo; + if cargo.type_tag != MDL_CARGO_TYPE { + return Err(MdlError::Bridge(format!( + "cargo type {} ({}) is not an mdoc", + cargo.type_tag, + cargo.kind() + ))); } + MobileDoc::from_cbor(&cargo.bytes) } -/// Compute the SAID an envelope would have if an mdoc were stamped with -/// `provenance`, `ownership`, and `joule_cost`. -pub fn mdoc_said( +/// The identity an mdoc takes as a byte issued with these inputs. +pub fn mdoc_identity( doc: &MobileDoc, - provenance: Provenance, - ownership: OwnershipChain, - joule_cost: JouleCost, + issuer: &Signer, + recipient: Aid, + nonce: [u8; 16], ) -> Result { - let env = mdoc_envelope(doc, provenance, ownership, joule_cost)?; - Ok(env.id) + Ok(mdoc_envelope(doc, issuer, recipient, nonce, JouleCost::default())?.identity()) } #[cfg(test)] @@ -101,25 +84,25 @@ mod tests { .unwrap() } - fn provenance() -> Provenance { - Provenance::new( - Said::hash(b"mdl-issuer"), - chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap(), - b"x5chain".to_vec(), + fn issuer() -> Signer { + Signer::single( + Aid::of_inception(Said::hash(b"mdl-issuer")), + 0, + smart_byte_core::SigningKey::from_bytes(&[0x66; 32]), ) } + fn holder() -> Aid { + Aid::of_inception(Said::hash(b"mdl-holder")) + } + #[test] fn envelope_round_trip() { let doc = issue_mdl(); - let env = mdoc_envelope( - &doc, - provenance(), - OwnershipChain::empty(), - JouleCost::measured(11), - ) - .unwrap(); - env.verify_said().unwrap(); + let env = mdoc_envelope(&doc, &issuer(), holder(), [1; 16], JouleCost::measured(11)).unwrap(); + let mut keys = smart_byte_core::KeyBook::new(); + keys.insert(issuer().aid, 0, issuer().key_state()); + env.verify(&keys).unwrap(); let back = mdoc_from_envelope(&env).unwrap(); assert_eq!(back.doc_type, doc.doc_type); assert_eq!( @@ -129,34 +112,16 @@ mod tests { } #[test] - fn said_stable_across_calls() { + fn identity_stable_across_calls() { let doc = issue_mdl(); - let a = mdoc_said( - &doc, - provenance(), - OwnershipChain::empty(), - JouleCost::measured(11), - ) - .unwrap(); - let b = mdoc_said( - &doc, - provenance(), - OwnershipChain::empty(), - JouleCost::measured(11), - ) - .unwrap(); + let a = mdoc_identity(&doc, &issuer(), holder(), [2; 16]).unwrap(); + let b = mdoc_identity(&doc, &issuer(), holder(), [2; 16]).unwrap(); assert_eq!(a, b); } #[test] fn rejects_wrong_cargo_kind() { - let env = Envelope::new( - provenance(), - OwnershipChain::empty(), - Cargo::Bytes(vec![1, 2, 3]), - JouleCost::default(), - ) - .unwrap(); + let env = Envelope::issue(&issuer(), holder(), [3; 16], Cargo::opaque(vec![1, 2, 3]), JouleCost::default()); assert!(mdoc_from_envelope(&env).is_err()); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/lib.rs b/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/lib.rs index 5beb7967b..f205b5fec 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/lib.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-mdl/src/lib.rs @@ -32,10 +32,8 @@ //! URL form. //! * [`online_presentment`] — the 18013-7 reader-side `ItemsRequest` //! and `DeviceResponse` wire types. -//! * [`cargo_bridge`] — packs an mdoc into a Smart Byte envelope as a -//! `Cargo::Custom { type_uri = "urn:smart-byte:cargo:mdoc:v1", body = -//! cbor(mdoc) }` payload so the envelope's SAID and signature -//! machinery work unchanged. +//! * [`cargo_bridge`] — issues an mdoc as a smart byte whose cargo is +//! the registered mdoc type, carrying the mdoc's CBOR unchanged. //! //! # Scope notes //! @@ -68,7 +66,7 @@ pub mod session_transcript; pub mod verifier; pub use cargo_bridge::{ - MDL_CARGO_TYPE_URI, mdoc_envelope, mdoc_from_envelope, mdoc_said, + MDL_CARGO_TYPE, mdoc_envelope, mdoc_from_envelope, mdoc_identity, }; pub use device_engagement::{DeviceEngagement, DeviceRetrievalMethod}; pub use error::MdlError; diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-vc/src/cargo_bridge.rs b/smart-byte/smart-byte-rs/crates/smart-byte-vc/src/cargo_bridge.rs index 7a3a97060..caeebb98f 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-vc/src/cargo_bridge.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-vc/src/cargo_bridge.rs @@ -1,85 +1,58 @@ //! Smart Byte cargo bridge for Verifiable Credentials. //! -//! Rather than modify the closed [`smart_byte_core::Cargo`] enum, this -//! module recognises a VC as a `Cargo::Custom { type_uri, body }` -//! payload where: -//! -//! * `type_uri` is the constant [`VC_CARGO_TYPE_URI`]. -//! * `body` is the canonical JCS encoding (RFC 8785) of the -//! credential. JCS is used so the envelope's SAID — which is -//! BLAKE3 over the canonical CBOR of the envelope, with `body` an -//! opaque byte string — is stable across any JSON-shape-preserving -//! round-trip of the credential. -//! -//! Signing reuses [`smart_byte_core::sign`], i.e. Ed25519 over the -//! envelope's SAID. The W3C Data Integrity proof on the credential is -//! orthogonal and can travel inside the cargo body. +//! A credential travels as cargo of the registered type +//! [`VC_CARGO_TYPE`] (treatise §8.5), whose bytes are the credential's +//! canonical JCS encoding (RFC 8785). JCS keeps those bytes — and so the +//! byte's identity, which covers them — stable across any shape-preserving +//! JSON round trip of the credential. The credential's own Data Integrity +//! proof is orthogonal and travels inside the cargo. -use smart_byte_core::{ - Cargo, Envelope, JouleCost, OwnershipChain, Provenance, Said, -}; +use smart_byte_core::cargo::types; +use smart_byte_core::{Aid, Cargo, Envelope, JouleCost, Said, Signer}; use crate::credential::VerifiableCredential; use crate::error::VcError; -/// Cargo `type_uri` constant for a Verifiable Credential payload. -pub const VC_CARGO_TYPE_URI: &str = "https://www.w3.org/ns/credentials/v2#VerifiableCredential"; +/// The registered cargo type of a verifiable credential. +pub const VC_CARGO_TYPE: u64 = types::VERIFIABLE_CREDENTIAL; -/// Pack a credential into a Smart Byte envelope. -/// -/// The envelope's SAID is computed by `smart-byte-core` over the -/// canonical CBOR of `{provenance, ownership, Cargo::Custom{type_uri, -/// body=JCS(vc)}, joule_cost}`, with `id` zeroed. +/// Issue a credential as a smart byte: `issuer` attests to it for +/// `recipient`. pub fn vc_envelope( vc: &VerifiableCredential, - provenance: Provenance, - ownership: OwnershipChain, + issuer: &Signer, + recipient: Aid, + nonce: [u8; 16], joule_cost: JouleCost, ) -> Result { - let body = vc.to_jcs()?; - let cargo = Cargo::Custom { - type_uri: VC_CARGO_TYPE_URI.to_string(), - body, - }; - Envelope::new(provenance, ownership, cargo, joule_cost) - .map_err(|e| VcError::Bridge(e.to_string())) + let cargo = Cargo::new(VC_CARGO_TYPE, vc.to_jcs()?); + Ok(Envelope::issue(issuer, recipient, nonce, cargo, joule_cost)) } -/// Extract a credential from an envelope whose cargo is the VC type. -/// -/// Errors if the cargo is not a `Custom` payload tagged with -/// [`VC_CARGO_TYPE_URI`] or if the body fails to deserialize as a -/// `VerifiableCredential`. -pub fn vc_from_envelope( - envelope: &Envelope, -) -> Result { - match &envelope.cargo { - Cargo::Custom { type_uri, body } if type_uri == VC_CARGO_TYPE_URI => { - let vc: VerifiableCredential = serde_json::from_slice(body)?; - Ok(vc) - } - Cargo::Custom { type_uri, .. } => Err(VcError::Bridge(format!( - "unexpected cargo type_uri: {type_uri}" - ))), - other => Err(VcError::Bridge(format!( - "cargo kind {} is not a VC payload", - other.kind() - ))), +/// Extract the credential from a byte whose cargo is a verifiable +/// credential. +pub fn vc_from_envelope(envelope: &Envelope) -> Result { + let cargo = &envelope.provenance.cargo; + if cargo.type_tag != VC_CARGO_TYPE { + return Err(VcError::Bridge(format!( + "cargo type {} ({}) is not a verifiable credential", + cargo.type_tag, + cargo.kind() + ))); } + Ok(serde_json::from_slice(&cargo.bytes)?) } -/// Compute the SAID an envelope would have if a VC were stamped today -/// with `provenance`, `ownership`, and `joule_cost`. Useful for clients -/// that want to address a credential before actually emitting the -/// envelope. -pub fn vc_said( +/// The identity a credential takes as a byte issued with these inputs. +/// Issuance is deterministic, so a client can address the credential before +/// sending it. +pub fn vc_identity( vc: &VerifiableCredential, - provenance: Provenance, - ownership: OwnershipChain, - joule_cost: JouleCost, + issuer: &Signer, + recipient: Aid, + nonce: [u8; 16], ) -> Result { - let envelope = vc_envelope(vc, provenance, ownership, joule_cost)?; - Ok(envelope.id) + Ok(vc_envelope(vc, issuer, recipient, nonce, JouleCost::default())?.identity()) } #[cfg(test)] @@ -88,7 +61,7 @@ mod tests { use crate::credential::{CredentialSubject, VcBuilder}; use crate::issuer::Issuer; use chrono::TimeZone; - use smart_byte_core::Said; + use smart_byte_core::{KeyBook, SigningKey}; fn fixture_vc() -> VerifiableCredential { let subj = CredentialSubject { @@ -103,58 +76,40 @@ mod tests { .unwrap() } - fn fixture_provenance() -> Provenance { - Provenance::new( - Said::hash(b"vc-issuer"), - chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).unwrap(), - b"vc-auth".to_vec(), + fn issuer() -> Signer { + Signer::single( + Aid::of_inception(Said::hash(b"vc-issuer")), + 0, + SigningKey::from_bytes(&[0x11; 32]), ) } + fn holder() -> Aid { + Aid::of_inception(Said::hash(b"vc-holder")) + } + #[test] fn envelope_roundtrip_preserves_vc() { let vc = fixture_vc(); - let env = vc_envelope( - &vc, - fixture_provenance(), - OwnershipChain::empty(), - JouleCost::measured(7), - ) - .unwrap(); - env.verify_said().unwrap(); - let back = vc_from_envelope(&env).unwrap(); - assert_eq!(back, vc); + let env = vc_envelope(&vc, &issuer(), holder(), [1; 16], JouleCost::measured(7)).unwrap(); + let mut keys = KeyBook::new(); + keys.insert(issuer().aid, 0, issuer().key_state()); + assert_eq!(env.verify(&keys).unwrap().owner, holder()); + assert_eq!(vc_from_envelope(&env).unwrap(), vc); } #[test] - fn said_is_stable_across_calls() { + fn identity_is_stable_across_calls() { let vc = fixture_vc(); - let a = vc_said( - &vc, - fixture_provenance(), - OwnershipChain::empty(), - JouleCost::measured(7), - ) - .unwrap(); - let b = vc_said( - &vc, - fixture_provenance(), - OwnershipChain::empty(), - JouleCost::measured(7), - ) - .unwrap(); + let a = vc_identity(&vc, &issuer(), holder(), [1; 16]).unwrap(); + let b = vc_identity(&vc, &issuer(), holder(), [1; 16]).unwrap(); assert_eq!(a, b); + assert_ne!(a, vc_identity(&vc, &issuer(), holder(), [2; 16]).unwrap()); } #[test] fn rejects_wrong_cargo_kind() { - let env = Envelope::new( - fixture_provenance(), - OwnershipChain::empty(), - Cargo::Bytes(vec![1, 2, 3]), - JouleCost::default(), - ) - .unwrap(); + let env = Envelope::issue(&issuer(), holder(), [1; 16], Cargo::opaque(vec![1, 2, 3]), JouleCost::default()); assert!(vc_from_envelope(&env).is_err()); } } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-vc/src/lib.rs b/smart-byte/smart-byte-rs/crates/smart-byte-vc/src/lib.rs index b2d4d93cd..39899764b 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-vc/src/lib.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-vc/src/lib.rs @@ -43,7 +43,7 @@ pub mod proof; pub mod sd_jwt; pub mod status_list; -pub use cargo_bridge::{VC_CARGO_TYPE_URI, vc_envelope, vc_from_envelope}; +pub use cargo_bridge::{VC_CARGO_TYPE, vc_envelope, vc_from_envelope, vc_identity}; pub use credential::{ CredentialStatus, CredentialSubject, Evidence, TermsOfUse, VcBuilder, VerifiableCredential, VC_CONTEXT_V2, diff --git a/smart-byte/smart-byte-rs/crates/smart-byte-vc/tests/w3c_vectors.rs b/smart-byte/smart-byte-rs/crates/smart-byte-vc/tests/w3c_vectors.rs index 36e3bee47..a1fc5b0f9 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte-vc/tests/w3c_vectors.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte-vc/tests/w3c_vectors.rs @@ -5,10 +5,10 @@ use chrono::TimeZone; use ed25519_dalek::SigningKey; use rand::rngs::OsRng; use serde_json::json; -use smart_byte_core::{JouleCost, OwnershipChain, Provenance, Said}; +use smart_byte_core::{Aid, JouleCost, KeyBook, Said, Signer}; use smart_byte_vc::{ BitstringStatusList, CredentialSubject, Disclosure, Holder, Issuer, - StatusListCredential, StatusPurpose, VC_CARGO_TYPE_URI, VcBuilder, + StatusListCredential, StatusPurpose, VC_CARGO_TYPE, VcBuilder, VerifiableCredential, VerifiablePresentation, check_status, issue_data_integrity, issue_sd_jwt, issue_vc_jwt, present_sd_jwt, vc_envelope, vc_from_envelope, verify_data_integrity, verify_sd_jwt, @@ -187,35 +187,22 @@ fn status_list_entry_fixture_parses() { #[test] fn cargo_bridge_envelopes_a_vc_with_stable_said() { let vc = load_fixture("vcdm_v2_example.json"); - let prov = Provenance::new( - Said::hash(b"vc-issuer"), - chrono::Utc.with_ymd_and_hms(2026, 5, 23, 12, 0, 0).single().expect("ts"), - b"vc-auth".to_vec(), + let issuer = Signer::single( + Aid::of_inception(Said::hash(b"vc-issuer")), + 0, + SigningKey::from_bytes(&[0x22; 32]), ); - let env_a = vc_envelope( - &vc, - prov.clone(), - OwnershipChain::empty(), - JouleCost::measured(11), - ) - .expect("envelope a"); - let env_b = vc_envelope( - &vc, - prov, - OwnershipChain::empty(), - JouleCost::measured(11), - ) - .expect("envelope b"); - // SAID stability - assert_eq!(env_a.id, env_b.id); - env_a.verify_said().expect("said verify"); - // Cargo carries the VC type uri - match &env_a.cargo { - smart_byte_core::Cargo::Custom { type_uri, .. } => { - assert_eq!(type_uri, VC_CARGO_TYPE_URI); - } - _ => panic!("expected Cargo::Custom"), - } + let holder = Aid::of_inception(Said::hash(b"vc-holder")); + let env_a = vc_envelope(&vc, &issuer, holder, [3; 16], JouleCost::measured(11)).expect("envelope a"); + let env_b = vc_envelope(&vc, &issuer, holder, [3; 16], JouleCost::measured(11)).expect("envelope b"); + // Issuance is deterministic: the same inputs give the same byte. + assert_eq!(env_a.identity(), env_b.identity()); + assert_eq!(env_a.said(), env_b.said()); + let mut keys = KeyBook::new(); + keys.insert(issuer.aid, 0, issuer.key_state()); + env_a.verify(&keys).expect("verifies"); + // The cargo is registered as a verifiable credential. + assert_eq!(env_a.provenance.cargo.type_tag, VC_CARGO_TYPE); // Round-trip extraction let back = vc_from_envelope(&env_a).expect("from envelope"); assert_eq!(back, vc); diff --git a/smart-byte/smart-byte-rs/crates/smart-byte/Cargo.toml b/smart-byte/smart-byte-rs/crates/smart-byte/Cargo.toml index 4aeda372d..902c4992f 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte/Cargo.toml +++ b/smart-byte/smart-byte-rs/crates/smart-byte/Cargo.toml @@ -17,5 +17,4 @@ smart-byte-net = { workspace = true } smart-byte-lockstep = { workspace = true } clap = { workspace = true } anyhow = { workspace = true } -chrono = { workspace = true } -serde_cbor = { workspace = true } +blake3 = { workspace = true } diff --git a/smart-byte/smart-byte-rs/crates/smart-byte/src/main.rs b/smart-byte/smart-byte-rs/crates/smart-byte/src/main.rs index cf9fd939a..f0e652b51 100644 --- a/smart-byte/smart-byte-rs/crates/smart-byte/src/main.rs +++ b/smart-byte/smart-byte-rs/crates/smart-byte/src/main.rs @@ -1,8 +1,9 @@ //! `smart-byte` — reference CLI for the Smart Byte substrate. //! //! Subcommands: -//! - `envelope new` — build a fresh envelope from CLI args -//! - `envelope verify` — re-derive a SAID and check it matches +//! - `envelope new` — issue a byte from a demo issuer and print it +//! - `envelope verify` — decode a byte, check its SAIDs, and check its +//! signatures where the CLI knows the signer's keys //! - `cluster simulate` — run the deterministic lockstep demo //! //! The lockstep demo's output is deterministic: the same `--nodes` / @@ -12,11 +13,11 @@ use std::fs; use std::path::PathBuf; use anyhow::{Context, Result}; -use chrono::{TimeZone, Utc}; use clap::{Parser, Subcommand, ValueEnum}; use smart_byte_core::{ - Cargo, Envelope, JouleCost, OwnershipChain, Provenance, Said, + Aid, Cargo, ControllerSignatureError, Envelope, EnvelopeError, JouleCost, KeyBook, Said, + Signer, SigningKey, }; use smart_byte_lockstep::{Cluster, synthetic_transitions}; @@ -43,21 +44,21 @@ enum Cmd { #[derive(Subcommand, Debug)] enum EnvelopeCmd { - /// Construct a new envelope and print its SAID + CBOR length. + /// Issue a byte from the CLI's demo issuer and print its SAIDs. New { #[arg(long = "cargo-type", value_enum, default_value_t = CargoType::Usd)] cargo_type: CargoType, - /// USD minor units (cents). Required for cargo-type=usd. - #[arg(long = "amount-minor", default_value_t = 0)] - amount_minor: i64, - /// Microjoules. Used for cargo-type=joule-claim. + /// US dollars in micros (six decimals). Used for cargo-type=usd. + #[arg(long = "amount-micros", default_value_t = 0)] + amount_micros: i64, + /// Microjoules. Used for cargo-type=joules, and as the measured cost. #[arg(long = "microjoules", default_value_t = 0)] microjoules: u64, /// Optional path to write the CBOR-encoded envelope to. #[arg(long = "out")] out: Option, }, - /// Verify the SAID of an envelope read from disk. + /// Decode a byte from disk and verify it. Verify { /// Path to the CBOR-encoded envelope. file: PathBuf, @@ -80,9 +81,9 @@ enum ClusterCmd { #[derive(Copy, Clone, Debug, ValueEnum)] enum CargoType { - Bytes, + Opaque, Usd, - JouleClaim, + Joules, } fn main() -> Result<()> { @@ -91,10 +92,10 @@ fn main() -> Result<()> { Cmd::Envelope { sub } => match sub { EnvelopeCmd::New { cargo_type, - amount_minor, + amount_micros, microjoules, out, - } => envelope_new(cargo_type, amount_minor, microjoules, out), + } => envelope_new(cargo_type, amount_micros, microjoules, out), EnvelopeCmd::Verify { file } => envelope_verify(file), }, Cmd::Cluster { sub } => match sub { @@ -107,41 +108,36 @@ fn main() -> Result<()> { } } +/// The CLI's demo issuer: a fixed key, so repeated `envelope new` runs with +/// the same flags issue the same byte. +fn demo_issuer() -> Signer { + let seed = *blake3::hash(b"smart-byte cli demo issuer").as_bytes(); + Signer::single(Aid::of_inception(Said::hash(&seed)), 0, SigningKey::from_bytes(&seed)) +} + +fn demo_holder() -> Aid { + Aid::of_inception(Said::hash(b"smart-byte cli demo holder")) +} + fn envelope_new( cargo_type: CargoType, - amount_minor: i64, + amount_micros: i64, microjoules: u64, out: Option, ) -> Result<()> { - let issuer = Said::hash(b"cli-issuer"); - // Fixed issuance time so that repeated `envelope new` invocations - // with the same flags produce the same SAID. Real issuers stamp - // wall-clock time; the CLI's job is to demonstrate the schema, not - // to imitate issuer behavior. - let issued_at = Utc - .with_ymd_and_hms(2026, 5, 23, 0, 0, 0) - .single() - .context("fixed timestamp is valid")?; - let provenance = Provenance::new(issuer, issued_at, b"cli-auth".to_vec()); - let cargo = match cargo_type { - CargoType::Bytes => Cargo::Bytes(b"demo".to_vec()), - CargoType::Usd => Cargo::Usd { - minor: amount_minor, - }, - CargoType::JouleClaim => Cargo::JouleClaim { microjoules }, + CargoType::Opaque => Cargo::opaque(b"demo".to_vec()), + CargoType::Usd => Cargo::usd_micros(amount_micros), + CargoType::Joules => Cargo::joules(microjoules), }; + let issuer = demo_issuer(); + let envelope = Envelope::issue(&issuer, demo_holder(), [0; 16], cargo, JouleCost::measured(microjoules)); - let envelope = Envelope::new( - provenance, - OwnershipChain::empty(), - cargo, - JouleCost::measured(microjoules), - )?; - - let bytes = envelope.to_cbor()?; - println!("said : {}", envelope.id); - println!("cargo_kind : {}", envelope.cargo.kind()); + let bytes = envelope.to_cbor(); + println!("identity : {}", envelope.identity()); + println!("snapshot : {}", envelope.said()); + println!("issuer : {}", issuer.aid); + println!("cargo_kind : {}", envelope.provenance.cargo.kind()); println!("cbor_bytes : {}", bytes.len()); if let Some(path) = out { fs::write(&path, &bytes).context("write envelope to disk")?; @@ -152,9 +148,21 @@ fn envelope_new( fn envelope_verify(file: PathBuf) -> Result<()> { let bytes = fs::read(&file).context("read envelope")?; - let env = Envelope::from_cbor(&bytes).context("decode envelope")?; - env.verify_said().context("verify SAID")?; - println!("ok : {}", env.id); + let env = Envelope::from_cbor(&bytes).context("decode envelope (canonical bytes, both SAIDs)")?; + println!("identity : {}", env.identity()); + println!("snapshot : {}", env.said()); + // The CLI knows one controller's keys: its demo issuer's. + let mut keys = KeyBook::new(); + let issuer = demo_issuer(); + keys.insert(issuer.aid, issuer.establishment, issuer.key_state()); + match env.verify(&keys) { + Ok(v) => println!("verified : owner {} after {} transfers", v.owner, env.ownership_chain.len()), + Err(EnvelopeError::Signature { + source: ControllerSignatureError::UnknownKeyState { aid, .. }, + .. + }) => println!("signatures : not checked — no key state for {aid}"), + Err(e) => return Err(e).context("verify"), + } Ok(()) } diff --git a/smart-byte/spec/envelope_wire_format.md b/smart-byte/spec/envelope_wire_format.md new file mode 100644 index 000000000..617ec5159 --- /dev/null +++ b/smart-byte/spec/envelope_wire_format.md @@ -0,0 +1,164 @@ +# Envelope Wire Format + +*Smart Byte Substrate — Spec, Section: Envelope Wire Format. v1.0-draft.* + +--- + +## 1. What this section fixes + +Treatise [§8](./treatise_v1_parts_I_II_III_combined.md) says what a smart byte carries and why. This section fixes how it is written: member names and types, the messages its signatures cover, how its history hash advances, and how it is verified. Everything is canonical CBOR as [identity spec §2.3](./identity_and_key_rotation.md) defines it. Where the treatise's prose leaves a choice open, this section makes it. + +--- + +## 2. The envelope + +``` +{ + "d": SAID of this envelope as it stands (§4), + "provenance": the origin attestation (§3), + "ownership_chain": [transition, ...] (§5), oldest first, + "joule_cost": {"measured_microjoules": uint, "estimated_microjoules": uint} +} +``` + +An envelope has exactly these members, and so does every map inside it: a member this section does not define makes the envelope malformed. `joule_cost` is the two-part energy accounting of treatise §8.6. + +An envelope carries two SAIDs. The provenance's `d` is the byte's **identity** (treatise §8.2): fixed when the byte is issued, never changed by anything that happens to it afterwards. The envelope's own `d` addresses this **snapshot** of the byte — its chain and joule cost as they stand now. + +--- + +## 3. Provenance: the origin attestation + +``` +{ + "d": SAID: the byte's identity, + "issuer": the issuer's controller AID (identity spec §3.1), + "recipient": the first owner's controller AID, + "nonce": 16 bytes, + "cargo": cargo (§6), + "signature": the issuer's controller signature (§7) +} +``` + +- The issuer signs the canonical encoding of the four attested fields (treatise §8.3): the map `{"cargo", "issuer", "nonce", "recipient"}` holding the provenance's own values. +- `d` is the SAID of the provenance map (identity spec §2.1). It covers the signature, so the identity binds the issuer, the cargo, the recipient, the nonce, and the issuer's signature (treatise §8.2). +- The nonce keeps two otherwise-identical issuances distinct. + +Parties are named by controller AIDs, which identity spec §3.1 makes authoritative over the treatise's raw public keys. A signature is checked against the key state the signer's key-event log establishes (§7), so a byte stays verifiable across its owners' key rotations. + +--- + +## 4. The snapshot SAID + +The envelope's `d` is the SAID of the whole envelope map, computed per identity spec §2.1 with the provenance's `d` already in place. Each transfer and each joule-cost update produces a new snapshot, and so a new snapshot SAID; none changes the identity. The gossip overlay deduplicates and serves snapshots by this SAID (identity spec §2.6). + +--- + +## 5. The ownership chain + +``` +{ + "previous_history_hash": 32 bytes, + "from": the current owner's AID, + "to": the new owner's AID, + "frame": uint: the lockstep frame the transfer is applied in, + "signature": from's controller signature (§7) +} +``` + +- `from` signs the canonical encoding of `{"byte", "previous_history_hash", "from", "to", "frame"}`, where `byte` is the identity's SAID text (treatise §8.4). The signature is therefore scoped to exactly one transition, of exactly one byte, at exactly one point in its history. +- The **history hash** (treatise §10.1) starts as the identity's 32 digest bytes. Each transition advances it: `next = BLAKE3(previous ‖ payload)`, where `payload` is the canonical encoding of the transition's `{"from", "to", "frame", "signature"}`. +- Frames rise strictly along the chain: a byte is transferred at most once per frame (treatise §8.4). + +--- + +## 6. Cargo + +``` +{"type": uint, "bytes": bytes} +``` + +`type` is a small-integer tag from the registry below, and `bytes` is opaque to the substrate (treatise §8.5). A registered type fixes what its bytes hold; the substrate transports them unchanged. + +| type | name | bytes | +| ---- | ----------------------- | ------------------------------------------------------------ | +| 0 | `opaque` | application-defined | +| 1 | `usd` | canonical CBOR `{"micros": int}`: dollars, six decimals | +| 2 | `joules` | canonical CBOR `{"microjoules": uint}` | +| 16 | `verifiable-credential` | the credential's JCS encoding (RFC 8785) | +| 17 | `bbs-credential` | canonical CBOR of the BBS+ credential body | +| 18 | `bbs-presentation` | canonical CBOR of the BBS+ presentation body | +| 19 | `crdt-snapshot` | canonical CBOR of a CRDT document snapshot | +| 20 | `crdt-delta` | canonical CBOR of a CRDT op batch | +| 21 | `mdoc` | the mobile document's CBOR, as its issuer signed it | + +Tags 3–15 are reserved for the substrate's own cargo classes. New types are added to this table. + +--- + +## 7. Controller signatures + +``` +{ + "establishment": uint, + "signatures": [{"index": uint, "signature": bytes}, ...] +} +``` + +- `establishment` is the sequence number of the signer's establishment event — inception or rotation — whose key set made the signatures. +- Each signature is by key `index` of that set: the key's one-byte algorithm identifier, then the signature. For Ed25519 that is `0x01` followed by 64 bytes. + +A controller signature is valid if and only if all of the following hold: + +1. The named establishment event is in the signer's verified key-event log (identity spec §4). +2. Every listed signature verifies. One bad signature refuses the whole set, so a set means one thing however it is relayed. +3. No index appears twice. +4. At least the event's threshold `kt` of keys signed. + +Ed25519 is verified strictly. A key whose encoded `y` is not below the field prime is refused (RFC 8032 §5.1.3), as is a scalar `S` outside `[0, L)` (§5.1.7). So is any small-order key or commitment. + +--- + +## 8. Verification + +A verifier holding an envelope's bytes and its parties' key-event logs runs treatise §10.2's procedure: + +1. **Decode.** The bytes are canonical, and exactly the encoding of an envelope: every member is defined by this section and written in the type and form it gives — a byte string, not an array of integers, where bytes belong. Both SAIDs check: the envelope's `d` against the envelope, then the provenance's `d` against the provenance. +2. **Provenance.** The issuer's signature is valid over the attested fields (§3, §7). +3. **Chain.** For each transition in order: + - its `previous_history_hash` equals the running history hash; + - its `from` is the current owner (the recipient, for the first transition); + - its frame exceeds the previous transition's; + - its signature is valid. + + The running hash then advances, and `to` becomes the owner. +4. **Result.** The identity, the issuer, the current owner and the current history hash. + +Each failure has an error class, shared with the conformance pack: + +- `said_mismatch` — a SAID does not check. +- `not_an_envelope` — an undefined member, or a member of the wrong type or form. +- `unknown_key_state` — the named establishment event is not in the verifier's logs. +- `signature_invalid`, `below_threshold`, `duplicate_key_index`, `key_index_out_of_range`, `algorithm_mismatch`, `unsupported_algorithm`, `malformed_signature`, `malformed_verifying_key` — a signature rule fails. +- `broken_history_chain` — a transition does not continue the history it follows. +- `not_current_owner` — a transition is not signed away by the current owner. +- `frame_not_increasing` — two transitions share a frame, or frames go backwards. +- A canonical-encoding class (identity spec §2.3) — the bytes are not canonical. + +--- + +## 9. What the cluster decides + +Three questions depend on cluster state rather than on the envelope: + +- whether the issuer is registered for the cargo type (treatise §10.2 step 3, §15); +- whether a signer's establishment event was the one in force at a transition's frame; +- how nodes account the joule cost (treatise §8.6). + +The cluster answers these when it applies a transfer (treatise §9.4 step 5). + +--- + +## 10. Implementation and vectors + +`smart-byte-rs/crates/smart-byte-core` implements this section: `envelope.rs`, `provenance.rs`, `ownership.rs` and `cargo.rs`, with signatures in `keys.rs`. Key-event logs are replayed by `smart-byte-rs/crates/smart-byte-keri-witness`. The conformance vectors are `../conformance/v1/envelope_vectors.json`, built on the key-event logs of `../conformance/v1/kel_vectors.json`. diff --git a/smart-byte/spec/identity_and_key_rotation.md b/smart-byte/spec/identity_and_key_rotation.md index c8d614687..e60161af6 100644 --- a/smart-byte/spec/identity_and_key_rotation.md +++ b/smart-byte/spec/identity_and_key_rotation.md @@ -113,7 +113,7 @@ A verifier holding the object's bytes confirms they are canonical, replaces `d` ### 2.5 Test-vector hook -The full set of canonical worked examples — placeholder bytes, canonical CBOR bytes, BLAKE3 digest, final SAID — lives in `../conformance/v1/said_vectors.json`. Implementers MUST pass every vector. The suite covers the empty-map case (`said-006`), the one-field case (`said-007`, a §3.4 per-key object), the worked example above (`said-008`), five nested objects built from an envelope's members (`said-001` to `said-005`), and two adversarial cases where a non-canonical CBOR encoding produces a different digest (`said-009`, `said-010`), to verify that implementations reject non-canonical input rather than silently re-encoding it or hashing it as received. `said-011` is a canonical object that states a SAID its content does not hash to. The canonical-encoding rules of §2.3 are exercised one by one in `../conformance/v1/canonicalisation_vectors.json`. +The full set of canonical worked examples — placeholder bytes, canonical CBOR bytes, BLAKE3 digest, final SAID — lives in `../conformance/v1/said_vectors.json`. Implementers MUST pass every vector. The suite covers the empty-map case (`said-006`), the one-field case (`said-007`, a §3.4 per-key object), the worked example above (`said-008`), five objects the substrate addresses by SAID (`said-001` to `said-005`: an issued envelope, its provenance — the byte's identity — the envelope after transfers, and a rotation and an interaction event), and two adversarial cases where a non-canonical CBOR encoding produces a different digest (`said-009`, `said-010`), to verify that implementations reject non-canonical input rather than silently re-encoding it or hashing it as received. `said-011` is a canonical object that states a SAID its content does not hash to. The canonical-encoding rules of §2.3 are exercised one by one in `../conformance/v1/canonicalisation_vectors.json`. ### 2.6 Receiving SAID-addressed objects @@ -134,9 +134,11 @@ The Smart Byte key-event log is a sequence of self-addressing events that record ### 3.1 Controller identifiers -A **controller AID** (Autonomic IDentifier) is the SAID of the controller's inception event with the prefix `B` substituted for `E`. The substitution distinguishes a controller identifier from any other SAID, and tells a verifier that the referenced object is an inception event whose key history is recoverable by replaying the log. +A **controller AID** (Autonomic IDentifier) is the SAID of the controller's inception event with the prefix `B` substituted for `E`: `B` followed by the same 52 lowercase base32 characters. The substitution distinguishes a controller identifier from any other SAID, and tells a verifier that the referenced object is an inception event whose key history is recoverable by replaying the log. -The controller AID replaces raw ed25519 public keys in envelope `issuer_pubkey` and ownership-chain `from_pubkey` / `to_pubkey` fields (treatise §8.3, §8.4). Each AID dereferences via §4 replay to the *current* signing key set and threshold; envelope signatures are verified against the replayed set, not a static key. A future revision of §8 will harmonize terminology; until then, this section is authoritative. +An inception event carries its own AID in `i`, which is derived from the very SAID being computed. While the inception event is hashed, `i` therefore holds the 53-`#` placeholder exactly as `d` does (§2.1), and both are then stamped from the result. + +The controller AID replaces raw ed25519 public keys in envelopes: the provenance's `issuer` and `recipient` and each transition's `from` and `to` are AIDs ([envelope wire format](./envelope_wire_format.md) §3, §5). An envelope signature names the establishment event whose key set made it, and is verified against that event's key set as §4 replay establishes it — so a byte stays verifiable across its owners' rotations. ### 3.2 Event types @@ -160,7 +162,9 @@ There are four event types in v1. Each is a CBOR map with the field set describe - **Interaction (`ixn`)** — a signed anchor to other content. Carries an anchors array (`a`) referencing one or more external objects by SAID — envelopes, ownership transitions, external commitments, third-party attestations. Does *not* change the controller's keys; the signing-key set after an `ixn` event is identical to the set before. Interaction events are the substrate's mechanism for binding off-log content to the controller's verifiable history without spending a rotation. -- **Recovery (`rec`)** — **reserved**. A future revision of this section will specify key-loss recovery semantics — the rules under which a controller who has lost the pre-images for an `n` commitment can re-establish control without a full chain reissue. v1 implementations MUST reject `rec` events as malformed. The reservation exists so the event-type field is forward-compatible; the protocol is not yet specified. +- **Recovery (`rec`)** — **reserved**. A future revision of this section will specify key-loss recovery semantics — the rules under which a controller who has lost the pre-images for an `n` commitment can re-establish control without a full chain reissue. v1 implementations MUST reject `rec` events. The reservation exists so the event-type field is forward-compatible; the protocol is not yet specified. + +No other event type exists in v1. Delegated inception, which KERI defines, is not part of this specification. ### 3.3 Event field layout @@ -195,11 +199,13 @@ Notes on individual fields: - `t` is one of the four lowercase tokens above. - `s` starts at `0` for `icp` and increments by exactly 1 per event — no gaps, no reuse. - `p` is the empty string for `icp` and the previous event's `d` for every other event. -- `kt` is an unweighted integer threshold; KERI's weighted-threshold extension is reserved for a later revision. -- Each entry in `k` is a byte string: the one-byte algorithm identifier from treatise §8.3 (`0x01` for ed25519; the remaining range is reserved for post-quantum schemes such as ML-DSA) followed by the public key. Order matters; the same key in a different position produces a different `n` commitment and breaks the chain. +- `kt` is an unweighted integer threshold; KERI's weighted-threshold extension is reserved for a later revision. It is at least 1 and at most the number of keys in `k`. +- Each entry in `k` is a byte string: the one-byte algorithm identifier from treatise §8.3 (`0x01` for ed25519; the remaining range is reserved for post-quantum schemes such as ML-DSA) followed by the public key. Order matters; the same key in a different position produces a different `n` commitment and breaks the chain. No key appears twice: one holder of a key listed twice could meet a two-key threshold alone. - `nt` is committed in advance so an adversary cannot lower the threshold without revealing pre-rotated keys. -- Each entry in `n` is the SAID of the corresponding next signing key per §3.4. An empty `n` signals the controller does not intend to rotate further; thereafter only `ixn` events are accepted. +- Each entry in `n` is the SAID of the corresponding next signing key per §3.4, and no entry appears twice. An empty `n` signals the controller does not intend to rotate further; thereafter only `ixn` events are accepted. +- `nt` is 0 when `n` is empty, and otherwise at least 1 and at most the number of entries in `n`. - `a` is populated for `ixn` and empty for `icp` and `rot`. +- An event has exactly these members, each of the type given. A member this section does not define — or a member written in another form, such as a key as an array of integers rather than a byte string — makes the event malformed: its bytes must be the event's own canonical encoding, so that its SAID covers exactly what it says. ### 3.4 Per-key SAID derivation @@ -215,7 +221,13 @@ A per-key SAID is used rather than a raw key digest so that adding fields to the ### 3.5 Signatures on events -Each event is signed by the key set established by the *previous* event (or, for the inception event, by the keys revealed in its own `k` field — the inception is self-signed). The signature(s) are carried outside the event map, in a wrapper structure that pairs the canonical CBOR-encoded event with an array of `(key-index, signature)` tuples. The wrapper is not part of the SAID computation; the event's `d` covers the event body only, and the signatures are evaluated against that body. +Who signs depends on the event: + +- **Inception.** Signed by the keys it reveals in its own `k`: the inception is self-signed. +- **Rotation.** Signed by the keys it reveals. Those keys must match the previous establishment event's `n` commitment, and the signatures must meet both that event's `nt` and the rotation's own `kt`. Only the holder of the pre-rotated keys can rotate, so an adversary who has compromised the current keys cannot. Nor can the committed threshold be lowered without the pre-rotated keys. +- **Interaction.** Signed by the current key set. + +The signatures are carried outside the event map, in a wrapper that pairs the canonical CBOR-encoded event with an array of `(key-index, signature)` pairs. Each signature is the key's one-byte algorithm identifier followed by the signature (`0x01` and 64 bytes for Ed25519) over the event's canonical encoding. The wrapper is not part of the SAID computation; the event's `d` covers the event body only. This separation lets a verifier confirm the event's SAID independently of signature verification, and lets multiple signing parties append signatures incrementally without changing the event's identifier. @@ -225,7 +237,7 @@ This separation lets a verifier confirm the event's SAID independently of signat Given a sequence of events and a current controller AID, a verifier accepts the log if and only if every check below passes for every event in order from `s: 0` to the latest event seen. -1. **SAID verification.** Confirm the event's bytes are canonical (§2.3) and reject them otherwise (§2.6). Replace the event's `d` field with the 53-character placeholder, encode canonically, compute BLAKE3-256, base32-encode with the `E` prefix, and confirm equality with the asserted `d`. Reject the event if the recomputed SAID does not match. +1. **Decoding and SAID verification.** Confirm the event's bytes are canonical (§2.3) and reject them otherwise (§2.6). Confirm `t` is `icp`, `rot` or `ixn` — `rec` is reserved (§3.2), and no other type exists — and that the event has exactly the §3.3 members, each of its type, and `v` is `SBYTE10JSON`. Then replace the event's `d` field — and, for an inception, its `i` field (§3.1) — with the 53-character placeholder, encode canonically, compute BLAKE3-256, base32-encode with the `E` prefix, and confirm equality with the asserted `d`. Reject the event if the recomputed SAID does not match. For an inception, also confirm `i` is the AID its `d` names. 2. **Sequence verification.** Confirm the event's `s` is exactly the previous event's `s` + 1. For the inception event, confirm `s` is `0`. Reject the event on any gap, repeat, or out-of-order value. @@ -234,20 +246,21 @@ Given a sequence of events and a current controller AID, a verifier accepts the 4. **Controller AID verification.** Confirm the event's `i` equals the inception event's `d` with the digest-algorithm prefix replaced by `B`. Reject the event on mismatch (an event purporting to be in this controller's log but bearing a different AID is malformed and may indicate a confused or hostile log assembler). 5. **Type-specific structural check.** - - For `icp`: confirm `p` is the empty string, `a` is the empty array, `s` is `0`. - - For `rot`: confirm the keys in `k` have per-key SAIDs (per §3.4) matching the previous event's `n` array, in order. Reject the event if any per-key SAID mismatches or if `len(k) != len(previous.n)`. + - For `icp`: confirm it is the first event, `a` is the empty array, and its key fields are well-formed: `k` holds at least one key and no key twice, `1 <= kt <= len(k)`, `n` holds no digest twice, and `nt` is `0` if `n` is empty and otherwise `1 <= nt <= len(n)` (§3.3). + - For `rot`: confirm the previous establishment event's `n` is not empty — a controller that committed to no next keys cannot rotate. Confirm the keys in `k` have per-key SAIDs (per §3.4) matching that `n` array, in order; reject the event if any per-key SAID mismatches or if `len(k) != len(n)`. Confirm `a` is the empty array, and that its key fields are well-formed as for `icp`. - For `ixn`: confirm `k` equals the previous event's `k`, `kt` equals the previous event's `kt`, `n` equals the previous event's `n`, `nt` equals the previous event's `nt`. An interaction event does not change keys, so the unchanged fields must be carried forward verbatim. Reject the event on any divergence. - For `rec`: reject. v1 does not define recovery semantics, and any `rec` event in a v1 log is malformed. -6. **Signature verification.** Verify the event's signatures meet the threshold of the *current* signing key set. The current signing key set is defined as: - - For the inception event: the keys revealed in the inception event's own `k` field. - - For every subsequent event: the keys established by the *most recent prior event whose type changes keys*, which is the inception event or the most recent `rot` event. An `ixn` event does not change the current key set. +6. **Signature verification.** Verify the event's signatures over its canonical encoding, against the key set and threshold §3.5 assigns it: + - `icp`: the keys in its own `k`, threshold `kt`. + - `rot`: the keys in its own `k`, threshold the greater of the previous establishment event's `nt` and its own `kt`. + - `ixn`: the current key set — the inception's or the most recent `rot`'s — and its `kt`. - Confirm that at least `kt` of the signatures in the event's wrapper are valid signatures by distinct keys in the current key set over the canonical CBOR encoding of the event body. Reject the event if fewer than `kt` distinct valid signatures are present. + Every listed signature must be valid, no key index may appear twice, and at least the threshold of keys must have signed. Reject the event otherwise. 7. **Replay determinism.** After all checks pass, update the verifier's notion of the controller's current signing key set, signing threshold, next-key commitment, and next threshold according to the event's `k`, `kt`, `n`, `nt` fields. For `ixn` events, these fields are unchanged from the previous event (per §3.5 above) and the update is a no-op. -The current signing key set after full replay is the set against which envelope and ownership-chain signatures (treatise §8.3, §8.4) are validated. An envelope signed under a since-rotated key remains valid *at the frame in which it was signed*, because verification chains back through the log to the event that established the key set in force at that frame — the property that lets a byte's ownership chain span a rotation without a break. +Replay also records the key state each establishment event put in force. An envelope's signatures name the establishment event whose keys made them and are verified against that event's key state ([envelope wire format](./envelope_wire_format.md) §7), so a byte signed under keys since rotated away stays verifiable — the property that lets a byte's ownership chain span a rotation without a break. Whether the named event was the one in force at a transfer's frame is for the cluster to decide (envelope wire format §9). A verifier MUST treat any failed check as terminal: events with later sequence numbers are not considered part of the log until the rejected event is supplied in a valid form (a different event with a different SAID). v1 does not specify a protocol for replacing a rejected event — that is the recovery problem reserved for `rec`. @@ -266,10 +279,10 @@ The Rust reference implementation of this section lives in the `smart-byte-rs` w - `smart-byte-rs/crates/smart-byte-core/src/canonical.rs` — the canonical CBOR encoding of §2.3, and a strict decoder that refuses non-canonical input instead of re-encoding it. - `smart-byte-rs/crates/smart-byte-core/src/said.rs` — SAID derivation and verification (§2.1, §2.2), placeholder substitution, and per-key SAID derivation (§3.4). - `smart-byte-rs/crates/smart-byte-core/src/inbox.rs` — receiving SAID-addressed objects (§2.6). -- `smart-byte-rs/crates/smart-byte-conformance` — the conformance-vector harness referenced in §2.5. It generates `../conformance/v1` from the two modules above and checks the published pack under `cargo test`. -- `smart-byte-rs/crates/smart-byte-keri-witness` — the key-event log: event type definitions, the wrapper that pairs events with signatures, the verifier algorithm in §4, and helpers for emitting `icp`, `rot`, and `ixn` events. **Its event SAIDs do not follow §2 yet**: it hashes a general-purpose CBOR serialization with a zeroed placeholder, not the canonical encoding with the 53-`#` placeholder, so its identifiers are not interoperable with this section. +- `smart-byte-rs/crates/smart-byte-conformance` — the conformance-vector harness referenced in §2.5. It generates `../conformance/v1` from the reference modules and checks the published pack under `cargo test`. +- `smart-byte-rs/crates/smart-byte-keri-witness` — the key-event log: event type definitions, the wrapper that pairs events with signatures, the verifier algorithm in §4, and helpers for emitting `icp`, `rot`, and `ixn` events. Its verifier reports every establishment event's key state, which is what envelope signatures are checked against. Outside spec v1 it also carries extensions — a witness pool with receipts and watchers, delegated inception, and recovery that counts only with its witnesses' receipts — which its default, spec-v1 verifier refuses. -Where the reference crate and this section disagree, this section governs. The conformance test vectors in `../conformance/v1/said_vectors.json` are the authoritative reference, independent of any single language's implementation. +Where the reference crate and this section disagree, this section governs. The conformance vectors are the authoritative reference, independent of any single language's implementation: `../conformance/v1/said_vectors.json` for §2, and `../conformance/v1/kel_vectors.json` for §3–§4. ---