[tracking] germ-integration — do not merge - #5
Draft
germ-mark wants to merge 13 commits into
Draft
Conversation
germ-mark
force-pushed
the
germ-integration
branch
7 times, most recently
from
July 28, 2026 19:18
b5ed34c to
c4e7dfc
Compare
Xcode 26 / the macOS 26 SDK report `librariesRequireRPath = true` for every Apple target, including iOS, which tripped the guard in `configure()` and failed the build outright. Shipping the cdylib inside an `@rpath/....framework` is exactly the intended layout, so downgrade the panic to a warning and keep emitting the Swift runtime link-search paths. Also drop MIN_OSX_DEPLOYMENT_TARGET from 26.0 to 15.0: macOS ships the Swift runtime in the OS, so a modern target needs no rpath, and 26.0 tripped the same guard even for the host bindgen build. Previously carried only on germ-shadow-safe-exporter (ec69dc2), outside any feature branch.
Implement the forward-secure exporter of draft-ietf-mls-extensions-08 Section 4.4 behind a new (default-off) safe_extensions feature: * Derive application_export_secret = DeriveSecret(epoch_secret, "application_export") at the beginning of the epoch, alongside the other Table 4 secrets of RFC 9420. * Use it as the root of an Exporter Tree with 2^16 leaves that reuses the Secret Tree structure and ExpandWithLabel(., "tree", "left" | "right") derivation of RFC 9420 Section 9, materialized on demand via the existing SecretTree type. * Group::safe_export_secret(component_id) returns the leaf secret for a component. The secret is regarded as consumed once exported and source material is deleted per the RFC 9420 Section 9.2 deletion schedule; a second export in the same epoch fails. * Group::derive_secret exposes RFC 9420 DeriveSecret so callers can derive multiple independent values from one exported secret. draft -08 declares uint32 ComponentID but sizes the tree at 2^16 leaves (draft -09 narrows ComponentID to uint16). We keep the crate's existing u32 ComponentID and reject ids >= 2^16 rather than truncate. Enabling the feature adds a field to the serialized EpochSecrets, so stored group state is not portable across builds that differ in this feature. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement psk_type = application(3) of draft-ietf-mls-extensions-08
Section 4.5 behind the safe_extensions feature:
* New public ApplicationPsk { component_id, psk_id } and a
JustPreSharedKeyID::Application variant with PSKType 3. The wire
format follows -08 (uint32 ComponentID); the external and resumption
variants stay byte-identical to RFC 9420, with codec tests pinning
all three encodings.
* CommitBuilder::add_application_psk mirrors add_external_psk.
* Application PSK values are resolved from the group's
PreSharedKeyStorage under ApplicationPsk::storage_id, the serialized
psktype and type-specific fields (0x03 || component_id || psk_id<V>),
so lookups are component-bound. Proposal validation checks storage
the same way it does for external PSKs.
* PreSharedKeyProposal::application_psk lets MlsRules::filter_proposals
classify pre-shared key proposals as external, application, or
resumption.
Together with Group::safe_export_secret this provides the primitives
required by draft-ietf-mls-combiner-02 Section 6.2 (apq_psk derivation
via SafeExportSecret and import as an application PSK), exercised by an
end-to-end commit test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Key hygiene and coverage: * Reset the exporter tree when demoting an epoch to PriorEpoch — it is only readable while its epoch is current, so retaining it kept deletable key material alive with no reader (RFC 9420 Section 9.2). * Pin the epoch_secret -> "application_export" -> leaf derivation in a known-answer test; member-agreement tests cannot catch a wrong label. * Add a value-mismatch test proving the application PSK is mixed into the key schedule (a member with a different stored value fails with InvalidConfirmationTag). Robustness and hygiene: * Redact psk_id bytes in ApplicationPsk's Debug via pretty_bytes, matching PskGroupId/PskNonce/ExternalPskId. * Derive storage_id by encoding JustPreSharedKeyID::Application instead of hardcoding the discriminant byte a second time. * Document that application PSK storage keys share the ExternalPskId namespace with app-chosen external PSK ids. * Use T::zero() (matching SecretTree::empty) instead of T::default() in take_leaf_secret's empty-tree guard. * Make PreSharedKeyProposal::application_psk match exhaustively so a future PSK type fails closed at compile time. * Document exporter-tree snapshot growth on Group::safe_export_secret and member-local divergence on Group::equal_group_state. Deduplication: * Extract SecretTree::take_leaf_node, sharing the root-to-leaf copath-consume walk between take_leaf_ratchet and take_leaf_secret. * Collapse the duplicated external/application storage presence check in filter_out_invalid_psks behind one Cow-keyed lookup. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…aves add_application_psk accepted any u32 component_id, so a commit could reference an application PSK whose component_id has no leaf in the 2^16-leaf Exporter Tree -- a PSK no member could ever export to install. Reject an out-of-range component_id up front with MlsError::InvalidComponentId, matching the bound safe_export_secret already enforces on the export side. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tree
Implements the draft-sullivan-mls-attachments key schedule as a layer over
the exporter tree, rather than as a second copy of it. The exporter tree's
root is the application_export_secret, so no new epoch state is needed:
CEK = ExpandWithLabel(SafeExportSecret(component_id),
ComponentOperationLabel(component_id, "attachment"),
object_id, 32)
- SecretTree::peek_leaf_secret derives a leaf from the deepest retained
ancestor without mutating the tree, so a component whose references arrive
after the fact stays derivable. Group::safe_export_secret keeps its
consume semantics; a component should use one or the other.
- New group::attachment module: the -09 ComponentOperationLabel
("MLS Component", uint16, label<V>) — a different encoding from the
older-draft struct in component_operation, which is unchanged — and the
per-object CEK, object_id bounded to 1..=255 bytes.
- Group API: attachment_cek, attachment_cek_at_epoch, delete_component_secret.
insert_past_epoch no longer clears the exporter tree when archiving. That
clearing was correct while every reader consumed on export; attachment CEKs
need the epoch's tree for as long as the epoch is retained, and a prior epoch
already retains strictly more sensitive material in secret_tree. Anything
consumed or deleted during the epoch stays gone.
No serialized struct changes shape relative to the exporter tree commits, and
no cargo feature is enabled anywhere in the tree — consumers opt into
safe_extensions themselves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
legacy_interop is gated off when safe_extensions is on, because that feature changed the EpochSecrets layout — so that build had no stored-state coverage at all. Pin real blobs captured from germ-shadow-safe-exporter, the build currently in production. - The shipped Snapshot still loads and still yields attachment CEKs, which covers both the upstream resync and the attachment work on top of it. - The shipped PriorEpoch decodes but declines to derive: that build cleared the exporter tree when archiving, so pre-upgrade epochs must degrade rather than return a wrong key. Gated off under prior_epoch_membership_key, which appends a field to PriorEpoch and so cannot decode blobs captured without it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
No commit in this fork — including the shipped release pin — had ever been checked by anything but a laptop. Actions was disabled, and several upstream workflows cannot run here regardless: Benchmarks on Merge assumes an IAM role in awslabs' AWS account, Pull Request Slack Notifier needs their webhook, and Native's coverage step needs their Codecov token. Nothing here needs a credential. Nine jobs: tests on Linux and macOS (default, safe_extensions, TwoMLSPQ's exact feature set, all-features), lint, cargo-hack --each-feature, mls_build_async, a thumbv6m-none-eabi build, security audit, wasm, and fuzz. The last three are folded in from upstream workflows now disabled on this fork; disabling rather than deleting keeps those files identical to upstream so they resync cleanly. Feature-specific steps are gated on a probe of mls-rs/Cargo.toml, because `safe_extensions` does not exist on every ref this can run against — not on main, not on this branch, not on germ-crypto-providers. Without the gate every such step fails with "the package 'mls-rs' does not contain this feature", which would make the workflow_dispatch ref input useless for exactly the branches worth checking with it. Three things this had to get right, each found by a red run: - `rustflags: ''` on every setup-rust-toolchain step. The action defaults RUSTFLAGS to `-D warnings`, which promotes pre-existing upstream warnings (unused imports in mls-rs and mls-rs-identity-x509) into build errors and failed three jobs for reasons unrelated to the code under test. Strictness belongs where the lint job puts it — passed to clippy explicitly. - macOS on macos-26. mls-rs-crypto-cryptokit's cryptokit-bridge declares swift-tools-version 6.2; macos-14 ships Xcode 15.4 / Swift 5.10 and fails in the build script before compiling anything. - No `wasm-pack test --test '*'`. client_tests exceeds ChromeDriver's 30s renderer timeout, the driver is SIGKILLed, and wasm-bindgen then fails to parse the truncated response as `missing field 'chunk'`. Not a code failure: the 395 wasm lib tests pass on the same target. Triggers on push to germ-integration and on pull requests. Release pins are frozen, so a workflow file cannot be added to one without moving it; workflow_dispatch takes a ref input instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main mirrors upstream; each Germ change lives on its own feature branch rebased onto it; germ-integration is their composition; release pins are immutable. FORK.md records: that no PR in this fork ever merges, because main is a fast-forward-only mirror and every PR exists only to give a change a reviewable diff; the reconstruction recipe for germ-integration, expressed in branch names and cherry-pick ranges so it survives rebases; which consumer turns on safe_extensions and where; how to audit for commits belonging to no feature branch; and why each release pin needs a captured state fixture. The crate README gets a pointer only, so it keeps resyncing from upstream cleanly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
germ-mark
force-pushed
the
germ-integration
branch
from
July 28, 2026 19:27
c4e7dfc to
b43703f
Compare
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.
Draft, permanently. This PR renders the running diff between
mainand the composed Germ stack.No PR in this fork is ever merged.
mainis a fast-forward-only mirror of upstream, so merging anything into it would put Germ commits on the mirror and break that invariant. That applies to #4, #6 and #7 as much as to this one — they exist to give each change a reviewable diff and a discussion thread, not to land. Content leaves the fork by being submitted upstream toawslabs/mls-rs;germ-integrationreaches consumers by being pinned.See FORK.md.
Reconstruction order
germ-integrationis defined by this recipe, not by its commit history. It takes only branch names, so it survives rebases:germ-crypto-providersfix/cryptokit-build-rpathfeat/safe-extensions-exporterfeat/attachment-cekci/fork-ciThe fourth range is
feat/safe-extensions-exporter..feat/attachment-cek, notmain..— #7 is stacked on #4 and carries its commits, somain..would apply them twice.Reconstruction is deterministic. Rebuilding from these five ranges reproduces the previous tree hash exactly. Verified twice while adding
ci/fork-ci: each rebuild differed only in the files intentionally changed, nothing else moved. Current tree7209a6ca…. Re-check withgit diff --stat <previous> HEADafter any rebuild.Auditing
Every commit here should be reachable from a feature branch. Compare by
git patch-id --stable, not commit id — cherry-picking rewrites ids. The FORK.md commit is the one legitimate exception, since it describes the composition.That audit is not decorative:
fix/cryptokit-build-rpathrode on the shipped release pin for months without ever appearing in a PR, and was only found by running it.Verification
CI now runs on this fork (#8) and is green here — all nine jobs on the PR run, seven on push with
wasm/fuzzskipped by design. Before that, nothing in this repo had been machine-verified: Actions was disabled andtotal_countwas 0 runs, including for the shipped release pin.Covered on Linux and macOS:
cargo hack build --each-featureacross all 35 combinations · 435 default (legacy_interopungated) · 464safe_extensions· 524--all-features· 464 undermls_build_async· 465 on TwoMLSPQ's exact feature set · 200--no-default-features, 449 withrfc_compliant,safe_extensions·thumbv6m-none-eabi·cargo audit· wasm (395 lib tests + 3 crates) · six fuzz targets at 10k runs · clippy-D warningsin four configurations ·fmtclean.Still not covered: Windows, and the interop matrix (needs protoc and kotlin).
mls-rs-crypto-awslcis excluded throughout — it builds aws-lc from source and TwoMLSPQ does not use it.