P6 vNext SDK wire and binding helpers

August 18, 2026 ยท View on GitHub

Scope and protocol snapshot

This SDK slice implements the frozen cross-domain surface of anp.group.e2ee.v2 from AgentNetworkProtocol@6c6aa9b8dc63b18fba228672165fc9a22aa4601f. It is side-by-side with the existing anp.group.e2ee.v1 models and typed OpenMLS library operations; no v1 wire object is silently reinterpreted as v2.

Rust exposes the v2 helpers under anp::group_e2ee; Go exposes them from golang/group_e2ee. Both SDKs include:

  • closed publish/get/create/add/remove/send/notice/incoming DTOs;
  • required stable Notice IDs and the 5013 / group.e2ee.leaf_not_current allocation;
  • delivered group.incoming auth with origin_context, reconstruction of the original group.e2ee.send Signed Request Object, and P1 logical origin-proof verification;
  • method-specific metadata, target-kind, security-profile and Origin Proof shape checks;
  • owner_did + owner_device_id KeyPackage binding;
  • canonical MLS authenticated_data, Add/Remove submission binding, and Group Application Plaintext helpers;
  • the P6 did_wba_binding Object Proof and current P2 Manifest eligibility verifier;
  • same-DID sibling-leaf validation without sharing a device ID or leaf key;
  • the recommended P6 error table.

The shared Rust/Go vectors are in testdata/group_e2ee/p6_v2_wire_vectors.json.

MLS trust boundary

The JSON group_key_package.did_wba_binding member is a convenience copy. It does not prove what is inside mls_key_package_b64u. A caller must first use a conforming MLS implementation to decode and cryptographically validate the TLS-serialized KeyPackage, then pass this authenticated projection to the SDK:

  • credential identity bytes;
  • the verified TLS-serialized KeyPackage bytes, which must equal the outer mls_key_package_b64u bytes;
  • actual LeafNode signature public key;
  • LeafNode extensions and extension bytes;
  • LeafNode capability extension types;
  • GroupContext required-capability extension types.

The verifier requires credential.identity to equal the UTF-8 Agent DID, requires exactly one device-binding extension, compares its bytes with RFC 8785 JCS of the full signed binding, and checks both capability declarations. It then follows the current Manifest entry to the device signing_key_id and verifies the P1 Ed25519 Object Proof. Device-ID, leaf-key, credential, extension or capability substitution fails closed.

The existing v1 typed OpenMLS operation path does not emit this complete v2 extension/capability profile and must not be advertised as P6 v2. Rust now has a separate persistent v2 operation facade at anp::group_e2ee::operations::v2; it does not reinterpret or migrate v1 group state.

Persistent Rust operation facade

The v2 facade uses GroupMlsStore and ImCoreSqliteGroupMlsStore so every local DID/device pair has independent OpenMLS signer, KeyPackage private material, Leaf state and epoch secrets. It provides typed operations for:

  • device-bound KeyPackage generation;
  • create/Add/Remove preparation followed by explicit finalize or abort after the Group Host accepts or rejects the request;
  • device-targeted Welcome and ordered Commit processing;
  • direct processing of standard V2GroupNoticeMetadata + V2E2eeNotice delivery without reconstructing an originating control DTO;
  • one-shot MLS application encryption and device-local decryption.

Every KeyPackage, Add target and message sender is checked against the exact DID/device binding extension and current DID document. Add rejects an existing sibling pair or leaf key. Remove instead authenticates exactly one target Leaf from the locally accepted OpenMLS tree, so a device that has already lost P2 Manifest eligibility (or whose old binding proof has expired) can still be removed without affecting a sibling Leaf. The product and Group Host must still verify current P4 state, the allowed Remove trigger and the current owner device's authorization. Application decryption verifies both the frozen JCS authenticated_data and the sender Leaf binding, and no private MLS state appears in a returned wire object.

process_notice_v2 verifies the exact recipient DID/device before touching local MLS state. For Commit delivery it obtains subject_method, the affected DID/device, group/state/epoch, originating operation_id, and sender DID/device from the authenticated MLS Commit AAD. It then verifies the MLS sender Leaf and current DID documents against that same pair. Welcome delivery additionally requires the outer recipient to equal the added subject device. Exact replay of the same notice operation and bytes returns the persisted result; reusing the operation ID with different bytes fails closed.

Welcome processing also persists exact Welcome and ratchet-tree digests. A repeat at the same crypto group and epoch succeeds only when both byte strings match the persisted receipt. A fresh Welcome may replace an older local group at a strictly newer epoch when the prior binding is removed, or when the delivered tree no longer contains the exact stored old LeafNode. The fresh Welcome must still decrypt under this device's unconsumed KeyPackage and the complete new tree must pass all DID/device/Leaf validations. A P4 or Host terminal signal can separately disable new application sends without pretending that the exact MLS Remove Commit has already been applied; sequential Commit processing remains available until the cryptographic state converges.

The Group Host may broadcast a Commit back to the device that prepared it. After that device has finalized locally, the echo is accepted only when one unique finalized journal entry matches the local actor DID/device, Add/Remove method, group and state reference, crypto group, exact subject device, from/target epochs, and exact Commit bytes/digest. The SDK records a normal notice receipt without applying the Commit twice. Missing, ambiguous, stale or conflicting journal/notice data fails closed, including after restart.

OpenMLS storage and SDK metadata use separate SQLite connections, so the SDK does not claim that prepare/finalize is one cross-provider SQL transaction. Instead, create/Add/Remove use a recoverable write-ahead journal:

preparing -> prepared -> accepted -> finalized
                    \-> aborted

preparing is durable before OpenMLS mutation. An interrupted prepare is conservatively rolled back by reconcile_pending_v2; a prepared entry waits for the Group Host decision and returns its persisted public request artifact; an accepted entry completes an already-started merge idempotently. finalize_commit_v2 and abort_commit_v2 are restart-safe. The journal stores only public operation artifacts and identifiers; it does not copy or export Leaf private keys, epoch secrets, or the OpenMLS database.

The facade performs the local cryptographic obligations only. P4 membership, owner authorization, current group-state CAS, KeyPackage lease/consumption and delivery authorization remain Group Host/product responsibilities.

Real OpenMLS multi-device gate

rust/tests/group_e2ee_v2_multi_device_mls.rs is a development integration gate over real OpenMLS 0.8 cryptography. It creates independent providers, signers, KeyPackages and group state for two devices of the same business DID, then verifies the frozen v2 binding before exercising Add/Commit/Welcome, post-join application decryption, exact-device Remove, epoch advancement and future-message exclusion. It also proves that a new device cannot decrypt pre-join history, another device's storage cannot consume its Welcome, a consumed KeyPackage cannot be added again, and an authenticated binding cannot be substituted around another device's actual TLS KeyPackage.

The test deliberately projects the business member count by de-duplicating MLS credential DIDs; it does not change MLS credentials or add an internal counter to the cross-domain P6 model. This gate proves that the frozen semantics are expressible with the pinned OpenMLS version. The persistent public-facade gate in rust/tests/group_e2ee_v2_operations.rs exercises the same lifecycle using separate on-disk device stores, service-acceptance finalization, canonical application plaintext, standard Welcome/Commit notice replay and binding, write-ahead restart reconciliation, and a single MLS-encrypted attachment Manifest. Neither test upgrades the legacy typed path or bypasses the draft extension release gate.

Draft extension release gate

The frozen draft uses provisional extension type 0xF0A1. Runtime development and contract tests may use it only after explicit anp.group.e2ee.v2 negotiation. ensure_p6_v2_public_release_ready / EnsureP6V2PublicReleaseReady intentionally returns an error while that value is not a stable registered codepoint. Public v2 discovery or release must stay disabled until the protocol publishes the stable assignment and the SDK is updated together with its vectors.

Validation

cargo test --manifest-path rust/Cargo.toml --locked --test group_e2ee_v2_wire_vectors
cargo test --manifest-path rust/Cargo.toml --locked --test group_e2ee_v2_multi_device_mls
cargo test --manifest-path rust/Cargo.toml --locked --test group_e2ee_v2_operations
cargo test --manifest-path rust/Cargo.toml --locked --test group_e2ee_typed_operations_tests
(cd golang && go test ./group_e2ee)