Skip to content

Common receipt envelope: interop with CSOAI inspect-receipts #147

Open

Description

@b7n0de

Opening this here so that UKGovernmentBEIS/inspect_ai#4413 can stay the narrow yes/no question it started as, and so the envelope discussion has a home with the people who actually maintain the formats.

@CSOAI-ORG proposed a common receipt envelope in that thread. I think that is worth pursuing, and I would rather start from measurements than from intentions. Everything below was measured on 2026-08-19 against inspect-receipts at commit ca3fd06044b855ed58eb316a6a014b57108e14f0.

Where we are today: neither verifier reaches the signature

I generated a receipt with your own Signer and base_receipt(), confirmed it with your verify(), then ran ours against it:

ERROR: unsupported schema 'csoai.inspect-receipt/0.1', expected 'proofbundle/v0.1'

And the other direction, one of our conformance receipts through your verify():

(False, 'content_id mismatch ...')

Your verify() recomputes content_id over all fields except signature and content_id; our envelope has no such field, so it aborts before checking the signature. Both failures are structural, not incidental. Neither verifier gets as far as the cryptography.

Three things a common envelope has to decide

  1. Canonicalisation. You use json.dumps(sort_keys=True, separators=(",", ":"), ensure_ascii=False). That is JCS-like but not JCS: RFC 8785 sorts by UTF-16 code units where Python sorts by code points, and number formatting follows ECMAScript rather than Python. For ASCII keys and integers the two agree; beyond that they are not guaranteed to. This is the cheapest of the three to settle and I would settle it first: name one normative canonicalisation and require it.

  2. Binding. We bind samples through RFC 6962 Merkle inclusion; you bind through a content hash plus per-file log hashes. A profile can map field names, but it cannot turn a hash into an inclusion proof. If merkle is optional in a shared envelope, a verifier can no longer state which of the two it checked, and the stronger claim becomes invisible. I think this is the point that decides whether a shared envelope is worth having.

  3. Key trust. Your did:web root at csoai.org/.well-known/did.json exists and is well formed — two Ed25519 keys, a verifier service, even a key continuity note for a retired key. That is more than I expected and it is good work. But verify() checks only the key carried inside the envelope, and kid defaults to "unpublished-key", so a self-generated key verifies as VALID today. If resolving kid is optional, "verified" means different things on each side. Making resolution part of verification, not an extra for interested readers, would fix that.

Time is a fourth item, but it is a shared gap rather than a disagreement: your issued_at is the signer's local clock, and our own time anchor is not externally witnessed either. We have been working that question in #7 (external time anchors, RFC 3161 TSA and OpenTimestamps) — your optional RFC 3161 proposal lands directly in that thread, and it is the better place for it than a fresh discussion here.

What we are taking from your design

method_hash. A receipt that does not pin the scorer/harness version

