wai: every Ed25519 signature verified strictly, by one verifier (SPEC §7) #186
66 changed files with 1126 additions and 443 deletions
6
.github/workflows/wai-http.yml
vendored
6
.github/workflows/wai-http.yml
vendored
|
|
@ -48,8 +48,9 @@ on:
|
|||
# The playlists render through hls_render and are sealed by wai_video_manifest.
|
||||
- 'wai/wai-rs/src/hls_render.rs'
|
||||
- 'wai/wai-rs/src/wai_video_manifest.rs'
|
||||
# Playlist receipts verify Ed25519 through the shared strict rule.
|
||||
# Playlist receipts verify Ed25519 through the shared strict rule, in Rust and Python.
|
||||
- 'wai/wai-rs/src/sig.rs'
|
||||
- 'wai/claim-conformance/_ed25519.py'
|
||||
- 'wai/wai-rs/Cargo.toml'
|
||||
- 'wai/http-conformance/**'
|
||||
- 'wai/cache-conformance/**'
|
||||
|
|
@ -75,8 +76,9 @@ on:
|
|||
# The playlists render through hls_render and are sealed by wai_video_manifest.
|
||||
- 'wai/wai-rs/src/hls_render.rs'
|
||||
- 'wai/wai-rs/src/wai_video_manifest.rs'
|
||||
# Playlist receipts verify Ed25519 through the shared strict rule.
|
||||
# Playlist receipts verify Ed25519 through the shared strict rule, in Rust and Python.
|
||||
- 'wai/wai-rs/src/sig.rs'
|
||||
- 'wai/claim-conformance/_ed25519.py'
|
||||
- 'wai/wai-rs/Cargo.toml'
|
||||
- 'wai/http-conformance/**'
|
||||
- 'wai/cache-conformance/**'
|
||||
|
|
|
|||
2
.github/workflows/wai-pack.yml
vendored
2
.github/workflows/wai-pack.yml
vendored
|
|
@ -30,6 +30,7 @@ on:
|
|||
- 'wai/wai-rs/**'
|
||||
- 'wai/pack-conformance/**'
|
||||
- 'wai/claim-conformance/_blake3.py'
|
||||
- 'wai/claim-conformance/_ed25519.py'
|
||||
- 'wai/int-payload-conformance/int_payload_verify.py'
|
||||
- 'wai/registry/capabilities.json'
|
||||
- '.github/workflows/wai-pack.yml'
|
||||
|
|
@ -40,6 +41,7 @@ on:
|
|||
- 'wai/wai-rs/**'
|
||||
- 'wai/pack-conformance/**'
|
||||
- 'wai/claim-conformance/_blake3.py'
|
||||
- 'wai/claim-conformance/_ed25519.py'
|
||||
- 'wai/int-payload-conformance/int_payload_verify.py'
|
||||
- 'wai/registry/capabilities.json'
|
||||
- '.github/workflows/wai-pack.yml'
|
||||
|
|
|
|||
|
|
@ -6,6 +6,34 @@ wire format may change and such changes are recorded here.
|
|||
|
||||
## v0.4.0 — current
|
||||
|
||||
### One Ed25519 rule for every signature
|
||||
|
||||
- **SPEC §7 *Signatures*.** Every Ed25519 signature WAI defines is verified strictly: the
|
||||
key is its point's one canonical encoding and not of small order, `S` < L, `R` is not of
|
||||
small order, and the cofactorless equation holds with `R` compared byte for byte. The
|
||||
receipt extensions cite the rule instead of restating it; jwp-receipts §3.1 step 3
|
||||
keeps its two reasons, `bad_signer_key` for steps 1 and 2 and
|
||||
`signature_verify_failed` for the rest. OER/2's chain signature keeps the rule
|
||||
oer-receipts §5 settles.
|
||||
- **One verifier in wai-rs.** `wai::sig::verify_strict` is the only Ed25519 check written
|
||||
in the crate but OER/2's. The video, SSAI, key-release, channel and package receipts
|
||||
(both versions), the provenance, ternary, spatial-audio, volumetric, film, score and
|
||||
haptics receipts, and a world's session receipt, ops and seal moved onto it; before,
|
||||
each read its key with the library's lenient decoder and checked the lenient
|
||||
equation, which accepts a key of small order (under the identity, `(R, S) = (B, 1)`
|
||||
verifies over every message), a second spelling of a key, and an `R` of small order.
|
||||
Claims, JWP groups and the key log, which held the rule in a copy of their own, call
|
||||
it too. It builds under the `ed25519` feature, the dependency alone.
|
||||
- **One verifier in Python.** The claim, JWP, signer, HTTP and packaging verifiers share
|
||||
`claim-conformance/_ed25519.py`, the rule written from RFC 8032 with the standard
|
||||
library. `pack_verify.py` had checked signatures leniently.
|
||||
- **Corpora.** Each receipt family's corpus carries the three signatures a verifier
|
||||
checking only the equation accepts — the identity as the key with `(R, S) = (B, 1)`,
|
||||
the identity spelled `y` = p + 1, and an `R` of small order under the base point —
|
||||
and every conforming verifier refuses them: new cases in jwp-conformance,
|
||||
signer-conformance and pack-conformance (`mutations/` and `receipts/`); claim- and
|
||||
http-conformance had them. No existing vector's expectation changed.
|
||||
|
||||
### The int_codec model and vectors: trained and coded on a generated signal
|
||||
|
||||
- The int_codec model and vectors are now trained and coded on a generated signal;
|
||||
|
|
|
|||
29
wai/SPEC.md
29
wai/SPEC.md
|
|
@ -1566,6 +1566,35 @@ not a digest of its output, and this rule does not reach it. A frozen digest
|
|||
SHOULD be re-derived on more than one instruction-set architecture before it is
|
||||
published.
|
||||
|
||||
### Signatures (normative)
|
||||
|
||||
Every Ed25519 signature (RFC 8032, pure Ed25519) that WAI or one of its extensions
|
||||
defines is verified strictly: on a receipt of any family, a claim, a key-log document,
|
||||
a module grant or status, an op, and a world's seal. A verifier MUST refuse a signature
|
||||
unless each of these holds, in this order:
|
||||
|
||||
1. **The key is canonical.** The public key `A` is the one encoding of its point that
|
||||
RFC 8032 §5.1.3 allows: `y` < p, and no sign bit when `x` is 0. A decoder that
|
||||
reduces `y` modulo p, or reads the sign bit beside `x` = 0, reads a second spelling
|
||||
of a point; that key is refused.
|
||||
2. **The key is not of small order.** `A`'s order does not divide 8. Under such a key
|
||||
the equation can hold for a signature made without any private key: under the
|
||||
identity, `(R, S) = (B, 1)` verifies over every message.
|
||||
3. **`S` < L**, the order of the base point B (RFC 8032 §5.1.7).
|
||||
4. **`R` is not of small order.** `R` decodes to a point whose order does not divide 8.
|
||||
5. **The cofactorless equation holds.** `[S]B − [k]A`, with
|
||||
`k = SHA-512(R ‖ A ‖ M) mod L` over the bytes of `R` and `A` as the signature and the
|
||||
key give them, encodes to `R` byte for byte; an `R` that is not its point's
|
||||
canonical encoding never does.
|
||||
|
||||
A key that fails step 1 or 2 is a defect of the key, and an extension MAY report it
|
||||
apart from a defect of the signature (jwp-receipts §3.1 step 3: `bad_signer_key` and
|
||||
`signature_verify_failed`). The rule fixes what RFC 8032 leaves to the verifier, so
|
||||
every conforming verifier accepts the same signatures.
|
||||
|
||||
OER/2 is the one exception: oer-receipts §5 settles its chain signature, which refuses
|
||||
a key by steps 1 and 2 and checks `R` as RFC 8032 does.
|
||||
|
||||
### Machine-checked totality (informative)
|
||||
|
||||
The cross-architecture matrix establishes byte-exactness by **sampling**: it
|
||||
|
|
|
|||
|
|
@ -326,8 +326,10 @@ decoding. Called on each vector's key, signature and signing input (`cryptograph
|
|||
50.0.1 over OpenSSL 3.6.4, Python 3.14.7, macOS on arm64), it accepted five of the six
|
||||
that satisfy the equation — both non-canonical spellings of the identity and the three
|
||||
vectors of the second group — and of the seven refused only `S` + L and the `y` = p + j
|
||||
vector. `claim_verify.py` therefore makes step 3's decoding, order and range checks
|
||||
itself, and leaves only the equation to `cryptography`.
|
||||
vector. `claim_verify.py` therefore checks no signature with `cryptography`: step 3 is
|
||||
`_ed25519.py`, WAI's one Ed25519 rule (SPEC §7 *Signatures*) written from RFC 8032's
|
||||
definitions with the standard library, which the verifiers of the JWP, signer, HTTP and
|
||||
packaging corpora call too.
|
||||
|
||||
## Running
|
||||
|
||||
|
|
@ -335,7 +337,7 @@ itself, and leaves only the equation to `cryptography`.
|
|||
# wai-rs: the canonical fixture, byte-identical regeneration, and every vector
|
||||
cargo run --no-default-features --features receipt_claims --bin wai_claim_vectors -- verify ../claim-conformance
|
||||
|
||||
# the second verifier (needs Python 3 and `cryptography`)
|
||||
# the second verifier (needs Python 3 and `cryptography`, with which it signs the fixture)
|
||||
python3 claim_verify.py
|
||||
|
||||
# the two verifiers' verdicts on generated inputs the corpus does not hold
|
||||
|
|
@ -407,16 +409,16 @@ checks close that:
|
|||
- **The canonical fixture.** An `OnChipCounter` claim given with members out of order at
|
||||
three levels, whitespace, and `\u` escapes in the input text. Its canonical bytes,
|
||||
signature and link hash were computed by `claim_verify.py fixture` — Python's JSON
|
||||
serialiser with members ordered by UTF-16 code units, `cryptography`'s Ed25519, and
|
||||
serialiser with members ordered by UTF-16 code units, `cryptography`'s Ed25519 signing, and
|
||||
BLAKE3 written from its specification — and are pinned as literals in `wai-rs`
|
||||
(`check_canonical_form`), which `wai_claim_vectors verify` checks first. Reversing
|
||||
the member order, or changing the signing domain, fails it.
|
||||
- **The second verifier.** `claim_verify.py` reports every vector's declared outcome and
|
||||
every pinned link hash, including a claim whose canonical form spans three BLAKE3
|
||||
chunks, and checks that the fixture it computes equals the literals in `wai-rs`. Its
|
||||
§3.1 step 3 checks other than the equation — RFC 8032 key decoding, the order of the
|
||||
key and of `R`, and `S` < L (which `cryptography` also checks) — are written from the
|
||||
curve's definition; the equation is `cryptography`'s. Its acquisition-class rule is
|
||||
§3.1 step 3 — RFC 8032 key decoding, the order of the key and of `R`, `S` < L and the
|
||||
cofactorless equation — is `_ed25519.py`, written from the curve's definition. Its
|
||||
acquisition-class rule is
|
||||
written from energy-measurement §2.3–§2.5, where `wai-rs` admits the class through
|
||||
`crate::energy_class`.
|
||||
|
||||
|
|
|
|||
182
wai/claim-conformance/_ed25519.py
Normal file
182
wai/claim-conformance/_ed25519.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
"""Ed25519 verification as WAI verifies every signature (SPEC §7 *Signatures*), written
|
||||
from RFC 8032's definitions with Python's integers and nothing beyond the standard
|
||||
library: the one routine the Python verifiers of WAI's corpora share. It shares no code
|
||||
with wai-rs (`wai::sig`).
|
||||
|
||||
The rule, in order:
|
||||
|
||||
1. the key A decodes, and is the one canonical encoding of its point: y < p, and x = 0
|
||||
never carries the sign bit (RFC 8032 §5.1.3);
|
||||
2. A is not of small order (its order divides 8): under such a key the equation can
|
||||
hold for a signature made without any private key;
|
||||
3. S < L (RFC 8032 §5.1.7);
|
||||
4. R decodes, is not of small order, and [S]B = R + [k]A holds with [S]B - [k]A encoded
|
||||
and compared with R byte for byte (the cofactorless equation), k = SHA-512(R || A ||
|
||||
M) mod L over the bytes of R and A as given.
|
||||
|
||||
`verify` returns None when every step holds, "bad_key" when step 1 or 2 fails and
|
||||
"bad_signature" otherwise, so a verifier names the two outcomes with its own reasons.
|
||||
|
||||
The OER/2 reference verifier does not use this: oer-receipts §5 settles its own rule,
|
||||
which refuses the same keys and leaves R to RFC 8032.
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
P = 2 ** 255 - 19
|
||||
L = 2 ** 252 + 27742317777372353535851937790883648493
|
||||
D = -121665 * pow(121666, P - 2, P) % P
|
||||
_SQRT_M1 = pow(2, (P - 1) // 4, P)
|
||||
|
||||
# The checks a verifier's own self-test may leave out, by name, to show its corpus
|
||||
# catches the defect; never left out otherwise.
|
||||
SKIPPABLE = ("small-order-key", "small-order-r")
|
||||
|
||||
|
||||
def _recover_x(y, sign):
|
||||
if y >= P:
|
||||
return None
|
||||
x2 = (y * y - 1) * pow(D * y * y + 1, P - 2, P) % P
|
||||
if x2 == 0:
|
||||
return None if sign else 0
|
||||
x = pow(x2, (P + 3) // 8, P)
|
||||
if (x * x - x2) % P:
|
||||
x = x * _SQRT_M1 % P
|
||||
if (x * x - x2) % P:
|
||||
return None
|
||||
return P - x if (x & 1) != sign else x
|
||||
|
||||
|
||||
def decode(b):
|
||||
"""RFC 8032 §5.1.3: the point `b` encodes, in extended coordinates (X, Y, Z, T), or
|
||||
None when it is not the canonical encoding of a point (y >= p, x = 0 with the sign
|
||||
bit set, or no x)."""
|
||||
if len(b) != 32:
|
||||
return None
|
||||
y = int.from_bytes(b, "little")
|
||||
sign, y = y >> 255, y & ((1 << 255) - 1)
|
||||
x = _recover_x(y, sign)
|
||||
return None if x is None else (x, y, 1, x * y % P)
|
||||
|
||||
|
||||
def _add(p, q):
|
||||
"""Twisted Edwards addition (a = -1) in extended coordinates; complete on this curve."""
|
||||
a = (p[1] - p[0]) * (q[1] - q[0]) % P
|
||||
b = (p[1] + p[0]) * (q[1] + q[0]) % P
|
||||
c = 2 * p[3] * q[3] * D % P
|
||||
d = 2 * p[2] * q[2] % P
|
||||
e, f, g, h = b - a, d - c, d + c, b + a
|
||||
return (e * f % P, g * h % P, f * g % P, e * h % P)
|
||||
|
||||
|
||||
def _neg(p):
|
||||
return (P - p[0], p[1], p[2], P - p[3])
|
||||
|
||||
|
||||
def _identity(p):
|
||||
return p[0] % P == 0 and (p[1] - p[2]) % P == 0
|
||||
|
||||
|
||||
def encode(p):
|
||||
zi = pow(p[2], P - 2, P)
|
||||
x, y = p[0] * zi % P, p[1] * zi % P
|
||||
return (y | ((x & 1) << 255)).to_bytes(32, "little")
|
||||
|
||||
|
||||
_BY = 4 * pow(5, P - 2, P) % P
|
||||
B = (_recover_x(_BY, 0), _BY, 1, _recover_x(_BY, 0) * _BY % P)
|
||||
IDENTITY = (0, 1, 1, 0)
|
||||
|
||||
|
||||
def small_order(p):
|
||||
"""Whether [8]p is the identity: p's order divides 8."""
|
||||
for _ in range(3):
|
||||
p = _add(p, p)
|
||||
return _identity(p)
|
||||
|
||||
|
||||
def strict_key(pk):
|
||||
"""Steps 1 and 2: whether `pk` is the canonical encoding of a point of large order."""
|
||||
a = decode(pk)
|
||||
return a is not None and not small_order(a)
|
||||
|
||||
|
||||
def _double_mul(s, p, k, q):
|
||||
"""[s]p + [k]q, one doubling per bit (Shamir's trick)."""
|
||||
pq = _add(p, q)
|
||||
acc = IDENTITY
|
||||
for i in range(max(s.bit_length(), k.bit_length()) - 1, -1, -1):
|
||||
acc = _add(acc, acc)
|
||||
bs, bk = (s >> i) & 1, (k >> i) & 1
|
||||
if bs and bk:
|
||||
acc = _add(acc, pq)
|
||||
elif bs:
|
||||
acc = _add(acc, p)
|
||||
elif bk:
|
||||
acc = _add(acc, q)
|
||||
return acc
|
||||
|
||||
|
||||
def verify(pk, msg, sig, skip=()):
|
||||
"""The whole rule over `msg`: None when every step holds, else "bad_key" (steps 1
|
||||
and 2) or "bad_signature" (steps 3 and 4). `skip` names checks of SKIPPABLE to leave
|
||||
out, for a verifier's self-test only."""
|
||||
if len(pk) != 32:
|
||||
return "bad_key"
|
||||
a = decode(pk)
|
||||
if a is None or (small_order(a) and "small-order-key" not in skip):
|
||||
return "bad_key"
|
||||
if len(sig) != 64:
|
||||
return "bad_signature"
|
||||
s = int.from_bytes(sig[32:], "little")
|
||||
if s >= L:
|
||||
return "bad_signature"
|
||||
r = decode(sig[:32])
|
||||
if r is None or (small_order(r) and "small-order-r" not in skip):
|
||||
return "bad_signature"
|
||||
k = int.from_bytes(hashlib.sha512(sig[:32] + pk + msg).digest(), "little") % L
|
||||
if encode(_double_mul(s, B, k, _neg(a))) != sig[:32]:
|
||||
return "bad_signature"
|
||||
return None
|
||||
|
||||
|
||||
def verifies(pk, msg, sig, skip=()):
|
||||
return verify(pk, msg, sig, skip) is None
|
||||
|
||||
|
||||
def _self_test():
|
||||
"""RFC 8032 §7.1 TEST 1 and TEST 2, and the three signatures a lenient verifier
|
||||
accepts and this rule refuses; a list of the checks that fail."""
|
||||
fails = []
|
||||
t1_pk = bytes.fromhex("d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a")
|
||||
t1_sig = bytes.fromhex("e5564300c360ac729086e2cc806e828a84877f1eb8e5d974d873e06522490155"
|
||||
"5fb8821590a33bacc61e39701cf9b46bd25bf5f0595bbe24655141438e7a100b")
|
||||
t2_pk = bytes.fromhex("3d4017c3e843895a92b70aa74d1b7ebc9c982ccf2ec4968cc0cd55f12af4660c")
|
||||
t2_sig = bytes.fromhex("92a009a9f0d4cab8720e820b5f642540a2b27b5416503f8fb3762223ebdb69da"
|
||||
"085ac1e43e15996e458f3613d0f11d8c387b2eaeb4302aeeb00d291612bb0c00")
|
||||
if verify(t1_pk, b"", t1_sig) is not None or verify(t2_pk, b"\x72", t2_sig) is not None:
|
||||
fails.append("rfc8032-vectors")
|
||||
if verify(t2_pk, b"\x73", t2_sig) != "bad_signature":
|
||||
fails.append("rfc8032-wrong-message")
|
||||
one = (1).to_bytes(32, "little")
|
||||
b_enc = encode(B)
|
||||
long_identity = (P + 1).to_bytes(32, "little")
|
||||
forged = b_enc + one # (R, S) = (B, 1): [1]B = B + [k]O under the identity
|
||||
if verify(one, b"any", forged) != "bad_key" or verify(long_identity, b"any", forged) != "bad_key":
|
||||
fails.append("identity-key")
|
||||
if verify(one, b"any", forged, skip=("small-order-key",)) is not None:
|
||||
fails.append("identity-key-is-a-forgery")
|
||||
k = int.from_bytes(hashlib.sha512(one + b_enc + b"m").digest(), "little") % L
|
||||
r_small = one + k.to_bytes(32, "little") # under B (scalar 1): [k]B = O + [k]B
|
||||
if verify(b_enc, b"m", r_small) != "bad_signature" or verify(b_enc, b"m", r_small, skip=("small-order-r",)) is not None:
|
||||
fails.append("small-order-r")
|
||||
s_plus_l = t1_sig[:32] + (int.from_bytes(t1_sig[32:], "little") + L).to_bytes(32, "little")
|
||||
if verify(t1_pk, b"", s_plus_l) != "bad_signature":
|
||||
fails.append("s-not-reduced")
|
||||
return fails
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
f = _self_test()
|
||||
print(("PASS" if not f else "FAIL") + " Ed25519 strict rule self-test " + ",".join(f))
|
||||
sys.exit(1 if f else 0)
|
||||
|
|
@ -6,11 +6,11 @@ computed with Python's integers.
|
|||
|
||||
Written from the extension text. It shares no code with wai-rs (`crate::claim_receipt`),
|
||||
which generates the corpus: the canonical form is Python's JSON serialiser with members
|
||||
ordered by UTF-16 code units, BLAKE3 is written below from its specification, Ed25519's
|
||||
equation is the `cryptography` package's and step 3's other checks (key decoding, the
|
||||
order of the key and of R, S < L) are written below from the curve's definition, and OER
|
||||
receipts named by an `energy` claim are read, verified and their figures read by the OER
|
||||
reference verifier (`../oer-conformance/oer2_verify.py`), under oer-receipts' own rules.
|
||||
ordered by UTF-16 code units, BLAKE3 is written from its specification (`_blake3.py`),
|
||||
Ed25519 is WAI's strict rule written from RFC 8032 (`_ed25519.py`, SPEC §7 *Signatures*;
|
||||
the verifiers of the other corpora call it too), and OER receipts named by an `energy`
|
||||
claim are read, verified and their figures read by the OER reference verifier
|
||||
(`../oer-conformance/oer2_verify.py`), under oer-receipts' own rules.
|
||||
|
||||
python3 claim_verify.py [vectors.json] verify every vector and the canonical fixture
|
||||
python3 claim_verify.py fixture print the fixture's canonical bytes,
|
||||
|
|
@ -40,7 +40,6 @@ sys.path.insert(0, os.path.join(HERE, "..", "staged-conformance"))
|
|||
import staged_verify as SV # noqa: E402 (the strict JSON and manifest reader http_verify.py uses too)
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
from cryptography.exceptions import InvalidSignature # noqa: E402
|
||||
from cryptography.hazmat.primitives import serialization # noqa: E402
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519 # noqa: E402
|
||||
|
||||
|
|
@ -51,71 +50,21 @@ from _blake3 import blake3 # noqa: E402 (written from its specification; share
|
|||
|
||||
# ------------------------------------------------ Ed25519 (RFC 8032), §3.1 step 3's rule
|
||||
#
|
||||
# RFC 8032 verifiers differ on two choices that decide whether a signature verifies:
|
||||
# whether keys and R of small order are refused (RFC 8032's procedure does not refuse
|
||||
# them), and whether the cofactored or the cofactorless equation is checked (§5.1.7 states
|
||||
# the first and allows the second). §3.1 step 3 fixes both, and restates RFC 8032's own decoding rule
|
||||
# (one encoding per point) and range rule (S < L). Every check but the equation is made
|
||||
# here, from the curve's definition: the `cryptography` package decodes keys more
|
||||
# leniently than RFC 8032 §5.1.3 and accepts keys and R of small order. It does check
|
||||
# S < L, which is made here as well. The equation itself is left to it.
|
||||
# §3.1 step 3 is WAI's one rule for every signature (SPEC §7 *Signatures*): the key is the
|
||||
# canonical encoding of a point of large order, S < L, R is not of small order, and the
|
||||
# cofactorless equation holds with R compared byte for byte. `_ed25519.py` implements it
|
||||
# from RFC 8032's definitions; the `cryptography` package alone would accept keys that are
|
||||
# not canonical encodings and keys and R of small order.
|
||||
|
||||
_P = 2 ** 255 - 19
|
||||
_L = 2 ** 252 + 27742317777372353535851937790883648493
|
||||
_D = (-121665 * pow(121666, _P - 2, _P)) % _P
|
||||
_SQRT_M1 = pow(2, (_P - 1) // 4, _P)
|
||||
|
||||
|
||||
def point_decode(b: bytes):
|
||||
"""RFC 8032 §5.1.3: the affine point (x, y), or None where decoding fails — including
|
||||
y >= p and x = 0 with the sign bit set, so a point has exactly one encoding."""
|
||||
y = int.from_bytes(b, "little")
|
||||
sign, y = y >> 255, y & ((1 << 255) - 1)
|
||||
if y >= _P:
|
||||
return None
|
||||
u, v = (y * y - 1) % _P, (_D * y * y + 1) % _P
|
||||
x2 = u * pow(v, _P - 2, _P) % _P
|
||||
if x2 == 0:
|
||||
return (0, y) if sign == 0 else None
|
||||
x = pow(x2, (_P + 3) // 8, _P)
|
||||
if (x * x - x2) % _P:
|
||||
x = x * _SQRT_M1 % _P
|
||||
if (x * x - x2) % _P:
|
||||
return None
|
||||
if x & 1 != sign:
|
||||
x = _P - x
|
||||
return (x, y)
|
||||
|
||||
|
||||
def _add(p, q):
|
||||
"""Twisted Edwards addition (a = -1), complete on this curve."""
|
||||
(x1, y1), (x2, y2) = p, q
|
||||
t = _D * x1 * x2 * y1 * y2 % _P
|
||||
x3 = (x1 * y2 + y1 * x2) * pow(1 + t, _P - 2, _P) % _P
|
||||
y3 = (y1 * y2 + x1 * x2) * pow(1 - t, _P - 2, _P) % _P
|
||||
return (x3, y3)
|
||||
|
||||
|
||||
def small_order(pt) -> bool:
|
||||
"""Whether [8]pt is the identity: the point's order divides 8."""
|
||||
for _ in range(3):
|
||||
pt = _add(pt, pt)
|
||||
return pt == (0, 1)
|
||||
import _ed25519 as ED # noqa: E402
|
||||
|
||||
|
||||
def check_signature(pk: bytes, sig: bytes, msg: bytes):
|
||||
"""§3.1 step 3, in its order: the key decodes and is not of small order
|
||||
(`bad_signer_key`); then S < L, R is not a point of small order, and the cofactorless
|
||||
equation holds with R compared byte for byte (`signature_verify_failed`)."""
|
||||
a = point_decode(pk)
|
||||
need(a is not None and not small_order(a), 3, "bad_signer_key")
|
||||
r = point_decode(sig[:32])
|
||||
need(int.from_bytes(sig[32:], "little") < _L, 3, "signature_verify_failed")
|
||||
need(r is None or not small_order(r), 3, "signature_verify_failed")
|
||||
try:
|
||||
ed25519.Ed25519PublicKey.from_public_bytes(pk).verify(sig, msg)
|
||||
except (InvalidSignature, ValueError):
|
||||
raise Reject(3, "signature_verify_failed")
|
||||
"""§3.1 step 3, in its order: the key (`bad_signer_key`), then S < L, R, and the
|
||||
equation (`signature_verify_failed`)."""
|
||||
why = ED.verify(pk, msg, sig)
|
||||
need(why != "bad_key", 3, "bad_signer_key")
|
||||
need(why is None, 3, "signature_verify_failed")
|
||||
|
||||
|
||||
# ------------------------------------------------------------- canonical form (RFC 8785)
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ so they are legitimately left to presentation and excluded from the hash
|
|||
|
||||
The interactive-worlds session receipt (`jwp-receipts.md`) applies
|
||||
unchanged: world identity over contract + tick-0 graph; the group's
|
||||
objects are the op batches, Merkle-bound; one Ed25519 seal over world
|
||||
objects are the op batches, Merkle-bound; one Ed25519 seal (verified as
|
||||
SPEC §7 *Signatures* requires) over world
|
||||
hash + root + checkpoints + exact signed `work` + carried `joules_micro`
|
||||
(with its acquisition class as an optional signed label,
|
||||
[energy-measurement](energy-measurement.md) §2.8);
|
||||
|
|
|
|||
|
|
@ -86,8 +86,8 @@ supplementary); there is no perceptual substitute to fall back *to*, so
|
|||
## 6. Receipt (JWP profile)
|
||||
|
||||
`signal_hash` is the identity; the group's objects are per-channel
|
||||
reconstruction blocks, Merkle-bound; one Ed25519 signature over
|
||||
signal_hash + root + `work` (= duration × channels) + carried
|
||||
reconstruction blocks, Merkle-bound; one Ed25519 signature (verified as
|
||||
SPEC §7 *Signatures* requires) over signal_hash + root + `work` (= duration × channels) + carried
|
||||
`joules_micro`; `parent_receipt_hash` chains — the worlds/audio receipt
|
||||
shape reused. The figure's acquisition class rides beside it as an optional signed label
|
||||
([energy-measurement](energy-measurement.md) §2.8): absent, the receipt keeps
|
||||
|
|
|
|||
|
|
@ -1212,13 +1212,10 @@ reads back (Appendix G) and has an RFC 8785 form; the BLAKE3 of the rendering is
|
|||
`manifest_hash`; `work_compose_ops` is the number of segments and `viewers_served` is
|
||||
at least 1; the segment at position `i` has `index` `i`, and `ad_id` exactly when it is
|
||||
an ad; `joules_micro` and the class are both present or the figure is the unmetered 0;
|
||||
and `sig` is an Ed25519 signature (RFC 8032) by `signer_pubkey` over the signed bytes,
|
||||
verified strictly: `signer_pubkey` decodes (§5.1.3) and is the one encoding of its point
|
||||
(`y` < p, and no sign bit when `x` is 0), and neither it nor `R` is of small order
|
||||
(`[8]P` is not the identity); `S` < L; and the encoding of `[S]B − [k]A` equals `R`
|
||||
byte for byte, so an `R` that is not its point's one encoding never verifies. A verifier
|
||||
that accepts a key or an `R` this refuses accepts a signature any party can make: under
|
||||
a key of small order, `(R, S) = (B, 1)` verifies over every message.
|
||||
and `sig` is an Ed25519 signature by `signer_pubkey` over the signed bytes, verified as
|
||||
SPEC §7 *Signatures* requires: a key that is not its point's one encoding or is of small
|
||||
order, `S` ≥ L, an `R` of small order, or an equation that does not hold with `R`
|
||||
compared byte for byte fails the receipt.
|
||||
|
||||
## Appendix I — Reading an MPD
|
||||
|
||||
|
|
|
|||
|
|
@ -159,7 +159,9 @@ seal_preimage = "wai:world-seal\x01"
|
|||
|
||||
Hashing the three sections — rather than raw container bytes — makes
|
||||
the commitment independent of section-table order and padding: the
|
||||
seal binds content, not framing.
|
||||
seal binds content, not framing. The seal's signature, every op's
|
||||
(§5.1) and the session receipt's (§8) are verified as SPEC §7
|
||||
*Signatures* requires.
|
||||
|
||||
### 4.1. Asset table — worlds are made of WAI objects
|
||||
|
||||
|
|
@ -474,7 +476,7 @@ unchanged):
|
|||
(source-side authoring/solver joules; sink-side joules append per
|
||||
the JWP sink-metering path).
|
||||
- One Ed25519 signature per group, Merkle-bound, relay-preserved
|
||||
byte-for-byte — all inherited.
|
||||
byte-for-byte — all inherited — verified as SPEC §7 *Signatures* requires.
|
||||
- The signing domain is the profile's own, `wai:world-receipt\x01`
|
||||
(`\x02` when the figure carries a label; energy-measurement.md
|
||||
§2.8), not the base group receipt's `moq-jwp:group-sign\x01`.
|
||||
|
|
|
|||
|
|
@ -1713,29 +1713,15 @@ order listed.
|
|||
decoder entry's grammar and tier, the switches' reasons and order, the
|
||||
missed presentations, the order of its stage sets, its counted bytes and
|
||||
its classical rendition.
|
||||
3. **Signature.** Ed25519 (RFC 8032). This step restates RFC 8032's
|
||||
own decoding and range rules, and fixes the two choices on which
|
||||
RFC 8032 verifiers differ — whether a key or `R` of small order is
|
||||
refused (RFC 8032's procedure does not refuse them; this step does),
|
||||
and which verification equation is checked — so that every
|
||||
conforming verifier accepts the same signatures. `signer_pubkey` is
|
||||
`bad_signer_key` if it does not decode under RFC 8032 §5.1.3 —
|
||||
which refuses a `y` coordinate ≥ p, and `x` = 0 with the sign bit
|
||||
set, so every point has exactly one encoding — or if it decodes to a
|
||||
point of small order (one whose order divides 8). `sig` is
|
||||
`R` ‖ `S`, and it is `signature_verify_failed` unless `S` < L (the
|
||||
order of the base point B, as RFC 8032 §5.1.7 requires), `R` is not
|
||||
the encoding of a point of small order, and the encoding of
|
||||
[S]B − [k]A equals `R` byte for byte, where A is the key's point and
|
||||
k = SHA-512(`R` ‖ `signer_pubkey` ‖ §2.4.3's signing input) mod L.
|
||||
That is the cofactorless equation. RFC 8032 §5.1.7
|
||||
states the cofactored one and allows the cofactorless one; they
|
||||
disagree on some signatures whose points have a small-order
|
||||
component, and this step uses the cofactorless one only. The key
|
||||
checks run first. Under a key of small order the equation can hold
|
||||
for a signature made without any private key — under the identity,
|
||||
`R` = [s]B with `S` = s verifies for every message — so such a key
|
||||
is refused before its signature is examined.
|
||||
3. **Signature.** Ed25519 over §2.4.3's signing input, verified by the
|
||||
rule every WAI signature is held to (SPEC §7 *Signatures*): a canonical
|
||||
key not of small order, `S` < L, `R` not of small order, and the
|
||||
cofactorless equation with `R` compared byte for byte. `signer_pubkey`
|
||||
is `bad_signer_key` if it fails that rule's steps 1 or 2 — it is not
|
||||
the one encoding of its point, or its point is of small order — and
|
||||
`sig` is `signature_verify_failed` if it fails steps 3 to 5. The key
|
||||
checks run first, so a key under which a signature needs no private
|
||||
key is refused before its signature is examined.
|
||||
4. **Class rules**, per §2.4.5–§2.4.12 and §2.6, each with its own
|
||||
reason (below). They run after the signature because a rule about
|
||||
what a signer said only means something once it is established who
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ synthesizable slice of "video," not a video codec.
|
|||
|
||||
`film_hash` is the identity; the group's objects are the per-frame
|
||||
reconstruction blocks (`frame_hash` per frame), Merkle-bound; one Ed25519
|
||||
signature over film_hash + root + `work` (= frames) + carried
|
||||
signature (verified as SPEC §7 *Signatures* requires) over film_hash + root + `work` (= frames) + carried
|
||||
`joules_micro` (the native `wai_meter` supplies a **measured** value);
|
||||
`parent_receipt_hash` chains — the worlds / volumetric receipt shape
|
||||
reused. The figure's acquisition class rides beside it as an optional signed label
|
||||
|
|
|
|||
|
|
@ -785,7 +785,8 @@ and the class's signed encoding. `total_entries` counts every entry; `unique_obj
|
|||
the distinct content hashes over every component; `total_bytes` the entries'
|
||||
`byte_len`; `stored_bytes` the `byte_len` of each content hash's first entry;
|
||||
`work_units` equals `total_entries`. A receipt whose content hash comes at two lengths
|
||||
has no counts. The signature is Ed25519 (RFC 8032) by `signer_pubkey`.
|
||||
has no counts. The signature is Ed25519 by `signer_pubkey`, verified as SPEC §7
|
||||
*Signatures* requires; a signature or a key that rule refuses is `receipt-signature`.
|
||||
|
||||
Its JSON form, `package.receipt.json`, is RFC 8785 canonical, hexadecimal lowercase,
|
||||
integers as numbers no greater than `2^53 − 1` (a packager refuses to seal anything
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ step chain, and the total joules. It chains across edits via
|
|||
> be linked — each step's `input_hash` equals the previous step's
|
||||
> `output_hash`, and the final `output_hash` equals `asset_hash`; (c)
|
||||
> recompute the Merkle root from the step hashes; (d) verify the Ed25519
|
||||
> signature. A failure at any point is a failure — the asset is reported
|
||||
> signature as SPEC §7 *Signatures* requires. A failure at any point is a failure — the asset is reported
|
||||
> unverified, never "probably fine".
|
||||
|
||||
There is no tolerance parameter: the content hash matches or it does not.
|
||||
|
|
|
|||
|
|
@ -44,7 +44,8 @@ A signed record of one bring-up / tune-up run:
|
|||
lineage of device states
|
||||
|
||||
`merkle_root` covers `[config_hash] ++ evidence leaves`; one Ed25519 signature
|
||||
covers the whole payload.
|
||||
covers the whole payload. Every receipt here is verified as SPEC §7 *Signatures*
|
||||
requires.
|
||||
|
||||
### `wai.quantum.job` — `JobReceipt`
|
||||
|
||||
|
|
|
|||
|
|
@ -203,7 +203,8 @@ is trusted the way it honestly can be.**
|
|||
n_qubits)` and it grows exponentially in qubits, so a sink cannot inflate or
|
||||
deflate the stated cost. `joules_micro` is *measured-attested*: only the signer
|
||||
can vouch for what its own silicon drew, and the same holds for the class that
|
||||
labels it and for the values of any report about it (energy-measurement §6). One Ed25519 signature covers all of it
|
||||
labels it and for the values of any report about it (energy-measurement §6). One Ed25519 signature,
|
||||
verified as SPEC §7 *Signatures* requires, covers all of it
|
||||
over a Merkle root of the leaf hashes (`circuit`, `statevector`, `[histogram]`) —
|
||||
the worlds / provenance / film receipt shape reused, `parent_receipt_hash`
|
||||
chaining a derivation exactly as they do.
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ HRTF render or the worlds renderer does not.
|
|||
## 5. Receipt (JWP profile)
|
||||
|
||||
`score_hash` is the identity; the group's objects are the mix PCM blocks
|
||||
(per a fixed sample span), Merkle-bound; one Ed25519 signature over
|
||||
score_hash + root + exact work (= duration × notes) + carried
|
||||
(per a fixed sample span), Merkle-bound; one Ed25519 signature (verified as
|
||||
SPEC §7 *Signatures* requires) over score_hash + root + exact work (= duration × notes) + carried
|
||||
`joules_micro`; `parent_receipt_hash` chains — the worlds/audio receipt
|
||||
shape reused. The figure's acquisition class rides beside it as an optional signed label
|
||||
([energy-measurement](energy-measurement.md) §2.8): absent, the receipt keeps
|
||||
|
|
|
|||
|
|
@ -150,7 +150,8 @@ impl):
|
|||
- the group's objects = the rendered mix's **PCM blocks** (fixed span of
|
||||
frames), each `BLAKE3("wai:audio-block\x01" || block_index || i16le)`,
|
||||
**Merkle-bound** with JWP's exact leaf/node domains.
|
||||
- one Ed25519 signature over a profile payload carrying `scene_hash`,
|
||||
- one Ed25519 signature (verified as SPEC §7 *Signatures* requires) over a
|
||||
profile payload carrying `scene_hash`,
|
||||
the Merkle root, the block list, `work_mac` (= output samples ×
|
||||
objects mixed — exact, signed) and `joules_micro` (carried; the native
|
||||
`wai_world_meter` pattern supplies a measured figure, `0` if
|
||||
|
|
|
|||
|
|
@ -99,7 +99,8 @@ state hash.
|
|||
|
||||
The interactive-worlds session receipt (`jwp-receipts.md`) applies
|
||||
unchanged: world identity over contract + tick-0 graph; the group's
|
||||
objects are the op batches, Merkle-bound; one Ed25519 seal over world
|
||||
objects are the op batches, Merkle-bound; one Ed25519 seal (verified as
|
||||
SPEC §7 *Signatures* requires) over world
|
||||
hash + root + checkpoints + exact signed `work` + carried `joules_micro`
|
||||
(with its acquisition class as an optional signed label,
|
||||
[energy-measurement](energy-measurement.md) §2.8);
|
||||
|
|
|
|||
|
|
@ -118,7 +118,8 @@ two costs so each is trusted honestly:
|
|||
| `joules_micro` | measured marginal energy | **attested** by signature |
|
||||
|
||||
`seal` decodes the stack (so `reconstruction_hash` is always the true
|
||||
reconstruction); `verify` checks the Merkle root + Ed25519; `verify_reconstruction`
|
||||
reconstruction); `verify` checks the Merkle root + Ed25519 (as SPEC §7 *Signatures*
|
||||
requires); `verify_reconstruction`
|
||||
recovers the stack from the `WQTT` payload and confirms hash, shape, work, and
|
||||
reconstruction independently. Chains via `parent_receipt_hash`. The JWP Merkle +
|
||||
Ed25519 are reused verbatim — the same shape as `QuantumReceipt`
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ than imply a compression claim the format does not make.
|
|||
|
||||
`clip_hash` is the identity; the group's objects are per-frame
|
||||
reconstruction blocks (`frame_hash` per frame), Merkle-bound; one
|
||||
Ed25519 signature over clip_hash + root + `work` (= n_frames ×
|
||||
Ed25519 signature (verified as SPEC §7 *Signatures* requires) over clip_hash + root + `work` (= n_frames ×
|
||||
n_gaussians) + carried `joules_micro`; `parent_receipt_hash` chains — the
|
||||
worlds / audio / haptics receipt shape reused. The figure's acquisition class rides beside it as an optional signed label
|
||||
([energy-measurement](energy-measurement.md) §2.8): absent, the receipt keeps
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ reference adaptation policies. Two implementations check every case and must agr
|
|||
- `wai_http_vectors verify .` (also `wai http vectors verify .`), in `wai-rs`, built
|
||||
with the features `http` and `video_manifest` (the playlist receipts);
|
||||
- `python3 http_verify.py .`, written from the extension text and SPEC §7.1, sharing no
|
||||
code with `wai-rs`. It needs only the Python standard library (its Ed25519 check of the
|
||||
playlist receipts is written from RFC 8032), and reads
|
||||
`../claim-conformance/_blake3.py` (BLAKE3 from its specification) and
|
||||
code with `wai-rs`. It needs only the Python standard library, and reads
|
||||
`../claim-conformance/_blake3.py` (BLAKE3 from its specification),
|
||||
`../claim-conformance/_ed25519.py` (WAI's strict Ed25519 rule, SPEC §7 *Signatures*,
|
||||
written from RFC 8032; it checks the playlist receipts) and
|
||||
`../registry/capabilities.json` (the published registry export).
|
||||
|
||||
**Provenance.** Generated here by `wai_http_vectors`, from a staged-delivery case and from the frozen sources `src/SOURCES.json` lists, each encoded once by an encoder of its format: pictures of the `int_mlicpp` vector's generated 64×64 pattern, AV1 colour-bar test patterns, and a clip of a square moving over a gradient. The test patterns and the clip are generated content whose generators are not in this tree. None is a recording or a photograph.
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# must fail: playlist_receipt_sig_key_identity playlist_receipt_sig_key_identity_long playlist_receipt_sig_r_small_order
|
||||
# keys read as the library reads them, and the equation checked leniently (wai::sig)
|
||||
diff --git a/wai/wai-rs/src/sig.rs b/wai/wai-rs/src/sig.rs
|
||||
index 46135933..01f85c39 100644
|
||||
index 48a10219..7a770fae 100644
|
||||
--- a/wai/wai-rs/src/sig.rs
|
||||
+++ b/wai/wai-rs/src/sig.rs
|
||||
@@ -62,9 +62,6 @@ fn below_l(s: &[u8; 32]) -> bool {
|
||||
@@ -64,9 +64,6 @@ fn below_l(s: &[u8; 32]) -> bool {
|
||||
/// large order.
|
||||
pub fn strict_key(bytes: &[u8; 32]) -> Result<VerifyingKey, SigError> {
|
||||
let key = VerifyingKey::from_bytes(bytes).map_err(|_| SigError::BadKey)?;
|
||||
|
|
@ -14,7 +14,7 @@ index 46135933..01f85c39 100644
|
|||
Ok(key)
|
||||
}
|
||||
|
||||
@@ -75,7 +72,7 @@ pub fn verify_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<()
|
||||
@@ -77,7 +74,7 @@ pub fn verify_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<()
|
||||
if !below_l(s) {
|
||||
return Err(SigError::BadSignature);
|
||||
}
|
||||
|
|
@ -22,4 +22,4 @@ index 46135933..01f85c39 100644
|
|||
+ ed25519_dalek::Verifier::verify(&key, msg, &Signature::from_bytes(sig)).map_err(|_| SigError::BadSignature)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// The identity point, canonically encoded (y = 1).
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
# must fail: playlist_receipt_sig_r_small_order
|
||||
# R of small order accepted (wai::sig::verify_strict checks the equation leniently)
|
||||
diff --git a/wai/wai-rs/src/sig.rs b/wai/wai-rs/src/sig.rs
|
||||
index 46135933..ebe58160 100644
|
||||
index 48a10219..dd32f7fc 100644
|
||||
--- a/wai/wai-rs/src/sig.rs
|
||||
+++ b/wai/wai-rs/src/sig.rs
|
||||
@@ -75,7 +75,7 @@ pub fn verify_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<()
|
||||
@@ -77,7 +77,7 @@ pub fn verify_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<()
|
||||
if !below_l(s) {
|
||||
return Err(SigError::BadSignature);
|
||||
}
|
||||
|
|
@ -12,4 +12,4 @@ index 46135933..ebe58160 100644
|
|||
+ ed25519_dalek::Verifier::verify(&key, msg, &Signature::from_bytes(sig)).map_err(|_| SigError::BadSignature)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// The identity point, canonically encoded (y = 1).
|
||||
|
|
|
|||
|
|
@ -2547,86 +2547,16 @@ def jcs(v):
|
|||
return "{" + ",".join(json.dumps(k, ensure_ascii=False) + ":" + jcs(v[k]) for k in keys) + "}"
|
||||
|
||||
|
||||
# Ed25519 (RFC 8032 §5.1), verification only, from the RFC's definitions, strictly as
|
||||
# WAI verifies it: the key and R decoded with y < p (and no sign bit on x = 0), neither of
|
||||
# small order, S < L, and [S]B = R + [k]A checked by encoding [S]B − [k]A and comparing it
|
||||
# with R byte for byte.
|
||||
_P = 2 ** 255 - 19
|
||||
_L = 2 ** 252 + 27742317777372353535851937790883648493
|
||||
_D = -121665 * pow(121666, _P - 2, _P) % _P
|
||||
_I = pow(2, (_P - 1) // 4, _P)
|
||||
|
||||
|
||||
def _recover_x(y, sign):
|
||||
if y >= _P:
|
||||
return None
|
||||
x2 = (y * y - 1) * pow(_D * y * y + 1, _P - 2, _P) % _P
|
||||
if x2 == 0:
|
||||
return None if sign else 0
|
||||
x = pow(x2, (_P + 3) // 8, _P)
|
||||
if (x * x - x2) % _P:
|
||||
x = x * _I % _P
|
||||
if (x * x - x2) % _P:
|
||||
return None
|
||||
return _P - x if (x & 1) != sign else x
|
||||
|
||||
|
||||
def _decode(b):
|
||||
y = int.from_bytes(b, "little")
|
||||
sign, y = y >> 255, y & ((1 << 255) - 1)
|
||||
x = _recover_x(y, sign)
|
||||
return None if x is None else (x, y, 1, x * y % _P)
|
||||
|
||||
|
||||
def _add(p, q):
|
||||
a = (p[1] - p[0]) * (q[1] - q[0]) % _P
|
||||
b = (p[1] + p[0]) * (q[1] + q[0]) % _P
|
||||
c = 2 * p[3] * q[3] * _D % _P
|
||||
d = 2 * p[2] * q[2] % _P
|
||||
e, f, g, h = b - a, d - c, d + c, b + a
|
||||
return (e * f % _P, g * h % _P, f * g % _P, e * h % _P)
|
||||
|
||||
|
||||
def _mul(s, p):
|
||||
q = (0, 1, 1, 0)
|
||||
while s:
|
||||
if s & 1:
|
||||
q = _add(q, p)
|
||||
p = _add(p, p)
|
||||
s >>= 1
|
||||
return q
|
||||
|
||||
|
||||
def _encode(p):
|
||||
zi = pow(p[2], _P - 2, _P)
|
||||
x, y = p[0] * zi % _P, p[1] * zi % _P
|
||||
return (y | ((x & 1) << 255)).to_bytes(32, "little")
|
||||
|
||||
|
||||
_BY = 4 * pow(5, _P - 2, _P) % _P
|
||||
_B = (_recover_x(_BY, 0), _BY, 1, _recover_x(_BY, 0) * _BY % _P)
|
||||
|
||||
|
||||
def _small_order(p):
|
||||
"""Whether [8]P is the identity: P's order divides 8."""
|
||||
q = _mul(8, p)
|
||||
return q[0] % _P == 0 and (q[1] - q[2]) % _P == 0
|
||||
# Ed25519, strictly as WAI verifies every signature (SPEC §7 *Signatures*): the key and R
|
||||
# decoded with y < p (and no sign bit on x = 0), neither of small order, S < L, and
|
||||
# [S]B = R + [k]A checked by encoding [S]B − [k]A and comparing it with R byte for byte.
|
||||
# `_ed25519.py` writes it from RFC 8032's definitions, with the standard library only;
|
||||
# --mutate small-order-key and small-order-r leave out its two order checks.
|
||||
import _ed25519 as ED # noqa: E402
|
||||
|
||||
|
||||
def ed25519_verify(pk, msg, sig):
|
||||
if len(pk) != 32 or len(sig) != 64:
|
||||
return False
|
||||
a, r = _decode(pk), _decode(sig[:32])
|
||||
if a is None or (_small_order(a) and MUTATE != "small-order-key"):
|
||||
return False
|
||||
if r is None or (_small_order(r) and MUTATE != "small-order-r"):
|
||||
return False
|
||||
s = int.from_bytes(sig[32:], "little")
|
||||
if s >= _L:
|
||||
return False
|
||||
k = int.from_bytes(hashlib.sha512(sig[:32] + pk + msg).digest(), "little") % _L
|
||||
neg_a = (_P - a[0], a[1], a[2], _P - a[3])
|
||||
return _encode(_add(_mul(s, _B), _mul(k, neg_a))) == sig[:32]
|
||||
return ED.verifies(pk, msg, sig, skip=(MUTATE,))
|
||||
|
||||
|
||||
RECEIPT_KNOWN = {"kind", "manifest_hash", "session_class_hash", "class", "segments", "merkle_root", "work_compose_ops", "viewers_served",
|
||||
|
|
|
|||
|
|
@ -12488,6 +12488,84 @@
|
|||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "group_signer_key_identity_forged",
|
||||
"about": "the identity point as signer_pubkey, with (R, S) = (B, 1): the equation holds over any payload, so a verifier that checks only the equation accepts it; the key is of small order",
|
||||
"procedure": "group",
|
||||
"input": {
|
||||
"group": {
|
||||
"kind": "group",
|
||||
"track_alias": 7,
|
||||
"group_id": 3,
|
||||
"root_hash": "62ceef6eff690aa495e0744b8f17dc530e69ed587f3de45fc3107992ee531f8d",
|
||||
"leaf_count": 5,
|
||||
"joules_total": 2800,
|
||||
"group_start": "2026-09-30T12:00:00Z",
|
||||
"group_end": "2026-09-30T12:00:01Z",
|
||||
"signer_pubkey": "0100000000000000000000000000000000000000000000000000000000000000",
|
||||
"signer_id": "did:example:publisher",
|
||||
"sig": "58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000",
|
||||
"parent_receipt_hash": null
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 1,
|
||||
"reason": "bad_signer_key"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "group_signer_key_identity_y_not_reduced",
|
||||
"about": "the identity spelled y = p + 1, with (R, S) = (B, 1): a decoder that reduces y reads the identity, under which the equation holds; the encoding is not canonical",
|
||||
"procedure": "group",
|
||||
"input": {
|
||||
"group": {
|
||||
"kind": "group",
|
||||
"track_alias": 7,
|
||||
"group_id": 3,
|
||||
"root_hash": "62ceef6eff690aa495e0744b8f17dc530e69ed587f3de45fc3107992ee531f8d",
|
||||
"leaf_count": 5,
|
||||
"joules_total": 2800,
|
||||
"group_start": "2026-09-30T12:00:00Z",
|
||||
"group_end": "2026-09-30T12:00:01Z",
|
||||
"signer_pubkey": "eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
||||
"signer_id": "did:example:publisher",
|
||||
"sig": "58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000",
|
||||
"parent_receipt_hash": null
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 1,
|
||||
"reason": "bad_signer_key"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "group_signature_r_small_order",
|
||||
"about": "signer_pubkey the base point, whose discrete logarithm is 1, R the identity and S = k: the equation holds, and R is of small order",
|
||||
"procedure": "group",
|
||||
"input": {
|
||||
"group": {
|
||||
"kind": "group",
|
||||
"track_alias": 7,
|
||||
"group_id": 3,
|
||||
"root_hash": "62ceef6eff690aa495e0744b8f17dc530e69ed587f3de45fc3107992ee531f8d",
|
||||
"leaf_count": 5,
|
||||
"joules_total": 2800,
|
||||
"group_start": "2026-09-30T12:00:00Z",
|
||||
"group_end": "2026-09-30T12:00:01Z",
|
||||
"signer_pubkey": "5866666666666666666666666666666666666666666666666666666666666666",
|
||||
"signer_id": "did:example:publisher",
|
||||
"sig": "0100000000000000000000000000000000000000000000000000000000000000f43db9669cf0eb3913db18561bb4d5d64b9ed5c5ec5a425b1bc7351ae6e93904",
|
||||
"parent_receipt_hash": null
|
||||
}
|
||||
},
|
||||
"expected": {
|
||||
"outcome": "reject",
|
||||
"step": 2,
|
||||
"reason": "signature_verify_failed"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,14 @@ digest and what it may ask a packager to emit (§3), how a source is cut into pi
|
|||
catalogs, the receipt and `LISTING`. The fixture's bumper plays twice, so its pieces
|
||||
are one object each time.
|
||||
- `mutations/` — stage indexes, objects and version-2 receipts, and `expected.json`:
|
||||
each case's reason (§6.2–§6.4, §9.1), a well-formed case of each kind among them.
|
||||
each case's reason (§6.2–§6.4, §9.1), a well-formed case of each kind among them,
|
||||
and three receipts carrying a signature only a verifier that checks the equation
|
||||
alone accepts (a key of small order, that key spelled with y = p + 1, an R of small
|
||||
order), each `receipt-signature` (SPEC §7 *Signatures*).
|
||||
- `receipts/` — version-1 package receipts as JSON (§9.3), with each one's receipt
|
||||
hash: the legacy receipt whose hash `wai-rs` pins, and an unmetered one.
|
||||
hash: the legacy receipt whose hash `wai-rs` pins, an unmetered one, and the
|
||||
unmetered one carrying each of those three signatures, marked
|
||||
`"signature": "refused"`.
|
||||
- `catalogs/` — stage indexes no golden package has (refinements on more than one
|
||||
deadline), with the MoQ catalog each gives (§8.4).
|
||||
- `package-mutations/` — packages a verifier must refuse (§11.3), each the golden
|
||||
|
|
@ -95,7 +100,7 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion.
|
|||
reason. `cargo test`
|
||||
runs the same check, with the scene cuts of each fixture stated as tests
|
||||
(`tests/scenecut.rs`).
|
||||
- `pack_verify.py`, Python standard library and, for Ed25519, `cryptography` (TOML through `tomllib`, so 3.11 or
|
||||
- `pack_verify.py`, Python standard library and, to sign what it re-derives, `cryptography` (TOML through `tomllib`, so 3.11 or
|
||||
later) and sharing no code with wai-pack, written from the extension: its own policy
|
||||
schema, constraints, canonical JSON and digest; its own Y4M and WAV readers; the
|
||||
fixtures regenerated from their definition; `wai.pack.scenecut/1` in its on-demand
|
||||
|
|
@ -106,8 +111,8 @@ Files are compared as bytes; `.gitattributes` turns off line-ending conversion.
|
|||
`../claim-conformance/_blake3.py`, and the `WIV1` parser and decoder from
|
||||
`../int-payload-conformance/int_payload_verify.py`. For the packages it has its own
|
||||
stage index reader and rules, object rules (each payload parsed as its pinned format,
|
||||
a `WIR1` reader included), version-1 and version-2 receipts (Ed25519 through the
|
||||
`cryptography` package, imported only where a signature is checked), an AV1 stream
|
||||
a `WIR1` reader included), version-1 and version-2 receipts (Ed25519 by WAI's strict
|
||||
rule, SPEC §7 *Signatures*, from `../claim-conformance/_ed25519.py`), an AV1 stream
|
||||
and sequence header reader, the CMAF files, and every catalog: it verifies each golden
|
||||
package in §11.3's order, rebuilding the floor's files and re-rendering the catalogs
|
||||
byte for byte, gives every mutation its reason, checks the version-1 receipts, and
|
||||
|
|
|
|||
|
|
@ -412,6 +412,33 @@
|
|||
],
|
||||
"reason": "receipt-signature"
|
||||
},
|
||||
{
|
||||
"name": "receipt-key-identity",
|
||||
"kind": "receipt",
|
||||
"receipt": "receipt/key-identity.json",
|
||||
"indexes": [
|
||||
"receipt/index-v.wsix"
|
||||
],
|
||||
"reason": "receipt-signature"
|
||||
},
|
||||
{
|
||||
"name": "receipt-key-identity-y-not-reduced",
|
||||
"kind": "receipt",
|
||||
"receipt": "receipt/key-identity-y-not-reduced.json",
|
||||
"indexes": [
|
||||
"receipt/index-v.wsix"
|
||||
],
|
||||
"reason": "receipt-signature"
|
||||
},
|
||||
{
|
||||
"name": "receipt-r-small-order",
|
||||
"kind": "receipt",
|
||||
"receipt": "receipt/r-small-order.json",
|
||||
"indexes": [
|
||||
"receipt/index-v.wsix"
|
||||
],
|
||||
"reason": "receipt-signature"
|
||||
},
|
||||
{
|
||||
"name": "receipt-unknown-member",
|
||||
"kind": "receipt",
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:[email protected]","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"b5e67aba7b6cc74f5a36f281eb009acf7d46c956214bbd101f49c9354368f6fa","sig":"58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6}
|
||||
1
wai/pack-conformance/mutations/receipt/key-identity.json
Normal file
1
wai/pack-conformance/mutations/receipt/key-identity.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:[email protected]","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"f14cce74876592260cb5840dbadd8046571994e54348c380d79f907a018cac34","sig":"58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"0100000000000000000000000000000000000000000000000000000000000000","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6}
|
||||
|
|
@ -0,0 +1 @@
|
|||
{"asset_id":"mutations","components":[{"id":"v","index":"index-v.wsix","index_digest":"354687c821d088507431959415d285bbb20d02526c614d2e3092adbd48a93d5e","media_class":"video","stage_root":"cc3482c7ed8d99b58f5fd11ae463919d09e27a853d1b3cd5bbfa4b5bedcaa2e3"}],"energy_provenance":null,"grant":{"capability":"wai.video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","packager":"builtin:[email protected]","parent_kind":"none","parent_receipt_hash":null,"policy_digest":"0101010101010101010101010101010101010101010101010101010101010101","receipt_hash":"a3a74cbfe3f2b89252a924286d67e0a227ee492831fb575213652bef2754588c","sig":"0100000000000000000000000000000000000000000000000000000000000000ac25b83496c7c49c920828631dbfcdac9123da5cd7a9011e9cee5bf229b44d03","signer_id":"did:key:z6MkghLt1e8m1fmANsdJJco3aCLV8Xnigr5UWwC3u5iZFPd3","signer_pubkey":"5866666666666666666666666666666666666666666666666666666666666666","stored_bytes":2312,"total_bytes":2746,"total_entries":6,"unique_objects":5,"version":2,"work_units":6}
|
||||
|
|
@ -6,8 +6,9 @@ corpus. It takes BLAKE3 from ../claim-conformance/_blake3.py and the WIV1 parser
|
|||
keyframe-0 decoder from ../int-payload-conformance/int_payload_verify.py, both written
|
||||
from their specifications, reads the capability registry from its published export
|
||||
(../registry/capabilities.json), reads TOML with the standard library's tomllib (TOML
|
||||
1.0), checks Ed25519 signatures (RFC 8032) with the `cryptography` package, imported only
|
||||
where a signature is checked, and implements the rest:
|
||||
1.0), checks Ed25519 signatures with ../claim-conformance/_ed25519.py, WAI's strict rule
|
||||
(SPEC §7 *Signatures*) written from RFC 8032, signs its own re-derivations with the
|
||||
`cryptography` package, imported only where it signs, and implements the rest:
|
||||
|
||||
- the policy (§3): the value-kind walk, the schema with every default, the constraints
|
||||
and their refusal names, the effective policy as JSON, RFC 8785 canonical JSON and the
|
||||
|
|
@ -73,6 +74,7 @@ HERE = os.path.dirname(os.path.abspath(__file__))
|
|||
sys.path.insert(0, os.path.join(HERE, "..", "claim-conformance"))
|
||||
sys.path.insert(0, os.path.join(HERE, "..", "int-payload-conformance"))
|
||||
from _blake3 import blake3 # noqa: E402
|
||||
import _ed25519 as ED # noqa: E402
|
||||
import int_payload_verify as IPV # noqa: E402
|
||||
|
||||
MAX_INTEGER = (1 << 53) - 1
|
||||
|
|
@ -1761,15 +1763,11 @@ def receipt_payload(j):
|
|||
|
||||
|
||||
def ed25519_verifies(pk, sig, msg):
|
||||
"""RFC 8032, through the `cryptography` package, imported here so the rest of the
|
||||
verifier runs on the standard library alone."""
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric import ed25519
|
||||
try:
|
||||
ed25519.Ed25519PublicKey.from_public_bytes(pk).verify(sig, msg)
|
||||
return True
|
||||
except (InvalidSignature, ValueError):
|
||||
return False
|
||||
"""WAI's strict rule (SPEC §7 *Signatures*): a canonical key of large order, S < L, R
|
||||
not of small order, and the cofactorless equation with R compared byte for byte. The
|
||||
`cryptography` package alone accepts a key of small order, a second spelling of a key,
|
||||
and an R of small order."""
|
||||
return ED.verifies(pk, msg, sig)
|
||||
|
||||
|
||||
def ed25519_from_seed(seed, msg):
|
||||
|
|
@ -2906,9 +2904,10 @@ def mutation_verdict(m, c, used):
|
|||
return e.code
|
||||
|
||||
|
||||
def verify_v1_receipt(rep, name, b, want_hash):
|
||||
def verify_v1_receipt(rep, name, b, want_hash, refused=False):
|
||||
"""§9.3: a version-1 receipt read, its payload rebuilt, its hash, counts, segment map
|
||||
root and signature."""
|
||||
root and signature; with `refused`, everything but a signature that every verifier
|
||||
refuses (SPEC §7 *Signatures*)."""
|
||||
try:
|
||||
r = read_receipt(b)
|
||||
except Refused as e:
|
||||
|
|
@ -2929,8 +2928,8 @@ def verify_v1_receipt(rep, name, b, want_hash):
|
|||
rep.check((j["total_segments"], j["unique_segments"], j["total_bytes"], j["stored_bytes"], j["work_segment_ops"])
|
||||
== (len(segs), len(first), sum(s["byte_len"] for s in segs), sum(first.values()), len(segs)),
|
||||
f"{name}: the segment counts")
|
||||
rep.check(ed25519_verifies(bytes.fromhex(j["signer_pubkey"]), bytes.fromhex(j["sig"]), r["payload"]),
|
||||
f"{name}: the signature verifies")
|
||||
rep.check(ed25519_verifies(bytes.fromhex(j["signer_pubkey"]), bytes.fromhex(j["sig"]), r["payload"]) != refused,
|
||||
f"{name}: the signature " + ("is refused" if refused else "verifies"))
|
||||
|
||||
|
||||
def verify(d):
|
||||
|
|
@ -3070,7 +3069,8 @@ def verify(d):
|
|||
|
||||
rc = json.loads(load(os.path.join(d, "receipts", "expected.json")))
|
||||
for c in rc["cases"]:
|
||||
verify_v1_receipt(rep, f"receipts/{c['file']}", load(os.path.join(d, "receipts", c["file"])), c["receipt_hash"])
|
||||
verify_v1_receipt(rep, f"receipts/{c['file']}", load(os.path.join(d, "receipts", c["file"])), c["receipt_hash"],
|
||||
c.get("signature") == "refused")
|
||||
rep.check(tree(os.path.join(d, "receipts")) == sorted([c["file"] for c in rc["cases"]] + ["expected.json"]),
|
||||
"receipts/: every receipt is a case")
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,24 @@
|
|||
{
|
||||
"file": "v1-unmetered.json",
|
||||
"receipt_hash": "2e9a0a221436ae3b7161b67c0d3215d8bed8f4c2f1a0b27ad786762120f71f31"
|
||||
},
|
||||
{
|
||||
"file": "v1-key-identity.json",
|
||||
"receipt_hash": "60731f7c63ea66eb14850f2578714ed83548914f5722ce1f1400416d03523106",
|
||||
"signature": "refused",
|
||||
"about": "the identity point as the key, with (R, S) = (B, 1): the equation holds for every message, and the key is of small order"
|
||||
},
|
||||
{
|
||||
"file": "v1-key-identity-y-not-reduced.json",
|
||||
"receipt_hash": "9a0167d70cb2bb27b05431c05f4fa907ca47beff5fd21df80533f49b368922f3",
|
||||
"signature": "refused",
|
||||
"about": "the identity spelled y = p + 1, with (R, S) = (B, 1): a decoder that reduces y reads the identity, and the encoding is not canonical"
|
||||
},
|
||||
{
|
||||
"file": "v1-r-small-order.json",
|
||||
"receipt_hash": "4b9ebabaf3d1cd22bf5ed0335840913c3162adc6d9315a587523c2654e0d6cdf",
|
||||
"signature": "refused",
|
||||
"about": "the base point as the key (its discrete logarithm is 1), R the identity and S = k: the equation holds, and R is of small order"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
{"asset_id":"asset","energy_provenance":null,"grant":{"capability":"video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","parent_receipt_hash":"0303030303030303030303030303030303030303030303030303030303030303","receipt_hash":"9a0167d70cb2bb27b05431c05f4fa907ca47beff5fd21df80533f49b368922f3","segment_map_root":"941704ebc6c0adc3394ebda28f50b3d6cf38cfc0fcd265a861d002365294829f","segments":[{"byte_len":16,"content_hash":"9f63cd63fcfb34a53f2113045123c0309bef7f8eb74dc1ccda2c17c1369558ab","duration_ms":2000,"index":0},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":1},{"byte_len":16,"content_hash":"864d8e31f5b44bde825e01963d73d6047b7acfae6ddaa64aa85ff0a47995e176","duration_ms":2000,"index":2},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":3}],"sig":"58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000","signer_id":"o","signer_pubkey":"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f","stored_bytes":48,"total_bytes":64,"total_segments":4,"unique_segments":3,"version":1,"work_segment_ops":4}
|
||||
1
wai/pack-conformance/receipts/v1-key-identity.json
Normal file
1
wai/pack-conformance/receipts/v1-key-identity.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"asset_id":"asset","energy_provenance":null,"grant":{"capability":"video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","parent_receipt_hash":"0303030303030303030303030303030303030303030303030303030303030303","receipt_hash":"60731f7c63ea66eb14850f2578714ed83548914f5722ce1f1400416d03523106","segment_map_root":"941704ebc6c0adc3394ebda28f50b3d6cf38cfc0fcd265a861d002365294829f","segments":[{"byte_len":16,"content_hash":"9f63cd63fcfb34a53f2113045123c0309bef7f8eb74dc1ccda2c17c1369558ab","duration_ms":2000,"index":0},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":1},{"byte_len":16,"content_hash":"864d8e31f5b44bde825e01963d73d6047b7acfae6ddaa64aa85ff0a47995e176","duration_ms":2000,"index":2},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":3}],"sig":"58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000","signer_id":"o","signer_pubkey":"0100000000000000000000000000000000000000000000000000000000000000","stored_bytes":48,"total_bytes":64,"total_segments":4,"unique_segments":3,"version":1,"work_segment_ops":4}
|
||||
1
wai/pack-conformance/receipts/v1-r-small-order.json
Normal file
1
wai/pack-conformance/receipts/v1-r-small-order.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"asset_id":"asset","energy_provenance":null,"grant":{"capability":"video.package","funds_ceiling":null,"grant_hash":"0000000000000000000000000000000000000000000000000000000000000000","joule_ceiling_micro":null},"joules_micro":0,"kind":"package","parent_receipt_hash":"0303030303030303030303030303030303030303030303030303030303030303","receipt_hash":"4b9ebabaf3d1cd22bf5ed0335840913c3162adc6d9315a587523c2654e0d6cdf","segment_map_root":"941704ebc6c0adc3394ebda28f50b3d6cf38cfc0fcd265a861d002365294829f","segments":[{"byte_len":16,"content_hash":"9f63cd63fcfb34a53f2113045123c0309bef7f8eb74dc1ccda2c17c1369558ab","duration_ms":2000,"index":0},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":1},{"byte_len":16,"content_hash":"864d8e31f5b44bde825e01963d73d6047b7acfae6ddaa64aa85ff0a47995e176","duration_ms":2000,"index":2},{"byte_len":16,"content_hash":"6897f7480bfd8cc5aaefaf33912d5f643ec6eae4450b593e98cd2ec4de18bcf2","duration_ms":2000,"index":3}],"sig":"01000000000000000000000000000000000000000000000000000000000000002722c1477d52c8f68054b88dc39ea3b8958f5a6f15927aea8e8a7a6c3350e00a","signer_id":"o","signer_pubkey":"5866666666666666666666666666666666666666666666666666666666666666","stored_bytes":48,"total_bytes":64,"total_segments":4,"unique_segments":3,"version":1,"work_segment_ops":4}
|
||||
|
|
@ -175,8 +175,7 @@ def verifies(pk: bytes, sig: bytes, msg: bytes) -> bool:
|
|||
|
||||
|
||||
def key_accepted(pk: bytes) -> bool:
|
||||
a = CV.point_decode(pk)
|
||||
return a is not None and not CV.small_order(a)
|
||||
return CV.ED.strict_key(pk)
|
||||
|
||||
|
||||
def commitment(k: bytes) -> bytes:
|
||||
|
|
|
|||
|
|
@ -2584,6 +2584,42 @@
|
|||
},
|
||||
"expected": "not_anchored reason=key_log_signature_invalid"
|
||||
},
|
||||
{
|
||||
"name": "anchor_root_identity_forged",
|
||||
"about": "the identity as root, with (R, S) = (B, 1): R is of large order, and the equation holds over any revision; the root is of small order",
|
||||
"procedure": "anchor",
|
||||
"context": {
|
||||
"origin": "https://publisher.example",
|
||||
"log": "{\"kind\":\"wai-key-log\",\"revisions\":[{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"0100000000000000000000000000000000000000000000000000000000000000\",\"seq\":0,\"sig\":\"58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000\",\"status_max_age_s\":3600}]}",
|
||||
"path": "fetched",
|
||||
"acquired_at": "2026-09-25T00:00:00Z"
|
||||
},
|
||||
"expected": "not_anchored reason=key_log_signature_invalid"
|
||||
},
|
||||
{
|
||||
"name": "anchor_root_identity_y_not_reduced",
|
||||
"about": "the identity spelled y = p + 1 as root, with (R, S) = (B, 1): a decoder that reduces y reads the identity; the encoding is not canonical",
|
||||
"procedure": "anchor",
|
||||
"context": {
|
||||
"origin": "https://publisher.example",
|
||||
"log": "{\"kind\":\"wai-key-log\",\"revisions\":[{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f\",\"seq\":0,\"sig\":\"58666666666666666666666666666666666666666666666666666666666666660100000000000000000000000000000000000000000000000000000000000000\",\"status_max_age_s\":3600}]}",
|
||||
"path": "fetched",
|
||||
"acquired_at": "2026-09-25T00:00:00Z"
|
||||
},
|
||||
"expected": "not_anchored reason=key_log_signature_invalid"
|
||||
},
|
||||
{
|
||||
"name": "anchor_root_signature_r_small_order",
|
||||
"about": "the base point as root (its discrete logarithm is 1), R the identity and S = k: the equation holds, and R is of small order",
|
||||
"procedure": "anchor",
|
||||
"context": {
|
||||
"origin": "https://publisher.example",
|
||||
"log": "{\"kind\":\"wai-key-log\",\"revisions\":[{\"grant_max_s\":43200,\"issued_at\":\"2026-09-01T00:00:00Z\",\"keys\":[{\"compromised_since\":null,\"id\":\"d1\",\"key\":\"a7f6dfaf8f38b89ba8ce649b594f91e4d01fdc57f9c9493df43b5e50a9987367\",\"may_sign\":[\"delivery\",\"receipt\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"m1\",\"key\":\"2bc2800b3316e009209ffd757dab19ccf0ae84bc7ae90654e1e81712d270f653\",\"may_sign\":[\"energy/OnChipCounter\",\"energy/ModelBased\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"op\",\"key\":\"d62f016a1efd1e4fdf793eb42cd84471e1ba9f0cf04d1287b5cc71f616287cb8\",\"may_sign\":[\"lawful-basis/declaration\"],\"status\":\"active\",\"until\":null},{\"compromised_since\":null,\"id\":\"modd\",\"key\":\"8320a51977d8c38ca8a4927c670df5821e449761945e15e9efb26a1509d230ea\",\"may_sign\":[\"module/decoder\"],\"status\":\"active\",\"until\":null}],\"kind\":\"wai-key-set\",\"next_root\":\"37332d293232e6e91c92aaccdd9fe2295a77447c7f4cd9df7e306b602643f5ac\",\"origin\":\"https://publisher.example\",\"prev\":null,\"revoked_modules\":[],\"root\":\"5866666666666666666666666666666666666666666666666666666666666666\",\"seq\":0,\"sig\":\"010000000000000000000000000000000000000000000000000000000000000025bb71609e49e43d53441bb54c5216aaf56400620aa38a2f8871ebbea267dc07\",\"status_max_age_s\":3600}]}",
|
||||
"path": "fetched",
|
||||
"acquired_at": "2026-09-25T00:00:00Z"
|
||||
},
|
||||
"expected": "not_anchored reason=key_log_signature_invalid"
|
||||
},
|
||||
{
|
||||
"name": "anchor_stranger_root",
|
||||
"about": "a revision signed by a root the previous one neither uses nor committed to",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
//! - `catalogs/`: indexes no golden package has (refinements on more than one deadline)
|
||||
//! with the MoQ catalog each gives (§8.4).
|
||||
//! - `receipts/`: version-1 package receipts as JSON (packaging §9.3), each with its
|
||||
//! receipt hash: the legacy receipt whose hash `wai-rs` pins, and an unmetered one.
|
||||
//! receipt hash: the legacy receipt whose hash `wai-rs` pins, an unmetered one, and
|
||||
//! three whose signature every verifier refuses (SPEC §7 *Signatures*).
|
||||
//! - `package-mutations/`: packages a verifier must refuse (§11.3), each the golden
|
||||
//! altered and re-sealed with its key where the receipt or `LISTING` alone would catch
|
||||
//! it, held as an overlay on the golden (`<name>/`, the files that differ or are new,
|
||||
|
|
@ -424,6 +425,20 @@ pub fn mutations() -> (Files, Value) {
|
|||
r.sig[0] ^= 1;
|
||||
r.to_json()
|
||||
}, "receipt/index-v.wsix"));
|
||||
// The rule every WAI signature is held to (SPEC §7 *Signatures*): three signatures over
|
||||
// the good receipt's payload, under the key each names, that a verifier checking only
|
||||
// the equation accepts (`wai::sig::forgeries`): a key of small order, the same key
|
||||
// spelled with y = p + 1, and R of small order.
|
||||
let forged = wai::sig::forgeries(|k| {
|
||||
let mut r = ok.clone();
|
||||
r.signer_pubkey = *k;
|
||||
r.signing_payload()
|
||||
});
|
||||
for (f, name) in forged.iter().zip(["key-identity", "key-identity-y-not-reduced", "r-small-order"]) {
|
||||
let mut r = ok.clone();
|
||||
(r.signer_pubkey, r.sig) = (f.key, f.sig);
|
||||
receipts.push((name, "receipt-signature", r.to_json(), "receipt/index-v.wsix"));
|
||||
}
|
||||
receipts.push(("unknown-member", "receipt-json", ok.to_json().replacen('{', "{\"extra\":1,", 1), "receipt/index-v.wsix"));
|
||||
// §9.1's reader: a member named twice, -0, a lone surrogate, null in place of a value,
|
||||
// energy_provenance left out, evidence beside no class.
|
||||
|
|
@ -1005,7 +1020,9 @@ pub fn apply_overlay(g: &Files, overlay: &Files, name: &str, remove: &[String])
|
|||
// ─────────────────────────────── version-1 receipts ───────────────────────────────
|
||||
|
||||
/// The version-1 receipts and their JSON: the legacy receipt `wai-rs` pins
|
||||
/// (`71a9da59…`), and an unmetered one with a parent.
|
||||
/// (`71a9da59…`), an unmetered one with a parent, and the unmetered one carrying each
|
||||
/// of the three signatures a verifier checking only the equation accepts
|
||||
/// (`wai::sig::forgeries`), which every verifier refuses (`"signature": "refused"`).
|
||||
#[allow(deprecated)] // the legacy label is the point of the first
|
||||
pub fn v1_receipts() -> (Files, Value) {
|
||||
use wai::wai_video_package::{segment, PackageReceipt};
|
||||
|
|
@ -1015,11 +1032,23 @@ pub fn v1_receipts() -> (Files, Value) {
|
|||
let unmetered = PackageReceipt::seal(&SigningKey::from_bytes(&[2; 32]), "o", "asset", segment(&src, 2000), 0, None, g, Some([3; 32]));
|
||||
let mut files = BTreeMap::new();
|
||||
let mut cases = Vec::new();
|
||||
for (name, r) in [("v1-legacy", legacy), ("v1-unmetered", unmetered)] {
|
||||
let forged = wai::sig::forgeries(|k| {
|
||||
let mut r = unmetered.clone();
|
||||
r.signer_pubkey = *k;
|
||||
r.signing_payload()
|
||||
});
|
||||
for (name, r) in [("v1-legacy", legacy), ("v1-unmetered", unmetered.clone())] {
|
||||
let path = format!("{name}.json");
|
||||
files.insert(path.clone(), r.to_json().expect("a JSON form").into_bytes());
|
||||
cases.push(json!({"file": path, "receipt_hash": hex(&r.receipt_hash())}));
|
||||
}
|
||||
for (f, name) in forged.iter().zip(["v1-key-identity", "v1-key-identity-y-not-reduced", "v1-r-small-order"]) {
|
||||
let mut r = unmetered.clone();
|
||||
(r.signer_pubkey, r.sig) = (f.key, f.sig);
|
||||
let path = format!("{name}.json");
|
||||
files.insert(path.clone(), r.to_json().expect("a JSON form").into_bytes());
|
||||
cases.push(json!({"file": path, "receipt_hash": hex(&r.receipt_hash()), "signature": "refused", "about": f.about}));
|
||||
}
|
||||
(files, json!({"cases": cases}))
|
||||
}
|
||||
|
||||
|
|
@ -1141,9 +1170,10 @@ pub fn check(dir: &Path) -> std::io::Result<Vec<String>> {
|
|||
}
|
||||
for c in expected["cases"].as_array().into_iter().flatten() {
|
||||
let f = c["file"].as_str().unwrap_or("");
|
||||
let ok = held.get(f).and_then(|b| std::str::from_utf8(b).ok()).and_then(|t| wai::wai_video_package::PackageReceipt::from_json(t).ok()).is_some_and(|r| r.verify() && hex(&r.receipt_hash()) == c["receipt_hash"]);
|
||||
let refused = c["signature"] == "refused";
|
||||
let ok = held.get(f).and_then(|b| std::str::from_utf8(b).ok()).and_then(|t| wai::wai_video_package::PackageReceipt::from_json(t).ok()).is_some_and(|r| r.verify() != refused && hex(&r.receipt_hash()) == c["receipt_hash"]);
|
||||
if !ok {
|
||||
out.push(format!("receipts/{f}: does not read back to a receipt that verifies with its hash"));
|
||||
out.push(format!("receipts/{f}: does not read back to a receipt that {} with its hash", if refused { "is refused" } else { "verifies" }));
|
||||
}
|
||||
}
|
||||
let mut held = read_tree(&dir.join("catalogs"))?;
|
||||
|
|
|
|||
|
|
@ -274,7 +274,9 @@ fn forged_body(view: &wai::energy_figure::FigureView) -> wai::claim_receipt::Cla
|
|||
}
|
||||
|
||||
/// Test 18: the golden's receipt JSON is RFC 8785 canonical and reads back to itself; the
|
||||
/// version-1 goldens read back to receipts that verify with their pinned hashes.
|
||||
/// version-1 goldens read back to receipts that verify with their pinned hashes, but for
|
||||
/// those marked `"signature": "refused"`, whose signature WAI's rule refuses (SPEC §7
|
||||
/// *Signatures*).
|
||||
#[test]
|
||||
fn the_receipts_json_round_trips() {
|
||||
let pkg = golden_package();
|
||||
|
|
@ -292,9 +294,10 @@ fn the_receipts_json_round_trips() {
|
|||
for c in expected["cases"].as_array().unwrap() {
|
||||
let t = std::str::from_utf8(&files[c["file"].as_str().unwrap()]).unwrap();
|
||||
let r1 = wai::wai_video_package::PackageReceipt::from_json(t).unwrap();
|
||||
assert!(r1.verify());
|
||||
assert_eq!(r1.verify(), c["signature"] != "refused", "{}", c["file"]);
|
||||
assert_eq!(wai_pack::hex(&r1.receipt_hash()), c["receipt_hash"].as_str().unwrap());
|
||||
assert_eq!(r1.to_json().unwrap(), t);
|
||||
}
|
||||
assert!(expected["cases"].as_array().unwrap().iter().any(|c| c["receipt_hash"] == "71a9da59a3202abb2c3fd22ee692b1485a9c674bb8c311f4c82455e6be29ae2b"), "the legacy pin is unchanged");
|
||||
assert_eq!(expected["cases"].as_array().unwrap().iter().filter(|c| c["signature"] == "refused").count(), 3, "a key of small order, its second spelling, an R of small order");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -148,9 +148,13 @@ neural = ["neural_int", "dep:ort", "dep:ndarray", "dep:zstd-safe"]
|
|||
# regenerate-at-sink. Reuses the ONNX runtime for the reference CLIP embedder;
|
||||
# the generator is always sink-supplied. Off by default like `neural`.
|
||||
semantic = ["codecs", "dep:ort", "dep:ndarray"]
|
||||
# Ed25519 verified as WAI verifies every signature (SPEC §7 *Signatures*, `src/sig.rs`):
|
||||
# the dependency on its own, so the one verifier builds exactly when ed25519-dalek does
|
||||
# and asks for no feature its caller does not need. Every signing feature enables it.
|
||||
ed25519 = ["dep:ed25519-dalek"]
|
||||
# Interactive worlds (extensions/interactive-worlds): media that consumes
|
||||
# input. Pure-Rust, no ML dep chain — the sim is closed-form fixed-point.
|
||||
world = ["dep:blake3", "dep:ed25519-dalek"]
|
||||
world = ["dep:blake3", "ed25519"]
|
||||
# Object-based spatial audio (extensions/spatial-audio): the deterministic
|
||||
# Fx mixdown engine. Reuses the world `Fx` floor + blake3; no codec/ML deps
|
||||
# (sources arrive as decoded mono PCM — decoding the wai.audio.* assets is
|
||||
|
|
@ -175,7 +179,7 @@ provenance = ["world"]
|
|||
# receipts that grade what a unit of work cost (projected/simulated/derived/measured),
|
||||
# content-address and chain it, and sign it. Conformance is reason-set equality with
|
||||
# the reference verifier and the portable C kernel over oer-conformance/.
|
||||
oer = ["dep:blake3", "dep:ed25519-dalek", "dep:num-bigint", "serde_json/float_roundtrip"]
|
||||
oer = ["dep:blake3", "ed25519", "dep:num-bigint", "serde_json/float_roundtrip"]
|
||||
# jwp-receipts §2.1's object content hash (src/jwp_hash.rs), on its own so any
|
||||
# receipt family that names an object can compute it with BLAKE3 alone.
|
||||
jwp_hash = ["dep:blake3"]
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ use crate::energy_class::{
|
|||
check_label, label_coheres, labelled_domain, write_label, AcquisitionError,
|
||||
EnergyProvenance,
|
||||
};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
const DOMAIN_RECEIPT: &[u8] = b"wai:audio-receipt\x01";
|
||||
const DOMAIN_BLOCK: &[u8] = b"wai:audio-block\x01";
|
||||
|
|
@ -141,8 +141,7 @@ impl SceneReceipt {
|
|||
if merkle_root(&leaves) != self.merkle_root {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Label the figure with its acquisition class and re-sign
|
||||
|
|
@ -245,4 +244,15 @@ mod tests {
|
|||
assert!(r0.verify() && r1.verify());
|
||||
assert_ne!(r0.receipt_hash(), r1.receipt_hash());
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let (scene, src) = scene_and_sources();
|
||||
let blocks = scene.block_hashes(&src, "stereo", 96).unwrap();
|
||||
let r = SceneReceipt::build(&key(9), "did:web:wai#audio", scene.scene_hash(), 0, blocks, 480, 1234, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ use crate::energy_class::{
|
|||
use crate::energy_figure::FigureView;
|
||||
use crate::energy_report::{Attribution, BracketSummary, BracketValues, BuildProfile, BuildRecord, ReportDefect};
|
||||
use std::collections::BTreeMap;
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use serde_json::{Map, Value};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
|
|
@ -2498,8 +2498,8 @@ impl ClaimReceipt {
|
|||
/// Ed25519 rule, then the class rules.
|
||||
pub fn verify(&self) -> Result<(), Rejection> {
|
||||
self.check_shape()?;
|
||||
verify_ed25519_strict(&self.signer_pubkey, &self.signing_input()?, &self.sig)
|
||||
.map_err(|r| Rejection::at(3, r))?;
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_input()?, &self.sig)
|
||||
.map_err(|e| Rejection::at(3, Reason::of_signature(e)))?;
|
||||
self.body
|
||||
.check_class_rules(self.subject_type, &self.subject)
|
||||
.map_err(|r| Rejection::at(4, r))
|
||||
|
|
@ -2804,50 +2804,19 @@ impl ClaimReceipt {
|
|||
}
|
||||
}
|
||||
|
||||
/// §3.1 step 3's key checks: `signer_pubkey` decodes under RFC 8032 §5.1.3 and is not of
|
||||
/// small order; `bad_signer_key` otherwise.
|
||||
///
|
||||
/// `VerifyingKey::from_bytes` is more lenient than RFC 8032 §5.1.3 in two ways: it reduces
|
||||
/// a `y` coordinate ≥ p modulo p, and it accepts `x` = 0 with the sign bit set. RFC 8032
|
||||
/// refuses both, which leaves every point exactly one encoding. A key therefore decodes
|
||||
/// under RFC 8032 exactly when it decodes here and re-encodes to the same 32 bytes.
|
||||
/// Without this check a `y` written as p + j would be a second spelling of the point with
|
||||
/// `y` = j, and would reach the signature check where RFC 8032 decoding has already
|
||||
/// refused the key: two verifiers, two reasons.
|
||||
///
|
||||
/// A key of small order (one whose order divides 8) is refused because the verification
|
||||
/// equation can then hold for a signature made without any private key. Under the
|
||||
/// identity key, every `R` = [s]B with `S` = s verifies.
|
||||
pub(crate) fn signer_key(bytes: &[u8; 32]) -> Result<VerifyingKey, Reason> {
|
||||
let key = VerifyingKey::from_bytes(bytes).map_err(|_| Reason::BadSignerKey)?;
|
||||
if key.to_edwards().compress().to_bytes() != *bytes || key.is_weak() {
|
||||
return Err(Reason::BadSignerKey);
|
||||
/// §3.1 step 3's Ed25519 rule is WAI's one rule (SPEC §7 *Signatures*, [`crate::sig`]):
|
||||
/// a key that is not the canonical encoding of a point of large order is
|
||||
/// `bad_signer_key`; `S` ≥ L, `R` of small order, and an equation that does not hold with
|
||||
/// `R` compared byte for byte are `signature_verify_failed`. A JWP group signature in
|
||||
/// either form (`crate::jwp_delivery`) and the documents of `crate::key_log` call the same
|
||||
/// routine and name its two outcomes with their own reasons.
|
||||
impl Reason {
|
||||
pub(crate) fn of_signature(e: crate::sig::SigError) -> Self {
|
||||
match e {
|
||||
crate::sig::SigError::BadKey => Reason::BadSignerKey,
|
||||
crate::sig::SigError::BadSignature => Reason::SignatureVerifyFailed,
|
||||
}
|
||||
}
|
||||
Ok(key)
|
||||
}
|
||||
|
||||
/// §3.1 step 3's Ed25519 rule over `msg`: the key decodes from its one canonical
|
||||
/// encoding and is not of small order (`bad_signer_key`, the checks of [`signer_key`]);
|
||||
/// then `S` < L, `R` is not of small order, and the cofactorless equation holds with `R`
|
||||
/// compared byte for byte (`signature_verify_failed`). One routine, so a claim, a JWP
|
||||
/// group signature in either group form, legacy or labelled (jwp-receipts §3 step 2,
|
||||
/// `crate::jwp_delivery`), and the documents of `crate::key_log` (key-log revisions,
|
||||
/// module grants and statuses, jwp-receipts §7) are held to the same rule, and every
|
||||
/// conforming verifier accepts the same signatures.
|
||||
pub(crate) fn verify_ed25519_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<(), Reason> {
|
||||
let key = signer_key(pubkey)?;
|
||||
// S < L (RFC 8032 §5.1.7), checked here as well as inside `verify_strict`: the
|
||||
// library's own range check shrinks to a top-three-bits test when any crate in a
|
||||
// build turns on its `legacy_compatibility` feature, and S + L passes that test.
|
||||
let s: [u8; 32] = sig[32..].try_into().expect("a signature is 64 bytes");
|
||||
if bool::from(curve25519_dalek::Scalar::from_canonical_bytes(s).is_none()) {
|
||||
return Err(Reason::SignatureVerifyFailed);
|
||||
}
|
||||
// `verify_strict` does the rest of the rule: R not of small order, and the
|
||||
// cofactorless equation checked by comparing the encoding of [S]B − [k]A with R byte
|
||||
// for byte (so a non-canonical R never verifies). Its own small-order test on the key
|
||||
// cannot fire here; `signer_key` refused those first.
|
||||
key.verify_strict(msg, &Signature::from_bytes(sig)).map_err(|_| Reason::SignatureVerifyFailed)
|
||||
}
|
||||
|
||||
/// An RFC 3339 `date-time` in UTC, to the second, for `seconds` since the Unix epoch:
|
||||
|
|
@ -3804,7 +3773,7 @@ mod corpus {
|
|||
fn off_curve_key() -> [u8; 32] {
|
||||
(1u8..0x7f)
|
||||
.map(|i| [i; 32])
|
||||
.find(|k| VerifyingKey::from_bytes(k).is_err())
|
||||
.find(|k| ed25519_dalek::VerifyingKey::from_bytes(k).is_err())
|
||||
.expect("half of all y values are off the curve")
|
||||
}
|
||||
|
||||
|
|
@ -6109,57 +6078,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/// §3.1 step 3's key rule over every encoding RFC 8032 §5.1.3 refuses that curve25519's
|
||||
/// decompression accepts — y = p + j for j = 0 … 18, and x = 0 with the sign bit set —
|
||||
/// and over the eight points of small order.
|
||||
#[test]
|
||||
fn a_key_is_refused_unless_canonical_and_of_large_order() {
|
||||
let mut lenient = 0;
|
||||
for j in 0u8..=18 {
|
||||
let mut b = [0xff; 32];
|
||||
b[0] = 0xed + j; // p + j, little-endian
|
||||
b[31] = 0x7f;
|
||||
if VerifyingKey::from_bytes(&b).is_ok() {
|
||||
lenient += 1;
|
||||
assert_eq!(signer_key(&b).err(), Some(Reason::BadSignerKey), "y = p + {j}");
|
||||
// The same point, spelt canonically, is refused only for being of small order.
|
||||
let mut c = [0u8; 32];
|
||||
c[0] = j;
|
||||
let canonical = VerifyingKey::from_bytes(&c).expect("the same point");
|
||||
assert_eq!(signer_key(&c).is_ok(), !canonical.is_weak(), "y = {j}");
|
||||
}
|
||||
}
|
||||
assert!(lenient > 2, "the leniency this rule closes is real: {lenient} encodings");
|
||||
let mut sign_set = [0u8; 32];
|
||||
sign_set[0] = 1;
|
||||
sign_set[31] = 0x80; // x = 0 (y = 1) with the sign bit set
|
||||
let mut minus_one = [0xff; 32];
|
||||
minus_one[0] = 0xec; // y = p - 1, x = 0, sign bit set
|
||||
for b in [sign_set, minus_one] {
|
||||
assert!(VerifyingKey::from_bytes(&b).is_ok());
|
||||
assert_eq!(signer_key(&b).err(), Some(Reason::BadSignerKey));
|
||||
}
|
||||
let small_order = [
|
||||
"0100000000000000000000000000000000000000000000000000000000000000",
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0000000000000000000000000000000000000000000000000000000000000080",
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85",
|
||||
];
|
||||
for h in small_order {
|
||||
let b: [u8; 32] = unhex(h).unwrap().try_into().unwrap();
|
||||
let key = VerifyingKey::from_bytes(&b).expect("a curve point");
|
||||
assert_eq!(key.to_edwards().compress().to_bytes(), b, "{h} is canonical");
|
||||
assert!(key.is_weak(), "{h} is of small order");
|
||||
assert_eq!(signer_key(&b).err(), Some(Reason::BadSignerKey), "{h}");
|
||||
}
|
||||
// A genuine key passes.
|
||||
assert!(signer_key(&k(1).verifying_key().to_bytes()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn claims_chain_per_signer() {
|
||||
let body = || energy_body(9, "ModelBased", "derived");
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ use crate::energy_class::{
|
|||
check_label, json_with_label, label_coheres, labelled_domain, write_label, AcquisitionError,
|
||||
EnergyProvenance,
|
||||
};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
const DOMAIN_RECEIPT: &[u8] = b"wai:film-receipt\x01";
|
||||
|
||||
|
|
@ -111,8 +111,7 @@ impl FilmReceipt {
|
|||
if merkle_root(&leaves) != self.merkle_root {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Label the figure with its acquisition class and re-sign
|
||||
|
|
@ -210,6 +209,15 @@ mod tests {
|
|||
assert_eq!(r1.parent_receipt_hash, Some(r0.receipt_hash()));
|
||||
assert_ne!(r0.receipt_hash(), r1.receipt_hash());
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let r = FilmReceipt::build(&key(9), "did:web:wai#film", [0xab; 32], 0, frames(), 5, 8080, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
||||
impl Film {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::energy_class::{
|
|||
check_label, json_with_label, label_coheres, labelled_domain, write_label, AcquisitionError,
|
||||
EnergyProvenance,
|
||||
};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
const DOMAIN_RECEIPT: &[u8] = b"wai:haptic-receipt\x01";
|
||||
const DOMAIN_BLOCK: &[u8] = b"wai:haptic-rblock\x01";
|
||||
|
|
@ -126,8 +126,7 @@ impl HapticReceipt {
|
|||
if merkle_root(&leaves) != self.merkle_root {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Label the figure with its acquisition class and re-sign
|
||||
|
|
@ -239,4 +238,14 @@ mod tests {
|
|||
assert_eq!(r1.parent_receipt_hash, Some(r0.receipt_hash()));
|
||||
assert_ne!(r0.receipt_hash(), r1.receipt_hash());
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let s = sig();
|
||||
let r = HapticReceipt::build(&key(9), "did:web:wai#haptic", s.signal_hash(), 0, s.channel_hashes(), 100, 1234, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@
|
|||
//! This crate seals group receipts only to generate its conformance corpus;
|
||||
//! [`GroupBuilder`] is not a publishing path.
|
||||
|
||||
use crate::claim_receipt::{verify_ed25519_strict, ClaimBody, ClaimReceipt, Reason as ClaimReason};
|
||||
use crate::claim_receipt::{ClaimBody, ClaimReceipt};
|
||||
use crate::energy_binding::Coverage;
|
||||
use crate::energy_class::{
|
||||
check_label, json_fields, label_from_json, AcquisitionError, AcquisitionKind, CounterFiltering, EnergyProvenance,
|
||||
|
|
@ -454,9 +454,9 @@ impl GroupReceipt {
|
|||
/// 3's strict rule (`signature_verify_failed`), then a labelled group's own
|
||||
/// checks (`group_label_invalid`).
|
||||
pub fn verify(&self) -> Result<(), DeliveryError> {
|
||||
verify_ed25519_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).map_err(|r| match r {
|
||||
ClaimReason::BadSignerKey => DeliveryError::at(1, Reason::BadSignerKey),
|
||||
_ => DeliveryError::at(2, Reason::SignatureVerifyFailed),
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).map_err(|e| match e {
|
||||
crate::sig::SigError::BadKey => DeliveryError::at(1, Reason::BadSignerKey),
|
||||
crate::sig::SigError::BadSignature => DeliveryError::at(2, Reason::SignatureVerifyFailed),
|
||||
})?;
|
||||
if let Some(l) = &self.label {
|
||||
l.check(self.joules_total).map_err(|_| DeliveryError::at(2, Reason::GroupLabelInvalid))?;
|
||||
|
|
|
|||
|
|
@ -508,6 +508,28 @@ fn vectors() -> Vec<V> {
|
|||
// Last again, so every vector before them keeps its place: holding, re-anchoring
|
||||
// and joining (§3 step 3).
|
||||
out.extend(recovery_vectors());
|
||||
// Last again, so every vector before them keeps its place: §3.1 step 3's rule is the
|
||||
// one every WAI signature is held to (SPEC §7 *Signatures*), and these are three
|
||||
// signatures over the mixed group's own payload, under the key each names, that a
|
||||
// verifier checking only the equation accepts.
|
||||
let forged = crate::sig::forgeries(|k| {
|
||||
let mut g = gm.clone();
|
||||
g.signer_pubkey = *k;
|
||||
g.signing_payload()
|
||||
});
|
||||
let under = |f: &crate::sig::Forgery| {
|
||||
let mut g = gm.clone();
|
||||
(g.signer_pubkey, g.sig) = (f.key, f.sig);
|
||||
json!({ "group": g.to_json() })
|
||||
};
|
||||
out.extend([
|
||||
v("group_signer_key_identity_forged", "the identity point as signer_pubkey, with (R, S) = (B, 1): the equation holds over any payload, so a verifier that checks only the equation accepts it; the key is of small order", "group",
|
||||
under(&forged[0]), reject(Some(1), Reason::BadSignerKey)),
|
||||
v("group_signer_key_identity_y_not_reduced", "the identity spelled y = p + 1, with (R, S) = (B, 1): a decoder that reduces y reads the identity, under which the equation holds; the encoding is not canonical", "group",
|
||||
under(&forged[1]), reject(Some(1), Reason::BadSignerKey)),
|
||||
v("group_signature_r_small_order", "signer_pubkey the base point, whose discrete logarithm is 1, R the identity and S = k: the equation holds, and R is of small order", "group",
|
||||
under(&forged[2]), reject(Some(2), Reason::SignatureVerifyFailed)),
|
||||
]);
|
||||
out
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,8 +36,7 @@
|
|||
//! and identity against values `signer-conformance/signer_verify.py` computed.
|
||||
|
||||
use crate::claim_receipt::{
|
||||
hex, is_lower_hex, jcs, parse_strict, rfc3339, signer_key, term_ok, verify_ed25519_strict, unhex, ClaimReceipt,
|
||||
MAX_INTEGER,
|
||||
hex, is_lower_hex, jcs, parse_strict, rfc3339, term_ok, unhex, ClaimReceipt, MAX_INTEGER,
|
||||
};
|
||||
use crate::signer_id::{self, Level};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
|
@ -340,7 +339,7 @@ impl KeyEntry {
|
|||
/// The constraints §7.2 states with the entry table.
|
||||
fn check(&self) -> Result<(), Reason> {
|
||||
let bad = Err(Reason::KeyLogMalformed);
|
||||
if !signer_id::key_id_ok(&self.id) || signer_key(&self.key).is_err() {
|
||||
if !signer_id::key_id_ok(&self.id) || crate::sig::strict_key(&self.key).is_err() {
|
||||
return bad;
|
||||
}
|
||||
if self.may_sign.is_empty()
|
||||
|
|
@ -544,7 +543,7 @@ impl Revision {
|
|||
if self.origin != origin {
|
||||
return Err(Reason::OriginMismatch);
|
||||
}
|
||||
verify_ed25519_strict(&self.root, &self.signing_input(), &self.sig).map_err(|_| Reason::KeyLogSignatureInvalid)
|
||||
crate::sig::verify_strict(&self.root, &self.signing_input(), &self.sig).map_err(|_| Reason::KeyLogSignatureInvalid)
|
||||
}
|
||||
|
||||
/// Whether this revision is a rotation of `parent`: signed by another root, the one
|
||||
|
|
@ -1366,7 +1365,7 @@ impl ModuleGrant {
|
|||
sig: s("sig").ok().as_deref().and_then(unhex).and_then(|b| b.try_into().ok()).ok_or(bad)?,
|
||||
};
|
||||
g.check()?;
|
||||
verify_ed25519_strict(&g.signer_pubkey, &g.signing_input(), &g.sig).map_err(|_| bad)?;
|
||||
crate::sig::verify_strict(&g.signer_pubkey, &g.signing_input(), &g.sig).map_err(|_| bad)?;
|
||||
Ok(g)
|
||||
}
|
||||
|
||||
|
|
@ -1486,7 +1485,7 @@ impl KeyStatus {
|
|||
self.origin == rev.origin
|
||||
&& self.head == rev.revision_hash()
|
||||
&& self.seq == rev.seq
|
||||
&& verify_ed25519_strict(&rev.root, &self.signing_input(), &self.sig).is_ok()
|
||||
&& crate::sig::verify_strict(&rev.root, &self.signing_input(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Sign a status for `head` under its root.
|
||||
|
|
|
|||
|
|
@ -1297,6 +1297,23 @@ fn vectors() -> Vec<V> {
|
|||
None,
|
||||
not_anchored("key_log_signature_invalid"),
|
||||
));
|
||||
// The rule every WAI signature is held to (SPEC §7 *Signatures*), one step at a time:
|
||||
// three signatures over the revision's own signing input, under the root each names,
|
||||
// that a verifier checking only the equation accepts.
|
||||
let unsigned_root = unsigned(P, None, "2026-09-01T00:00:00Z", R1, l.r0.keys.clone());
|
||||
let with_root = |key: [u8; 32], sig: [u8; 64]| {
|
||||
let mut r = unsigned_root.clone();
|
||||
(r.root, r.sig) = (key, sig);
|
||||
r
|
||||
};
|
||||
let forged = crate::sig::forgeries(|k| with_root(*k, [0; 64]).signing_input());
|
||||
for (f, name, about) in [
|
||||
(&forged[0], "anchor_root_identity_forged", "the identity as root, with (R, S) = (B, 1): R is of large order, and the equation holds over any revision; the root is of small order"),
|
||||
(&forged[1], "anchor_root_identity_y_not_reduced", "the identity spelled y = p + 1 as root, with (R, S) = (B, 1): a decoder that reduces y reads the identity; the encoding is not canonical"),
|
||||
(&forged[2], "anchor_root_signature_r_small_order", "the base point as root (its discrete logarithm is 1), R the identity and S = k: the equation holds, and R is of small order"),
|
||||
] {
|
||||
v.push(anchor_vec(name, about, P, log_json(&[with_root(f.key, f.sig)]), "fetched", None, not_anchored("key_log_signature_invalid")));
|
||||
}
|
||||
let st1 = Revision::seal_unchecked(&key(STRANGER), unsigned(P, Some(&l.r0), "2026-09-10T00:00:00Z", R1, l.r1.keys.clone()));
|
||||
v.push(anchor_vec(
|
||||
"anchor_stranger_root",
|
||||
|
|
|
|||
|
|
@ -312,9 +312,10 @@ pub mod energy_binding;
|
|||
pub mod c2pa_emit;
|
||||
|
||||
// Ed25519 verified strictly (RFC 8032 §5.1.7 with a canonical key of large order, S < L,
|
||||
// and R of large order compared byte for byte): the one routine every receipt family's
|
||||
// signature check is to move onto. Needs ed25519-dalek, which `world` and `oer` bring.
|
||||
#[cfg(any(feature = "world", feature = "oer"))]
|
||||
// and R of large order compared byte for byte, SPEC §7 *Signatures*): the one routine
|
||||
// every signature check written in this crate calls but OER/2's (oer-receipts §5). Built
|
||||
// whenever ed25519-dalek is, under the `ed25519` feature every signing feature enables.
|
||||
#[cfg(feature = "ed25519")]
|
||||
pub mod sig;
|
||||
|
||||
// RFC 8785 canonical JSON over the subset WAI signs and digests (integers up to
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
//!
|
||||
//! Integers are big-endian, as in every receipt of the family.
|
||||
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use serde_json::{json, Map, Value};
|
||||
|
||||
use crate::energy_class::{
|
||||
|
|
@ -447,10 +447,12 @@ impl PackageReceiptV2 {
|
|||
}
|
||||
check_parent(self.parent, &parsed)?;
|
||||
self.check_energy()?;
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
|
||||
return err("receipt-signature", "signer_pubkey is not an Ed25519 key");
|
||||
};
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).map_err(|_| PackageReason::new("receipt-signature", "the signature does not verify"))
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).map_err(|e| {
|
||||
PackageReason::new("receipt-signature", match e {
|
||||
crate::sig::SigError::BadKey => "signer_pubkey is not a canonical Ed25519 key of large order",
|
||||
crate::sig::SigError::BadSignature => "the signature does not verify",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn json_value(&self) -> Result<Value, String> {
|
||||
|
|
|
|||
|
|
@ -313,3 +313,23 @@ fn the_v2_figure_view_and_its_measurement_claim() {
|
|||
let uview = (&u, ug.as_slice()).figure_view().unwrap();
|
||||
assert!(meter_report::emit(&uview, None, None, crate::energy_report::BuildRecord::of_this_build(), "model", &key(1), "x", None, "2026-10-06T00:00:00Z").is_err());
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each `receipt-signature`,
|
||||
/// though a lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let (r, ix) = seal((0, None), Parent::None);
|
||||
let good = refs(&ix);
|
||||
let set = |r: &mut PackageReceiptV2, k, s| (r.signer_pubkey, r.sig) = (k, s);
|
||||
crate::sig::assert_refuses_forgeries(&r, set, |r| r.signing_payload(), |r| r.verify_detailed(&good).is_ok());
|
||||
for f in crate::sig::forgeries(|k| {
|
||||
let mut g = r.clone();
|
||||
set(&mut g, *k, [0; 64]);
|
||||
g.signing_payload()
|
||||
}) {
|
||||
let mut g = r.clone();
|
||||
set(&mut g, f.key, f.sig);
|
||||
assert_eq!(g.verify_detailed(&good).map_err(|e| e.code), Err("receipt-signature"), "{}", f.name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ use crate::energy_class::{
|
|||
write_optional, AcquisitionError, EnergyProvenance,
|
||||
};
|
||||
use crate::world::receipt::merkle_root;
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
const DOMAIN_RECEIPT: &[u8] = b"wai:asset-receipt\x01";
|
||||
const DOMAIN_STEP: &[u8] = b"wai:prov-step\x01";
|
||||
|
|
@ -247,8 +247,7 @@ impl AssetReceipt {
|
|||
if !self.steps.iter().all(|s| label_coheres(s.joules_micro, s.energy_provenance.as_ref())) {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Confirm the receipt vouches for exactly this asset envelope:
|
||||
|
|
@ -458,4 +457,14 @@ mod tests {
|
|||
assert_eq!(r1.parent_receipt_hash, Some(r0.receipt_hash()));
|
||||
assert_ne!(r0.receipt_hash(), r1.receipt_hash());
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let (steps, asset) = chain();
|
||||
let r = AssetReceipt::build(&key(9), "did:web:wai#prov", asset, "image", "wai.image.jpeg", 0, steps, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ use crate::energy_class::{
|
|||
check_label, json_with_label, label_coheres, labelled_domain, write_label, AcquisitionError,
|
||||
EnergyProvenance,
|
||||
};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
const DOMAIN_RECEIPT: &[u8] = b"wai:score-receipt\x01";
|
||||
const DOMAIN_BLOCK: &[u8] = b"wai:score-rblock\x01";
|
||||
|
|
@ -146,8 +146,7 @@ impl ScoreReceipt {
|
|||
if merkle_root(&leaves) != self.merkle_root {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Label the figure with its acquisition class and re-sign
|
||||
|
|
@ -274,4 +273,14 @@ mod tests {
|
|||
assert_eq!(r1.parent_receipt_hash, Some(r0.receipt_hash()));
|
||||
assert_ne!(r0.receipt_hash(), r1.receipt_hash());
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let s = score();
|
||||
let r = ScoreReceipt::build(&key(9), "did:web:wai#score", s.score_hash(), 0, s.segment_hashes(), 20000, 4321, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
//! Ed25519 verification as WAI verifies it (RFC 8032 §5.1.7, strictly): one routine, so
|
||||
//! every receipt family that moves onto it accepts exactly the signatures every other
|
||||
//! conforming verifier accepts.
|
||||
//! Ed25519 verification as WAI verifies it (RFC 8032 §5.1.7, strictly; SPEC §7
|
||||
//! *Signatures*): one routine, so every receipt family accepts exactly the signatures every
|
||||
//! other conforming verifier accepts. Every signature check written in this crate calls it,
|
||||
//! except OER/2's (`crate::oer`), whose rule oer-receipts §5 settles: it refuses the same
|
||||
//! keys and leaves `R` to RFC 8032. The quantum receipts this crate re-exports are checked
|
||||
//! in the `wai-quantum` crate, which does not call it.
|
||||
//!
|
||||
//! The rule, in order:
|
||||
//!
|
||||
|
|
@ -78,16 +81,152 @@ pub fn verify_strict(pubkey: &[u8; 32], msg: &[u8], sig: &[u8; 64]) -> Result<()
|
|||
key.verify_strict(msg, &Signature::from_bytes(sig)).map_err(|_| SigError::BadSignature)
|
||||
}
|
||||
|
||||
/// The identity point, canonically encoded (y = 1).
|
||||
pub const IDENTITY: [u8; 32] = {
|
||||
let mut b = [0u8; 32];
|
||||
b[0] = 1;
|
||||
b
|
||||
};
|
||||
|
||||
/// The identity spelled with y = p + 1: a decoder that reduces `y` reads the identity.
|
||||
pub const IDENTITY_Y_NOT_REDUCED: [u8; 32] = {
|
||||
let mut b = [0xffu8; 32];
|
||||
b[0] = 0xee;
|
||||
b[31] = 0x7f;
|
||||
b
|
||||
};
|
||||
|
||||
/// The base point B, encoded. As a key its discrete logarithm is 1.
|
||||
pub const BASE_POINT: [u8; 32] = {
|
||||
let mut b = [0x66u8; 32];
|
||||
b[0] = 0x58;
|
||||
b
|
||||
};
|
||||
|
||||
/// `bytes` (little-endian, any length) mod L, by binary long division.
|
||||
fn mod_l(bytes: &[u8]) -> [u8; 32] {
|
||||
let mut r = [0u8; 33];
|
||||
for i in (0..bytes.len() * 8).rev() {
|
||||
let mut carry = (bytes[i / 8] >> (i % 8)) & 1;
|
||||
for b in r.iter_mut() {
|
||||
let top = *b >> 7;
|
||||
*b = (*b << 1) | carry;
|
||||
carry = top;
|
||||
}
|
||||
let lx = |j: usize| if j < 32 { L[j] } else { 0 };
|
||||
if (0..33).rev().find(|&j| r[j] != lx(j)).is_none_or(|j| r[j] > lx(j)) {
|
||||
let mut borrow = 0i16;
|
||||
for j in 0..33 {
|
||||
let v = r[j] as i16 - lx(j) as i16 - borrow;
|
||||
r[j] = v.rem_euclid(256) as u8;
|
||||
borrow = i16::from(v < 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
r[..32].try_into().expect("the remainder is below L")
|
||||
}
|
||||
|
||||
/// A signature a lenient verifier accepts and [`verify_strict`] refuses, for the key a
|
||||
/// receipt names: what a test or a conformance corpus uses to show a receipt family
|
||||
/// verifies strictly. None of them needs a private key.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Forgery {
|
||||
/// A short name a corpus can build a vector name from.
|
||||
pub name: &'static str,
|
||||
/// What it is, in a sentence a corpus can carry.
|
||||
pub about: &'static str,
|
||||
/// The key the receipt names.
|
||||
pub key: [u8; 32],
|
||||
/// The signature it carries.
|
||||
pub sig: [u8; 64],
|
||||
/// Why [`verify_strict`] refuses it.
|
||||
pub refusal: SigError,
|
||||
}
|
||||
|
||||
/// The three forgeries over `payload(key)`, the bytes a receipt signs once its key is
|
||||
/// `key`. Each satisfies the cofactorless equation with `R` compared byte for byte, so
|
||||
/// ed25519-dalek's lenient `verify` accepts it, which this asserts. That [`verify_strict`]
|
||||
/// refuses each is left to the caller to show, so a test of a signing site fails at the
|
||||
/// site when the site, or the rule, lets one through. Each fails one step of the rule:
|
||||
///
|
||||
/// - `key_identity`: the key is the identity and (R, S) = (B, 1), which verifies over any
|
||||
/// message (step 2);
|
||||
/// - `key_identity_y_not_reduced`: the same, with the identity spelled y = p + 1 (step 1,
|
||||
/// and step 2 under a decoder that reduces `y`);
|
||||
/// - `r_small_order`: the key is B, whose discrete logarithm is 1, `R` is the identity and
|
||||
/// `S` = k, so [S]B = R + [k]B (step 4: `R` of small order).
|
||||
pub fn forgeries(payload: impl Fn(&[u8; 32]) -> Vec<u8>) -> [Forgery; 3] {
|
||||
use ed25519_dalek::Verifier;
|
||||
use sha2::{Digest, Sha512};
|
||||
let sig = |r: [u8; 32], s: [u8; 32]| {
|
||||
let mut g = [0u8; 64];
|
||||
g[..32].copy_from_slice(&r);
|
||||
g[32..].copy_from_slice(&s);
|
||||
g
|
||||
};
|
||||
let r_small = {
|
||||
let m = payload(&BASE_POINT);
|
||||
let k = mod_l(&Sha512::new().chain_update(IDENTITY).chain_update(BASE_POINT).chain_update(&m).finalize());
|
||||
sig(IDENTITY, k)
|
||||
};
|
||||
let all = [
|
||||
Forgery {
|
||||
name: "key_identity",
|
||||
about: "the identity point as the key, with (R, S) = (B, 1): the equation holds for every message, and the key is of small order",
|
||||
key: IDENTITY,
|
||||
sig: sig(BASE_POINT, IDENTITY),
|
||||
refusal: SigError::BadKey,
|
||||
},
|
||||
Forgery {
|
||||
name: "key_identity_y_not_reduced",
|
||||
about: "the identity spelled y = p + 1, with (R, S) = (B, 1): a decoder that reduces y reads the identity, and the encoding is not canonical",
|
||||
key: IDENTITY_Y_NOT_REDUCED,
|
||||
sig: sig(BASE_POINT, IDENTITY),
|
||||
refusal: SigError::BadKey,
|
||||
},
|
||||
Forgery {
|
||||
name: "r_small_order",
|
||||
about: "the base point as the key (its discrete logarithm is 1), R the identity and S = k: the equation holds, and R is of small order",
|
||||
key: BASE_POINT,
|
||||
sig: r_small,
|
||||
refusal: SigError::BadSignature,
|
||||
},
|
||||
];
|
||||
for f in &all {
|
||||
let m = payload(&f.key);
|
||||
let lenient = VerifyingKey::from_bytes(&f.key).and_then(|k| k.verify(&m, &Signature::from_bytes(&f.sig)));
|
||||
assert!(lenient.is_ok(), "{}: a lenient verifier accepts the forgery", f.name);
|
||||
}
|
||||
all
|
||||
}
|
||||
|
||||
/// For a signing site's tests: `good` verifies, and each of [`forgeries`], set into it by
|
||||
/// `set` (the key and the signature it carries), is refused by `verifies`. `payload` is
|
||||
/// the bytes the site signs, computed after its key is set, since a payload may name it.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn assert_refuses_forgeries<R: Clone>(
|
||||
good: &R,
|
||||
set: impl Fn(&mut R, [u8; 32], [u8; 64]),
|
||||
payload: impl Fn(&R) -> Vec<u8>,
|
||||
verifies: impl Fn(&R) -> bool,
|
||||
) {
|
||||
assert!(verifies(good), "the site verifies what it signed");
|
||||
let with = |key: [u8; 32], sig: [u8; 64]| {
|
||||
let mut r = good.clone();
|
||||
set(&mut r, key, sig);
|
||||
r
|
||||
};
|
||||
for f in forgeries(|key| payload(&with(*key, [0; 64]))) {
|
||||
assert!(!verifies(&with(f.key, f.sig)), "{} is refused: {}", f.name, f.about);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
/// The base point B, encoded.
|
||||
const B: [u8; 32] = [
|
||||
0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
|
||||
];
|
||||
const B: [u8; 32] = BASE_POINT;
|
||||
|
||||
fn le(mut n: [u8; 32], add: u8) -> [u8; 32] {
|
||||
let mut carry = add as u16;
|
||||
|
|
@ -138,6 +277,80 @@ mod tests {
|
|||
assert_eq!(verify_strict(&signed, b"any", &forged), Err(SigError::BadKey));
|
||||
}
|
||||
|
||||
/// Steps 1 and 2 over every encoding RFC 8032 §5.1.3 refuses that curve25519's
|
||||
/// decompression accepts — y = p + j for j = 0 … 18, and x = 0 with the sign bit set —
|
||||
/// and over the eight points of small order.
|
||||
#[test]
|
||||
fn a_key_is_refused_unless_canonical_and_of_large_order() {
|
||||
let mut lenient = 0;
|
||||
for j in 0u8..=18 {
|
||||
let mut b = [0xff; 32];
|
||||
b[0] = 0xed + j; // p + j, little-endian
|
||||
b[31] = 0x7f;
|
||||
if VerifyingKey::from_bytes(&b).is_ok() {
|
||||
lenient += 1;
|
||||
assert_eq!(strict_key(&b).err(), Some(SigError::BadKey), "y = p + {j}");
|
||||
// The same point, spelt canonically, is refused only for being of small order.
|
||||
let mut c = [0u8; 32];
|
||||
c[0] = j;
|
||||
let canonical = VerifyingKey::from_bytes(&c).expect("the same point");
|
||||
assert_eq!(strict_key(&c).is_ok(), !canonical.is_weak(), "y = {j}");
|
||||
}
|
||||
}
|
||||
assert!(lenient > 2, "the leniency this rule closes is real: {lenient} encodings");
|
||||
let mut sign_set = [0u8; 32];
|
||||
sign_set[0] = 1;
|
||||
sign_set[31] = 0x80; // x = 0 (y = 1) with the sign bit set
|
||||
let mut minus_one = [0xff; 32];
|
||||
minus_one[0] = 0xec; // y = p - 1, x = 0, sign bit set
|
||||
for b in [sign_set, minus_one] {
|
||||
assert!(VerifyingKey::from_bytes(&b).is_ok());
|
||||
assert_eq!(strict_key(&b).err(), Some(SigError::BadKey));
|
||||
}
|
||||
let small_order = [
|
||||
"0100000000000000000000000000000000000000000000000000000000000000",
|
||||
"ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f",
|
||||
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"0000000000000000000000000000000000000000000000000000000000000080",
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
||||
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
||||
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85",
|
||||
];
|
||||
for h in small_order {
|
||||
let b: [u8; 32] = (0..32).map(|i| u8::from_str_radix(&h[2 * i..2 * i + 2], 16).unwrap()).collect::<Vec<_>>().try_into().unwrap();
|
||||
let key = VerifyingKey::from_bytes(&b).expect("a curve point");
|
||||
assert_eq!(key.to_edwards().compress().to_bytes(), b, "{h} is canonical");
|
||||
assert!(key.is_weak(), "{h} is of small order");
|
||||
assert_eq!(strict_key(&b).err(), Some(SigError::BadKey), "{h}");
|
||||
}
|
||||
// A genuine key passes.
|
||||
assert!(strict_key(&SigningKey::from_bytes(&[1; 32]).verifying_key().to_bytes()).is_ok());
|
||||
}
|
||||
|
||||
/// Each forgery passes the lenient check and fails exactly the step it names, over a
|
||||
/// message that names its key as a receipt's signing payload does; the constants are
|
||||
/// the points they say.
|
||||
#[test]
|
||||
fn the_forgeries_pass_a_lenient_check_and_fail_their_step() {
|
||||
let payload = |k: &[u8; 32]| [b"a receipt signed under ".as_slice(), k].concat();
|
||||
let fs = forgeries(payload);
|
||||
for f in &fs {
|
||||
assert_eq!(verify_strict(&f.key, &payload(&f.key), &f.sig), Err(f.refusal), "{}", f.name);
|
||||
}
|
||||
let got: Vec<_> = fs.iter().map(|f| (f.name, f.refusal)).collect();
|
||||
assert_eq!(
|
||||
got,
|
||||
[("key_identity", SigError::BadKey), ("key_identity_y_not_reduced", SigError::BadKey), ("r_small_order", SigError::BadSignature)]
|
||||
);
|
||||
assert_eq!(IDENTITY_Y_NOT_REDUCED, le(p(), 1));
|
||||
assert!(VerifyingKey::from_bytes(&IDENTITY).unwrap().is_weak());
|
||||
assert_eq!(VerifyingKey::from_bytes(&IDENTITY_Y_NOT_REDUCED).unwrap().to_edwards().compress().to_bytes(), IDENTITY);
|
||||
// B is a canonical key of large order: the r_small_order forgery above holds under it
|
||||
// with S = k, which says its discrete logarithm is 1.
|
||||
assert!(strict_key(&BASE_POINT).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_non_canonical_key_of_large_order_is_refused() {
|
||||
// A point of large order whose y is below 19 has the second spelling y + p,
|
||||
|
|
@ -177,29 +390,6 @@ mod tests {
|
|||
assert!(!below_l(&L) && below_l(&s));
|
||||
}
|
||||
|
||||
/// `bytes` (little-endian, any length) mod L, by binary long division.
|
||||
fn mod_l(bytes: &[u8]) -> [u8; 32] {
|
||||
let mut r = [0u8; 33];
|
||||
for i in (0..bytes.len() * 8).rev() {
|
||||
let mut carry = (bytes[i / 8] >> (i % 8)) & 1;
|
||||
for b in r.iter_mut() {
|
||||
let top = *b >> 7;
|
||||
*b = (*b << 1) | carry;
|
||||
carry = top;
|
||||
}
|
||||
let lx = |j: usize| if j < 32 { L[j] } else { 0 };
|
||||
if (0..33).rev().find(|&j| r[j] != lx(j)).is_none_or(|j| r[j] > lx(j)) {
|
||||
let mut borrow = 0i16;
|
||||
for j in 0..33 {
|
||||
let v = r[j] as i16 - lx(j) as i16 - borrow;
|
||||
r[j] = v.rem_euclid(256) as u8;
|
||||
borrow = i16::from(v < 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
r[..32].try_into().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_small_order_or_non_canonical_r_is_refused() {
|
||||
use ed25519_dalek::Verifier;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use crate::energy_class::{
|
|||
check_label, json_with_label, label_coheres, labelled_domain, write_label, AcquisitionError,
|
||||
EnergyProvenance,
|
||||
};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
const DOMAIN_RECEIPT: &[u8] = b"wai:splat4d-receipt\x01";
|
||||
|
||||
|
|
@ -119,8 +119,7 @@ impl Splat4dReceipt {
|
|||
if merkle_root(&leaves) != self.merkle_root {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Label the figure with its acquisition class and re-sign
|
||||
|
|
@ -229,4 +228,14 @@ mod tests {
|
|||
assert_eq!(r1.parent_receipt_hash, Some(r0.receipt_hash()));
|
||||
assert_ne!(r0.receipt_hash(), r1.receipt_hash());
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let c = clip();
|
||||
let r = Splat4dReceipt::build(&key(9), "did:web:wai#splat4d", c.clip_hash(), 0, c.frame_hashes(), 5, 9999, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ use crate::energy_class::{
|
|||
AcquisitionError, EnergyProvenance,
|
||||
};
|
||||
use crate::world::receipt::merkle_root;
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
const DOMAIN_RECEIPT: &[u8] = b"wai:ternary-receipt\x01";
|
||||
const DOMAIN_RECEIPT_ID: &[u8] = b"wai:ternary-receipt-id\x01";
|
||||
|
|
@ -153,11 +153,7 @@ impl TernaryReceipt {
|
|||
if merkle_root(&self.leaves()) != self.merkle_root {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
|
||||
return false;
|
||||
};
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
|
||||
.is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// The strong, portable check: recover the stack from the `WQTT` payload,
|
||||
|
|
@ -342,4 +338,14 @@ mod tests {
|
|||
assert!(child.verify());
|
||||
assert_eq!(child.parent_receipt_hash, Some(root.receipt_hash()));
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let stack = encode_residual(&stream(256), 8, 2, 4, 3);
|
||||
let r = TernaryReceipt::seal(&key(1), "did:key:test", &stack, 4200, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
//! dependency set; pulling blake3 and ed25519 into it would break the matrix
|
||||
//! that makes the paragraph above true.
|
||||
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::int_video::{decode, DecodedClip};
|
||||
|
|
@ -306,8 +306,7 @@ impl VideoReceipt {
|
|||
if !figure_and_class_cohere(self.joules_micro, self.energy_provenance.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// The independent check: re-decode the `WIV1` payload and confirm this receipt
|
||||
|
|
@ -435,3 +434,18 @@ impl VideoReceipt {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let clip = DecodedClip { frames: vec![vec![7u8; 4 * 4 * 3], vec![9u8; 4 * 4 * 3]], h: 4, w: 4 };
|
||||
let r = VideoReceipt::build(&SigningKey::from_bytes(&[7; 32]), "did:example:meter", [0xcd; 32], &clip, 0, None, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
//! measurement — `joules_micro`, its acquisition class and the class's evidence —
|
||||
//! is measured-attested: signed, and not re-measurable. Reuses the manifest floor.
|
||||
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
use crate::quantum_ops::GrantRef;
|
||||
use crate::energy_class::{figure_and_class_cohere, write_optional};
|
||||
|
|
@ -236,8 +236,7 @@ impl ChannelReceipt {
|
|||
if !figure_and_class_cohere(self.joules_micro, self.energy_provenance.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// The program-as-run: the schedule entries on air in this window.
|
||||
|
|
@ -350,4 +349,14 @@ mod tests {
|
|||
assert!(r.verify());
|
||||
assert_eq!(hex(&r.receipt_hash()), "4850a8245f9d3e62227a56791e68689dfa7a566e7d18665bf5dc89521a8e2841");
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let g = GrantRef::unbounded("video.channel");
|
||||
let r = ChannelReceipt::seal(&key(1), "did:key:chan", "fast-42", schedule(), 8_000, 13_000, 6_000, Some(crate::energy_class::test_on_chip()), g, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
//! manifest module's `EnergyProvenance` and the world Merkle floor's Ed25519 + `GrantRef`; links
|
||||
//! to a manifest via `parent_receipt_hash`. Reuse, not reinvention.
|
||||
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
use crate::quantum_ops::GrantRef;
|
||||
use crate::energy_class::{figure_and_class_cohere, write_optional};
|
||||
|
|
@ -260,8 +260,7 @@ impl KeyReleaseReceipt {
|
|||
if !figure_and_class_cohere(self.joules_micro, self.energy_provenance.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Confirm the released key this receipt commits to is the one the caller
|
||||
|
|
@ -381,4 +380,17 @@ mod tests {
|
|||
assert!(r.verify());
|
||||
assert_eq!(hex(&r.receipt_hash()), "12c976fb6f742aeefb1f71af9a7b4eca01be5ba726bd80e1a8ed22cf1ac28249");
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let r = KeyReleaseReceipt::seal(
|
||||
&key(1), "did:key:license", [0xAB; 16], "asset-abc", DrmSystem::Widevine, // allow-name: DRM system identifier
|
||||
"svod:tier2", 1000, 2000, CEK, 300, Some(crate::energy_class::test_on_chip()),
|
||||
grant("svod:tier2"), None,
|
||||
);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@
|
|||
//! manifest module's `EnergyProvenance` and the world Merkle floor's Ed25519 + `GrantRef`.
|
||||
|
||||
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
use crate::quantum_ops::GrantRef;
|
||||
use crate::energy_class::{figure_and_class_cohere, write_optional};
|
||||
|
|
@ -141,7 +141,8 @@ pub struct PackageReceipt {
|
|||
}
|
||||
|
||||
impl PackageReceipt {
|
||||
fn signing_payload(&self) -> Vec<u8> {
|
||||
/// The bytes the signature covers, under the key the receipt names.
|
||||
pub fn signing_payload(&self) -> Vec<u8> {
|
||||
let mut o = DOMAIN_RECEIPT.to_vec();
|
||||
o.extend_from_slice(&(self.asset_id.len() as u32).to_be_bytes());
|
||||
o.extend_from_slice(self.asset_id.as_bytes());
|
||||
|
|
@ -252,8 +253,7 @@ impl PackageReceipt {
|
|||
if !figure_and_class_cohere(self.joules_micro, self.energy_provenance.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Whether the segment at `index` in the attested map has this content —
|
||||
|
|
@ -392,4 +392,15 @@ mod tests {
|
|||
assert!(r.verify());
|
||||
assert_eq!(hex(&r.receipt_hash()), "71a9da59a3202abb2c3fd22ee692b1485a9c674bb8c311f4c82455e6be29ae2b");
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let segs = segment(&source(), 2000);
|
||||
let g = GrantRef::unbounded("video.package");
|
||||
let r = PackageReceipt::seal(&key(1), "did:key:pkg", "asset-abc", segs, 12_000, Some(crate::energy_class::test_on_chip()), g, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
//! manifest receipt via `parent_receipt_hash`: the ad break belongs to a
|
||||
//! specific attested manifest. Reuse, not reinvention.
|
||||
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
use crate::quantum_ops::GrantRef;
|
||||
// The decision policy is pure integer logic and lives apart from this
|
||||
|
|
@ -220,8 +220,7 @@ impl SsaiReceipt {
|
|||
if !figure_and_class_cohere(self.joules_micro, self.energy_provenance.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
let Ok(k) = VerifyingKey::from_bytes(&self.signer_pubkey) else { return false };
|
||||
k.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig)).is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// The billable impressions — those that started rendering.
|
||||
|
|
@ -341,4 +340,16 @@ mod tests {
|
|||
assert!(r.verify());
|
||||
assert_eq!(hex(&r.receipt_hash()), "7f24dda0e0b85bc01d27fbb98b701c8ed0ff84cde280a24d653f43963f2f25c2");
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let mut imps = decide(&pod(), 2, 30_000, 80_000);
|
||||
imps[0].quartiles_reached = 5;
|
||||
let g = GrantRef { grant_hash: [7u8; 32], capability: "video.ssai".into(), joule_ceiling_micro: 1_000_000, funds_ceiling: Some(80_000) };
|
||||
let r = SsaiReceipt::seal(&key(1), "did:key:ssai", "avail-1", [9u8; 32], pod_hash(&pod()), imps, 3, 500, Some(crate::energy_class::test_on_chip()), g, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
use super::contract::SimContract;
|
||||
use super::op::CausalOp;
|
||||
use super::{DOMAIN_SEAL, WorldError};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey, VerifyingKey};
|
||||
|
||||
pub const MAGIC_WWLD: &[u8; 4] = b"WWLD";
|
||||
|
||||
|
|
@ -191,9 +191,9 @@ impl WorldContainer {
|
|||
pub fn verify_seal(&self, publisher: &VerifyingKey) -> Result<Seal, WorldError> {
|
||||
let seal = self.seal.ok_or(WorldError::BadSeal("no seal section"))?;
|
||||
let msg = self.seal_preimage(seal.final_tick, &seal.final_hash);
|
||||
publisher
|
||||
.verify(&msg, &Signature::from_bytes(&seal.signature))
|
||||
.map_err(|_| WorldError::BadSignature)?;
|
||||
// The key's bytes as it was decoded from, so a non-canonical spelling the library
|
||||
// read is refused here like any other (SPEC §7 *Signatures*).
|
||||
crate::sig::verify_strict(publisher.as_bytes(), &msg, &seal.signature).map_err(|_| WorldError::BadSignature)?;
|
||||
Ok(seal)
|
||||
}
|
||||
}
|
||||
|
|
@ -236,3 +236,39 @@ fn decode_assets(bytes: &[u8]) -> Result<Vec<AssetEntry>, WorldError> {
|
|||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::world::sim::CAP_SIM_KINEMATIC0;
|
||||
use crate::world::{NUMERIC_FIXED64, RESOLVE_LWW};
|
||||
|
||||
/// The seal's signature is verified strictly (SPEC §7 *Signatures*), under the
|
||||
/// publisher key's bytes as they were decoded: a key of small order, a non-canonical
|
||||
/// spelling of one, and an R of small order are each refused, though a lenient check
|
||||
/// accepts each.
|
||||
#[test]
|
||||
fn the_seal_is_verified_strictly() {
|
||||
let contract = SimContract {
|
||||
tick_rate: 60,
|
||||
numeric: NUMERIC_FIXED64.into(),
|
||||
rng_alg: "xoshiro256++".into(),
|
||||
rng_seed: [0x42; 32],
|
||||
sim: CAP_SIM_KINEMATIC0.into(),
|
||||
resolve_default: RESOLVE_LWW.into(),
|
||||
checkpoint_every: 100,
|
||||
};
|
||||
let publisher = SigningKey::from_bytes(&[9; 32]);
|
||||
let mut c = WorldContainer { contract_bytes: contract.to_canonical_json(), contract, graph_bytes: vec![], assets: vec![], ops: vec![], seal: None };
|
||||
c.seal(&publisher, 10, [7; 32]);
|
||||
crate::sig::assert_refuses_forgeries(
|
||||
&(c, publisher.verifying_key().to_bytes()),
|
||||
|g: &mut (WorldContainer, [u8; 32]), k, s| {
|
||||
g.1 = k;
|
||||
g.0.seal.as_mut().expect("sealed").signature = s;
|
||||
},
|
||||
|g| g.0.seal_preimage(10, &[7; 32]),
|
||||
|g| VerifyingKey::from_bytes(&g.1).is_ok_and(|k| g.0.verify_seal(&k).is_ok()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
//! ```
|
||||
|
||||
use super::{DOMAIN_NODE, DOMAIN_OP, WorldError};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
/// Op identity: `(actor public key, actor-local Lamport counter)`.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||
|
|
@ -98,11 +98,12 @@ impl CausalOp {
|
|||
|
||||
/// Verify the signature against the op's own actor key.
|
||||
pub fn verify(&self) -> Result<(), WorldError> {
|
||||
let key = VerifyingKey::from_bytes(&self.actor).map_err(|_| WorldError::BadActorKey)?;
|
||||
let mut msg = DOMAIN_OP.to_vec();
|
||||
msg.extend_from_slice(&self.preimage());
|
||||
key.verify(&msg, &Signature::from_bytes(&self.signature))
|
||||
.map_err(|_| WorldError::BadSignature)
|
||||
crate::sig::verify_strict(&self.actor, &msg, &self.signature).map_err(|e| match e {
|
||||
crate::sig::SigError::BadKey => WorldError::BadActorKey,
|
||||
crate::sig::SigError::BadSignature => WorldError::BadSignature,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
|
|
@ -190,3 +191,35 @@ impl<'a> Cursor<'a> {
|
|||
Ok(u64::from_le_bytes(self.take_array::<8>()?))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::world::sim::{kin_mutation, pack_fx};
|
||||
use crate::world::{mint_node_id, Fx};
|
||||
|
||||
/// An op's signature is verified strictly (SPEC §7 *Signatures*): a key of small
|
||||
/// order, a non-canonical spelling of one, and an R of small order are each refused,
|
||||
/// though a lenient check accepts each; a key's defect is `BadActorKey`, R's
|
||||
/// `BadSignature`.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let a = SigningKey::from_bytes(&[1; 32]);
|
||||
let op = CausalOp::sign(&a, 1, 1, vec![], mint_node_id(&a.verifying_key().to_bytes(), 1), kin_mutation::CREATE, pack_fx(&[Fx::ZERO; 7]));
|
||||
let set = |o: &mut CausalOp, k, s| (o.actor, o.signature) = (k, s);
|
||||
let msg = |o: &CausalOp| [DOMAIN_OP, &o.preimage()].concat();
|
||||
crate::sig::assert_refuses_forgeries(&op, set, msg, |o| o.verify().is_ok());
|
||||
for f in crate::sig::forgeries(|k| {
|
||||
let mut o = op.clone();
|
||||
set(&mut o, *k, [0; 64]);
|
||||
msg(&o)
|
||||
}) {
|
||||
let mut o = op.clone();
|
||||
set(&mut o, f.key, f.sig);
|
||||
match f.refusal {
|
||||
crate::sig::SigError::BadKey => assert!(matches!(o.verify(), Err(WorldError::BadActorKey)), "{}", f.name),
|
||||
crate::sig::SigError::BadSignature => assert!(matches!(o.verify(), Err(WorldError::BadSignature)), "{}", f.name),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ use crate::energy_class::{
|
|||
check_label, json_with_label, label_coheres, labelled_domain, write_label, AcquisitionError,
|
||||
EnergyProvenance,
|
||||
};
|
||||
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||
use ed25519_dalek::{Signer, SigningKey};
|
||||
|
||||
/// JWP Merkle domain separators (jwp-receipts §2.2), reused verbatim —
|
||||
/// this profile builds the *same* tree shape JWP defines.
|
||||
|
|
@ -269,11 +269,7 @@ impl SessionReceipt {
|
|||
if !label_coheres(self.joules_micro, self.energy_provenance.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
let Ok(key) = VerifyingKey::from_bytes(&self.signer_pubkey) else {
|
||||
return false;
|
||||
};
|
||||
key.verify(&self.signing_payload(), &Signature::from_bytes(&self.sig))
|
||||
.is_ok()
|
||||
crate::sig::verify_strict(&self.signer_pubkey, &self.signing_payload(), &self.sig).is_ok()
|
||||
}
|
||||
|
||||
/// Confirm this receipt commits to exactly the given ops: rebatch
|
||||
|
|
@ -477,4 +473,13 @@ mod tests {
|
|||
// parses as valid JSON
|
||||
let _: serde_json::Value = serde_json::from_str(&j).unwrap();
|
||||
}
|
||||
|
||||
/// The signature is verified strictly (SPEC §7 *Signatures*): a key of small order, a
|
||||
/// non-canonical spelling of one, and an R of small order are each refused, though a
|
||||
/// lenient check accepts each.
|
||||
#[test]
|
||||
fn the_signature_is_verified_strictly() {
|
||||
let r = SessionReceipt::build(&k(9), "did:key:test", [0xab; 32], 0, &sample_ops(), &cps(), 12_345, 999, None);
|
||||
crate::sig::assert_refuses_forgeries(&r, |r, k, s| (r.signer_pubkey, r.sig) = (k, s), |r| r.signing_payload(), |r| r.verify());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue