Add opaque-digest handoff bodies with corrected discriminators - #40
Merged
Conversation
A digest's algorithm is a facet of the MLS backend's cipher suite, so requiring a TypedDigest meant the backend could not adopt a new suite until this package shipped a matching DigestTypes case — a release dependency pointing the wrong way. The v2 bodies commit to length-framed opaque bytes instead, so the caller's own self-describing encoding travels inside the value and cross-era signatures stay unambiguous without this package knowing the eras. Safe because a verifier never parses a digest out of a handoff: it rebuilds the body from its own locally derived reference digest and checks the signature against that. Algorithm agreement is enforced where the session is established. Purely additive — the typed overloads and v1 bodies are untouched, so every issued handoff keeps verifying, and v1/v2 are mutually unverifiable by construction. v1's ActiveAgentBody and RetiredAgentBody carry each other's discriminators; that is a labeling bug rather than a security one (separation needs distinctness, not correct names) and is now frozen and commented, since renaming in place would silently break every live relationship. v2 takes the corrected names, suffixed .v2 because v1 holds the plain ones. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: b3842d1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The typed overload made digest well-formedness structural — a TypedDigest is fixed-width, so empty was unrepresentable. The opaque one moves that to runtime, and empty is the one degenerate value both ends could plumb symmetrically from a default-value integration bug: the signatures would agree and the handoff would verify with its MLS binding absent. Mint and verify both refuse it, so the bug is loud at whichever end introduced it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
createNewAgentHandoffandPublicAnchorAgent.verify(anchorHandoff:…)gain overloads takinggroupContext/mlsUpdateDigestasData, backed by v2 signature bodies that commit to those bytes as length-framed values and never interpret them. The motivation is ownership: a digest's algorithm is a facet of the MLS backend's cipher suite, so requiring aTypedDigestmeant the backend could not adopt a new suite until this package shipped a matchingDigestTypescase — a release dependency pointing the wrong way. This is safe because a verifier never parses a digest out of a handoff; it rebuilds the body from its own locally derived reference digest and checks the signature against that, so agreement on the algorithm is enforced where the session is established rather than here.Purely additive. The typed overloads and the v1 bodies are untouched, so every already-issued handoff keeps verifying and nothing needs to re-pair — which is why this is v2-alongside rather than a replacement: classical relationships are live.
The discriminator fix you were looking for. v1's
ActiveAgentBodyandRetiredAgentBodycarry each other's names. It's a labeling bug rather than a security one — domain separation needs the committed strings to be distinct, not correctly named, and they are, so nothing can be replayed across contexts. But it's a real landmine for anyone writing a spec or a second implementation from the names. It is now frozen and commented (renaming in place would silently fail verification for every live relationship), and v2 carries the corrected names. They're.v2-suffixed because the plain corrected strings are unavailable — v1 has them live on the opposite structs, so reusing one would put two different structures under a single committed string.ActiveAnchorBodyV2takes the suffix too, though its name was never swapped, to keep discriminator↔encoding strictly 1:1.Tests: 6 new, full suite 118 green. The round trip runs on a digest tagged
0x7F— a valueTypedDigestcannot represent at all — which is the property the change exists for. v1↔v2 non-verification is pinned in both directions, and one test asserts all six discriminators are pairwise distinct while another pins the frozen swap so nobody "corrects" it later.Adoption is not urgent: the app can keep calling the typed overloads by lifting the backend's bytes with
TypedDigest(wireFormat:), which is byte-identical. Moving to these overloads is what removes the last cross-repo release coupling, and it costs a PQ re-pairing whenever you choose to do it.