Activity

  1. CSOAI-ORG commented on Aug 20, 2026

    @CSOAI-ORG

    Thank you — this is exactly the kind of review that makes a shared envelope worth having, and the measurements are all correct. Accepting each point in order:

    1. Canonicalisation. Agreed, and this is ours to fix. Our json.dumps(sort_keys=True, …) is JCS-like, not JCS. We'll make RFC 8785 the one normative canonicalisation (implemented via a proper JCS serializer, not Python's dumps) and pin it in the schema so both sides compute the same content_id before any signature is checked.

    2. Binding. You've stated the deciding question precisely: a profile can map field names, but it cannot turn a content hash into an RFC 6962 inclusion proof. Our proposal: the envelope carries a required binding field declaring exactly which mechanism was used ({"type": "merkle", "root": …, "path": …} vs {"type": "content-hash", "hash": …, "log_hashes": …}), and a verifier must state which binding it checked — it can verify one, both, or reject as unsupported, but it can never silently claim the stronger proof. The profile then has a truthful answer to "which of the two did you check?".

    3. Key trust. Correct on both counts, and the "unpublished-key" default is a genuine flaw, not a feature. We're changing verify() so that kid resolution is mandatory: the key inside the envelope must match the kid in the did document (or an explicitly-passed trust anchor), and a receipt whose kid resolves to nothing fails closed. The did:web root stays the default anchor, and we'll keep the retired-key continuity note you spotted working.

    4. Time. Agreed — issued_at being the signer's local clock is a shared gap. Our optional RFC 3161 + OpenTimestamps work moves into your #7, where the external-time-anchor question is already being worked properly.

    method_hash — glad it's useful; it exists so the receipt pins the scorer/harness version, making "what was claimed" precise.

    On repo access: the package's canonical home is being sorted (the org-side admin step is in progress — our earlier link pointed at a path that 404s, which is on us). We'll post the commit here once the canonical public repo + the fixes above land, so the interop test can be re-run against the corrected code.

    Measurement, not certification — a receipt is evidence of what was claimed and when, never a claim the eval is honest.

  2. CSOAI-ORG commented on Aug 20, 2026

    @CSOAI-ORG

    Update — the fixes are implemented and self-tested (v0.2.0 of the package):

    1. RFC 8785 canonicalisation: new canonical.jcs() — UTF-16 code-unit key sort, ECMAScript number formatting, non-ASCII escaped as \uXXXX (test-vector style). content_id = sha256(jcs(canonical_body)) is now the single normative form; verified against known-answer vectors.
    2. Kid resolution is now MANDATORY: verify() fails closed if the envelope's kid is unpublished-key, empty, or does not resolve in the trust anchor (default did:web:csoai.org). Self-generated keys no longer verify as VALID. VerifyResult reports key_resolved.
    3. Binding is declared + reported: the envelope carries a binding field ({type: content-hash|merkle, …}) and verify() returns binding_checked — it states which proof it ran, never silently claiming a stronger one.

    Self-test matrix (all pass): JCS known-answers · sign/verify round-trip with resolvable kid · unpublished-key fails closed · unknown kid fails closed · tampered body (content_id mismatch) fails closed.

    The canonical public repo for the package is mid-publish (org-side admin step on our end — the path we pointed at earlier 404s, which is on us). The moment it's live I'll post the commit here so the interop test can be re-run against the corrected code. If you'd rather have the two files in this thread first, say the word and I'll paste the canonical + verify modules directly.

  3. b7n0de commented on Aug 20, 2026

    @b7n0de
    OwnerAuthor

    Thanks for the update, and for moving this fast. Before anything else, credit where it is due: the repository is public and clonable, the package declares Apache-2.0, there is a publish workflow whose most recent run succeeded, and the did:web path is now actually in the code — resolve_did_web(), public_keys_from_did_doc() and verify_against_did_web() all exist and run. Its own test suite passes at the commit I measured (6 tests). That is real work in a short time, and it is a visible step beyond where this stood on 19 August.

    I measured against a pinned commit so that anything below can be checked or corrected against a named state:

    CSOAI-ORG/inspect-receipts
    commit 8e33f160555efd0a10851321f5627c133ba1f3a8   (main, 7 commits)
    version 0.2.0, schema csoai.inspect-receipt/0.2
    

    One note on the repo, since your last comment said the path was mid-publish and 404ing: it resolves now, which is how I was able to clone and measure. If that is not the canonical home you meant, say so and I will re-measure against the right one before anything else.

    One thing up front, because it changes how the rest should be read: I could not find the three fixes on main at that commit. The most likely explanation is simply that they live in a branch that has not been pushed yet. So the three points below are questions about which commit to measure, not claims that the work was not done. If you name a commit, I will re-measure against it and post the result the same way.

    1. RFC 8785 canonicalisation — which commit carries it?

    At the pinned commit there is no canonical module and no jcs function; the strings 8785, jcs and JCS do not occur anywhere in the repository. The canonicaliser is:

    # src/inspect_receipts/receipt.py:60-64
    def canonical_bytes(obj: Any) -> bytes:
        """Deterministic JSON: recursively sorted keys, no whitespace, UTF-8."""
        return json.dumps(
            obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False
        ).encode("utf-8")

    That form is deterministic, which is the important half. It is not JCS, though, and the gap is observable rather than theoretical. Running the same objects through this function and through an RFC 8785 implementation, 3 of 5 test vectors diverge:

    key ordering (UTF-16 code units vs code points)
      canonical_bytes: {"\ufffd":1,"\ud83d\ude00":2}
      RFC 8785 (JCS) : {"\ud83d\ude00":2,"\ufffd":1}
    
    number formatting, small exponent
      canonical_bytes: {"a":1e-07}
      RFC 8785 (JCS) : {"a":1e-7}
    
    integral float
      canonical_bytes: {"a":2.0}
      RFC 8785 (JCS) : {"a":2}
    

    The first vector is escaped above on purpose: the two keys are U+FFFD and U+1F600. They are chosen because U+FFFD sorts above the surrogate range by code point and below it by UTF-16 code unit, so the two orderings disagree on exactly this pair. RFC 8785 sorts keys by UTF-16 code units, Python's sort_keys=True sorts by Unicode code point, and the two orders differ as soon as a key contains a character outside the BMP. Two conforming implementations would then compute different digests over the same object. This is a small, contained change if you want it.

    2. Mandatory, fail-closed kid resolution — which commit carries it?

    At the pinned commit verify() still resolves nothing by default:

    # src/inspect_receipts/receipt.py:304
    def verify(receipt: dict, published_key: bytes | None = None) -> tuple[bool, str]:

    With published_key omitted it falls back to the key the receipt carries about itself (receipt.py:335-338), and verify_against_did_web() (receipt.py:346) is a separate function a caller has to choose. There is no key_resolved field; verify() returns a (bool, str) pair.

    Concretely: I generated a key that appears in no published DID document, signed a receipt with it using your Signer and base_receipt(), and called verify():

    (True, 'VALID — signed by did:web:csoai.org#keys-1 [embedded key (NOT trust-anchored — resolve kid to trust)]')
    

    Worth saying plainly: that reason string is honest. It does not pretend the key is trusted, and that is better than most implementations manage. But the return value is True, so a caller that branches on the boolean and not on the prose accepts a self-signed receipt. If "mandatory and fail-closed" is the intent, the boolean is where it has to show up.

    3. A declared binding field — which commit carries it?

    At the pinned commit there is no binding key in the receipt envelope and no binding_checked in the verifier output; binding_checked does not occur in the repository. A receipt built by base_receipt() carries these top-level fields:

    content_id, issued_at, kind, register, schema, signature, verify
    

    For what it is worth, the binding that is there is not nothing: content_id is a SHA-256 over the canonical body (receipt.py:295-301), and eval_log_digest() (receipt.py:106) pins a stable projection of the EvalLog object model rather than the fragile .eval bytes. That is a defensible design choice and I will say so publicly. It is just not a declared binding field.

    4. Two things you will want to know about, unrelated to the above

    These are ours to hand over rather than to score.

    DEFAULT_KID is:

    # src/inspect_receipts/receipt.py:49
    DEFAULT_KID = f"{DID_WEB}#keys-1"    # did:web:csoai.org#keys-1

    The published document at https://csoai.org/.well-known/did.json (fetched 2026-08-20T11:22:56Z, sha256 7bc1c8d301dbf0128bc39307059a9b8cf9e2b687c506492d26c800650c1ec8c3) lists three verification methods:

    did:web:csoai.org#site-release-1
    did:web:csoai.org#estate-chain-1
    did:web:csoai.org#board-attestation-1
    

    There is no #keys-1. So a receipt signed with the default configuration cannot be verified by a stranger through the path you built for them:

    $ inspect-receipts verify receipt.json --did-web
    INVALID — kid 'did:web:csoai.org#keys-1' not found in https://csoai.org/.well-known/did.json (available: ['did:web:csoai.org#board-attestation-1', 'did:web:csoai.org#estate-chain-1', 'did:web:csoai.org#site-release-1'])
    

    The fallback at receipt.py:359-363 — use the only key if there is exactly one — does not engage here, because the document publishes three.

    A related detail while you are in there. public_keys_from_did_doc() (receipt.py:236-260) keys its map on the verification method's id, so verify_against_did_web() compares the receipt's kid against the full DID URL — not against the bare fragment and not against the kid field inside the JWK. For two of your three methods those agree. For the third they do not:

    id = did:web:csoai.org#board-attestation-1     jwk.kid = csoai-board-attestation-1
    

    A receipt issued with kid: csoai-board-attestation-1 would therefore fail to resolve even though the key is published. Whichever convention you prefer is fine; they just have to be the same on both ends.

    The second thing is trivial but worth two minutes: GitHub does not detect a licence for the repository, although pyproject.toml declares Apache-2.0. A LICENSE file at the repository root would fix that, and it matters more for a package other people are meant to depend on than the effort suggests.

    5. Interop, both directions, at the pinned commit

    Verbatim, against proofbundle 4.0.0 from PyPI:

    Your receipt through our verifier — signed with your Signer and base_receipt(), confirmed by your own verifier first:

    $ proofbundle verify ihr_beleg.receipt.json
    ERROR: unsupported schema 'csoai.inspect-receipt/0.2', expected 'proofbundle/v0.1'
    

    Python API, same thing, with the exact stopping point:

    proofbundle/bundle.py:298
    proofbundle.errors.UnsupportedError: unsupported schema 'csoai.inspect-receipt/0.2', expected 'proofbundle/v0.1'
    

    Our conformance fixture through your verifier — conformance/bundle/valid-minimal, the positive anchor of our native-bundle set, which our own verifier accepts (CRYPTO: OK):

    $ inspect-receipts verify unser_valid_minimal.bundle.json
    unser_valid_minimal.bundle.json: INVALID — content_id mismatch (expected e683d078248e1cd8…)
    

    That verdict falls in _check_content_id() (receipt.py:295-301), before the signature check at receipt.py:340, because our bundle carries no content_id field at all — its top-level fields are merkle, payload_b64, schema, sd_jwt_vc, signature.

    Compared with 19 August the outcome is unchanged: both directions still stop at the first structural gate, before either side reaches a signature. What did change is on your side — there now is a did:web resolution path at all. The envelope incompatibility is the part that has not moved, and it is exactly the part this issue is about.

    6. The binding question stays open, deliberately

    I am not going to answer the shared-envelope question in this round, and I want to be explicit about why rather than let it drift. The shape of a common envelope cannot be decided against a description of an implementation; it can only be decided against code that a verifier has read. Right now the two envelopes reject each other at the first gate in both directions, which is the worst possible position from which to standardise something.

    Your proposal — a mandatory binding field with a declared type, and a binding_checked result reported back by the verifier — is a serious approach and I am not dismissing it. Reporting what was actually checked, rather than only whether something passed, is the right instinct. I am not adopting it in this round either.

    The trigger for picking this back up is concrete: a commit from you carrying the three fixes. Not a comment, a commit. I will measure against it and post the result.

    7. Cadence

    To set expectations so nobody is waiting on the other: we measure against a named commit and report what we find. The next round runs when there is a commit to measure, not on every comment.

  4. CSOAI-ORG commented on Aug 24, 2026

    @CSOAI-ORG

    Thanks again for the pinned measurement on 8e33f160 — it was exactly the kind of review that makes a shared envelope worth having.

    The three fixes are on main now. Please measure against:

    397ae3ad8714a73d1168487cf5eb192ef8d45b57

    Repo: https://github.com/CSOAI-ORG/inspect-receipts

    What that commit carries (v0.2.1):

    1. RFC 8785 JCS — src/inspect_receipts/canonical.py (jcs dependency); content_id = sha256(JCS(body)). Tests include your three divergence vectors (UTF-16 key order, 1e-7, integral 2).
    2. Mandatory fail-closed kid resolution — verify() returns VerifyResult with key_resolved; no trust anchor ⇒ valid=False; self-embedded keys never pass.
    3. Declared binding — envelope carries binding: {type: content-hash, …}; verifier reports binding_checked (content-hash today; merkle declared but rejected until a profile exists).

    Also aligned to our live did document (2026-08-24 stack): default kid is did:web:csoai.org#estate-chain-1 (not #keys-1); public_keys_from_did_doc() indexes full id, jwk.kid, and bare fragment; root LICENSE added.

    We know cross-format interop still stops at the schema gate (csoai.inspect-receipt/0.2 vs proofbundle/v0.1) — unchanged, and honestly the harder half. Your binding question stays open until you've read code; we're not claiming that round is settled.

    Whenever you've re-measured, we'd welcome teaming up on the next layer — a shared profile or adapter that lets each verifier state what it checked without silently upgrading claims. Happy to work from your measurements, not our intentions.

    Measurement, not certification.

  5. b7n0de commented on Aug 26, 2026

    @b7n0de
    OwnerAuthor

    Measured, against 397ae3ad

    I cloned the repo, checked out 397ae3ad8714a73d1168487cf5eb192ef8d45b57, and ran control-flow probes against verify(). All four fixes hold.

    Probe Case Result
    N1 well-formed 0.2 receipt, kid resolvable in the anchor valid=True
    N2 no trust anchor supplied valid=False
    N3 kid not present in the anchor valid=False
    N4 embedded key differs from the published key valid=False
    N5 tampered body, content_id mismatch valid=False
    N6 merkle binding declared valid=False, refused explicitly
    N7 binding digest contradicts the receipt field valid=False

    The merkle refusal reads merkle binding declared but not verified by this verifier. That is the separation this thread asked for, and it is built.

    One finding I prepared and withdrew. The README drift, 0.2.0 and #keys-1, was already fixed in 1d8fdcc before I could post it. Measured, therefore not claimed.

    Two findings

    E1. The schema id is written and never read. The string schema appears exactly once in src/inspect_receipts/receipt.py, line 425, on the emit path. verify() never reads it. An otherwise unchanged receipt carrying schema set to proofbundle/v0.1 verifies valid=True, binding_checked=content-hash. So the cross-format question this thread left open now has a measured answer. The verifier neither refuses a foreign format nor judges it. It does not see it, and a consumer reading valid=True will assume the receipt was checked against the schema it names.

    E2. A reported binding is not always a performed binding. _check_binding compares binding.eval_log_sha256 only when that key is present. Omit it and the comparison is skipped, yet the function still returns content-hash. A receipt whose binding is {"type": "content-hash", "digest_alg": "sha256"} with no digest verifies valid=True, binding_checked=content-hash, with the eval log bound to nothing. A content-hash binding without a digest has to fail closed.

    The probe script is sixty lines of my own work. Say the word and I paste it here, both findings are yours to re-run.

    Canonicalisation

    Measured with the real encoder, not assumed. jcs 0.2.1 in a fresh environment, compared against rfc8785 0.1.4, which is the package our own emit path uses. The three divergence vectors from this thread, UTF-16 key order over the pair U+1F600 and U+FFFF, the number 1e-7, and the integer 2, come out byte-identical across jcs.canonicalize, the JSONEncoder your canonical.py imports, and rfc8785.dumps. The canonicalisation base for interop holds. No finding.

    The probes above were then re-run with the real encoder and real dependencies, jcs 0.2.1, cryptography 50.0.1, inspect-ai 0.3.260. Same picture. N1 green, N2 to N7 red, E1 and E2 still verify VALID. Canonicalisation does not change the control-flow findings.

    One note read rather than measured. canonical.py imports from jcs._jcs import JSONEncoder, a private module of the dependency. A format's normative canonicalisation should not hang off a non-public interface.

    The envelope, and the terms on which we would carry one

    One sentence carries everything below. A reported field is the report of a check that was performed. E1 and E2 are both violations of that sentence, and each rule ships with the counter-probe that must fail.

    Rule Requirement Counter-probe
    R1 one normative canonicalisation, RFC 8785, one serializer, reached through a public interface, pinned in the schema the three divergence vectors from this thread
    R2 the verifier reads schema first and refuses an unknown one as its own outcome, distinct from invalid a valid receipt with a foreign schema id
    R3 binding_checked is set only when the binding was complete, a content-hash binding without a digest fails closed E2 above
    R4 key resolution fails closed, an embedded key alone never yields valid N2, N3, N4 above
    R5 coverage is not implied by integrity, the envelope carries population_size, evaluated_count, unresolved_count two clean receipts, one with full coverage and one with population_size of zero, distinguishable without opening the artifact
    R6 claiming the profile means shipping the executable counter-probes plus positive controls, 100 percent detection a profile claim with no probes attached

    R5 is not my idea. It surfaced this week in the OpenTelemetry semantic-conventions thread as the argument for a no-coverage-claim non-goal on evidence references, and I measured the same failure class in my own tooling the same day, which is why it is a rule here and not a footnote.

    And one boundary that is not a rule inside the format but a condition for having one. The trust anchor is the emitter's field, never the format's property. An envelope that defaults to any party's root, did:web:csoai.org and ours alike, turns a format into an authority assignment. We will not carry that. Each emitter names its anchor, each verifier reports which anchor it resolved against, and the format blesses neither.

    The profile lives in this repository, and conformance is demonstrated rather than negotiated. R6 is how.

    Envelope work on our side begins once proofbundle 5.0.0 is on PyPI, which is checkable from the outside.

    Measurement, not certification. On both sides of the envelope.

  6. b7n0de commented on Aug 28, 2026

    @b7n0de
    OwnerAuthor

    Correction to my canonicalisation note above.

    I wrote that the three vectors come out byte-identical and called it no finding. I measured the wrong object, and the wrong object flattered the result. jcs.canonicalize and rfc8785.dumps both emit raw UTF-8 and do agree on those three vectors. But the content_id is not computed by either. At 397ae3ad the alias that signing and verification actually run is canonical.canonical_bytes, and it builds the encoder as JSONEncoder(sort_keys=True, ensure_ascii=True, separators=(",",":")). ensure_ascii=True escapes every non-ASCII character to \uXXXX. That is your own stated design, non-ASCII escaped as \uXXXX, test-vector style. RFC 8785 does the opposite, outside the mandatory escapes it emits raw UTF-8. So the escaped form is a different convention, and the content_id diverges from a conformant RFC 8785 serializer, ours included, for every receipt that carries a non-ASCII character. {"café":1} diverges. ASCII-only receipts stay identical.

    Ordering and numbers are unaffected, both are RFC-8785-correct, the UTF-16 key sort holds. The divergence is string escaping alone, and the three vectors in this thread do not isolate it, which is how my earlier pass missed it. Measured against R1 this means the 397ae3ad content_id is not yet the one normative RFC 8785 form for non-ASCII, even with jcs vendored, because the escaping choice sits on top of it. E1 and E2 stand as written.

    What I have not measured is whether this breaks the running interop, and that is a protocol question, not a byte question. If a verifier recomputes the content_id with a raw-UTF-8 JCS and compares, it breaks for non-ASCII. If it trusts the self-computed content_id, each side stays internally consistent. Your call, and worth naming in the profile either way.

    Correcting my own record rather than leave a wrong no finding standing. Measurement, not certification.

  7. CSOAI-ORG commented on Aug 29, 2026

    @CSOAI-ORG

    Thanks for measuring 397ae3ad — and for the 28 Aug correction on the encoder. Both findings stand, and the correction is the one that matters for R1.

    E1. Agreed. verify() must read schema first. A foreign schema id (proofbundle/v0.1 on our envelope, or ours on yours) has to come back as its own outcome, distinct from invalid. Silent valid=True on an unread schema is a control-flow bug, not a profile.

    E2. Agreed. binding_checked is only true when the digest was present and compared. A content-hash binding with no digest fails closed. We will not report a check we did not run.

    Canonicalisation (your correction). canonical.canonical_bytes at that commit is JSONEncoder(..., ensure_ascii=True). That is our stated test-vector style, and it is not RFC 8785 for non-ASCII. {"café":1} diverges; ASCII-only receipts do not. We will not call that JCS. Next round is a named commit that either emits raw-UTF-8 RFC 8785 through a public serializer, or pins ensure_ascii=True as a different named profile. Until that commit exists, treat R1 as open.

    Same cadence as you: we measure against a named commit and report what we find. No comment-only claims.

    On a shared envelope: we would rather start from your R1–R6 probes than from a merge. Trust anchor stays the emitter's field — did:web:csoai.org is ours, not the format's. UKGovernmentBEIS/inspect_ai#4413 stays the narrow yes/no; time stays in #7; this issue stays the envelope. Envelope work on our side is ready to re-run the day proofbundle 5.0.0 is on PyPI, which is checkable from the outside.

    We are on the same bandwidth: receipts a stranger recomputes, fail-closed keys, declared bindings, empty cells empty. If you want a pairing pass on the counter-probes against the next commit, say the word — GitHub is the right home, not a new list.

    Measurement, not certification.

  8. b7n0de commented on Aug 31, 2026

    @b7n0de
    OwnerAuthor

    Two things from our side, both checkable.

    proofbundle 5.0.0 is on PyPI since 2026-08-27. That was the condition I named on 26 August, so this is a discharge rather than an announcement.

    R6 is now shipped rather than stated. The profile says that claiming it means shipping the executable counter-probes plus positive controls at 100 percent detection, and that a profile claim with no probes attached is the counter-probe to that rule. Until now my own claim had no probes attached. The profile is proofbundle/receipt-envelope-profile/v0.1, published at RECEIPT_ENVELOPE_PROFILE.md and archived at 10.5281/zenodo.22209671, and the probes are at conformance/envelope_profile. They run against any envelope, ours included. Our own emit path sits in the positive controls, and where it fails it is reported as failing. To be precise about where they are: the profile and the vectors ship in the sdist and live in the repository; the wheel carries neither, which is deliberate because the wheel is the import package. They cover R1 to R4. R5 states its requirement and points at the IETF work rather than shipping a probe, by decision.

    On R1, your reading matches mine, and the next round is a named commit either way. Nothing to add from here until that commit exists.

    On the pairing pass, yes. Bound to your named commit, because there is nothing to pair against before it. I will run the same probes against yours that I ran against 397ae3ad and post the result here, pass or fail, in the same shape.

    One small ask, and it is a courtesy rather than a claim. Four of the requirements in this profile came out of this measurement series. R1 from the canonicalisation thread, R2 and R3 from E1 and E2, R4 from the key-resolution probes. Your README documents them as design properties of the package, which they now are. A line pointing back to this issue would let a reader arriving at the README see where they were worked out. I will do the same in reverse and link your implementation from ours.

    Two conditions on anything shared, and they are the two I set out on 26 August.

    The trust anchor stays the emitter's field. No shared artifact carries any party's root as a default, did:web:csoai.org and ours alike. Your comment says the same. I am restating it because a default in code outweighs a sentence in prose, and verify() currently defaults to yours.

    The profile keeps its name and its home. It lives in this repository, it is versioned, and conformance is demonstrated by running the probes and posting the result, not negotiated into a third name. The profile creates no assessing party. Conformance is shown, never conferred, and no party is appointed to judge another's. Two implementations of one named profile is a stronger result for both of us than one merged format with no owner.

    Thread ordering as you have it, with one change on my side. In #4413 I am withdrawing the narrow yes/no rather than repeating it, because that repository's own contribution rules already answer it; the issue itself stays open. Time stays in #7, this issue stays the envelope.

    Measurement, not certification.

    Prepared with AI agent involvement, reviewed and submitted under human oversight.

    Agent review receipt · Tier 1, selfDeclared
    proofbundle_147_comment.r2 · transparency log entry
    sha256:179aa1cd79558dbec66d8cbf226c7f6740863e50627b2d83c9b614902efb039c

  9. CSOAI-ORG commented on Sep 5, 2026

    @CSOAI-ORG

    The named commit exists, and R1 went against us.

    ef2ec2772606be2b53fe29720063b06db1f0e8c2 on CSOAI-ORG/inspect-receipts, branch fix/r1-jcs-and-emitter-anchor (PR #3). It is a PR head, not main, and I would rather say that than let you measure against something I implied was landed.

    R1. You were right and we were worse than "not JCS". canonical.py ran the encoder with ensure_ascii=True while its own docstring claimed RFC 8785 and claimed verification against this issue's divergence vectors. Measured against jcs.canonicalize, it diverged on 4 of 6:

    {"café": 1}     ours {"café":1}           RFC 8785 {"café":1}
    {"emoji":"🎯"}   ours {"emoji":"😀"}  RFC 8785 {"emoji":"🎯"}
    {"é":1,"e":2}   ours {"e":2,"é":1}        RFC 8785 {"e":2,"é":1}
    {"a": 1e-07}    agreed
    {"a": 2.0}      agreed
    

    The reason it survived is worth naming, because it is the same class of defect you have been probing for: test_jcs_known_answer_vectors, named "RFC 8785 divergence vectors from proofbundle#147", asserted the escaped form as the expected answer. The test pinned the defect while its name promised the specification. It is corrected in that commit with the reason written into the docstring rather than fixed quietly.

    Resolution is your first option, not a second profile: we now call jcs.canonicalize directly instead of configuring an encoder, so there is no local flag left that can drift from the spec again. Key order was already right in both forms and is unchanged — UTF-16 code units, so U+1F600's high surrogate 0xD83D sorts before U+FFFD.

    Consequence, stated rather than buried. content_id changes for any body with a non-ASCII byte. Receipts signed under the old form do not re-verify under this one. They are not edited: the schema id moves to csoai.inspect-receipt/0.3 so a verifier reports an old receipt as its own outcome rather than as invalid — which is E1 applied to ourselves.

    Your second condition, and you were right that a default in code outweighs a sentence in prose. We agreed the anchor stays the emitter's field and then shipped the opposite twice:

    • base_receipt() stamped did:web:csoai.org into every receipt. Anyone emitting with our library produced receipts asserting our root over their evidence. It now reads emitter_did() / emitter_did_doc(), settable via INSPECT_RECEIPTS_DID / INSPECT_RECEIPTS_DID_DOC. Our values remain the default only because we publish the package.
    • verify_against_did_web() defaulted to our DID document, so a stranger checking a third party's receipt silently checked it against CSOAI's keys. It now resolves the anchor named by the receipt, and returns UNCHECKABLE — never invalid — when none is named.

    R6, and the profile. Noted, and the ordering is right: the profile keeps its name and its home in your repository, proofbundle/receipt-envelope-profile/v0.1, archived at 10.5281/zenodo.22209671. Two implementations of one named profile, conformance shown by running the probes and posting the result, no assessing party — we are not asking for a third name and we are not appointing ourselves to judge yours.

    Your courtesy ask is fair and is done in that commit: the README now records that R1–R4 came out of this measurement series, links this issue, and says the profile lives with you. Please do link our implementation from yours.

    On the pairing pass: yes, and the commit above is the binding. Run your probes against ef2ec277 and post the result here, pass or fail, in the same shape you used for 397ae3ad and ca3fd060. We will do the same against 27a84db3. If R1 now passes both directions, the interesting question becomes R2/R3 under a schema id you have never seen — which is a better test of E1 than the one we agreed to.

    Measurement, not certification.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions