What
MlsManager::import_key_package (crates/offline-protocol-mls/src/manager.rs:632) accepts a contact key package whose lifetime range is arbitrarily long, e.g. not_after decades out or u64::MAX. The same applies to the re-validation in get_contact_key_package, which re-runs the identical checks on every cache read.
Both paths validate via OpenMLS KeyPackageIn::validate. That checks the window against now (not_before < now < not_after), but it never enforces the range width: OpenMLS declares MAX_LEAF_NODE_LIFETIME_RANGE_SECONDS (1 hour + 3 months) and never calls the predicate. Verified empirically during the leaf-crypto Stage 0 interop work (PR #394): a 1-year-lifetime package sails through OpenMLS validation.
Why it matters
The lifetime bound is what ages a key package out cryptographically. A package with an unbounded window, once cached in the install-scoped protocol-state store, is importable and usable for new session establishment forever. That extends the useful window of a leaked or compromised init/leaf private key from weeks to unlimited, and it defeats the intent behind bounding our own packages: outbound packages are minted with DEFAULT_KEY_PACKAGE_LIFETIME_SECS = 30 days (manager.rs:24), and the leaf profile pins LEAF_KEY_PACKAGE_LIFETIME = 28 days in offline-protocol-sealed. We bound what we emit and accept anything inbound.
This becomes more load-bearing with the leaf work (ADR 0021/0022): pairing trust for a leaf is "fresh key package over the pairing radio", and freshness is only meaningful if the importer enforces a bound.
Proposed fix
At both validation sites (import and cache re-read), after validate, reject a package whose not_after - not_before exceeds a cap. The natural cap is the OpenMLS declared constant (1h + 3 months), which both our 30-day default and the leaf's 28 days clear with margin. New error variant note: the FFI error enum is append-only (docs/bridges C2), and a new variant needs arms in both bridge mappers, so mapping to the existing InvalidKeyPackage variant with a distinct message is the cheap correct shape.
Provenance
Found during Stage 0 of the leaf payload crypto plan (roadmap item 1); recorded there as an SDK gap deserving its own issue, untouched by the Stage 2 relocation (PR #395).
What
MlsManager::import_key_package(crates/offline-protocol-mls/src/manager.rs:632) accepts a contact key package whose lifetime range is arbitrarily long, e.g.not_afterdecades out oru64::MAX. The same applies to the re-validation inget_contact_key_package, which re-runs the identical checks on every cache read.Both paths validate via OpenMLS
KeyPackageIn::validate. That checks the window against now (not_before < now < not_after), but it never enforces the range width: OpenMLS declaresMAX_LEAF_NODE_LIFETIME_RANGE_SECONDS(1 hour + 3 months) and never calls the predicate. Verified empirically during the leaf-crypto Stage 0 interop work (PR #394): a 1-year-lifetime package sails through OpenMLS validation.Why it matters
The lifetime bound is what ages a key package out cryptographically. A package with an unbounded window, once cached in the install-scoped protocol-state store, is importable and usable for new session establishment forever. That extends the useful window of a leaked or compromised init/leaf private key from weeks to unlimited, and it defeats the intent behind bounding our own packages: outbound packages are minted with
DEFAULT_KEY_PACKAGE_LIFETIME_SECS= 30 days (manager.rs:24), and the leaf profile pinsLEAF_KEY_PACKAGE_LIFETIME= 28 days inoffline-protocol-sealed. We bound what we emit and accept anything inbound.This becomes more load-bearing with the leaf work (ADR 0021/0022): pairing trust for a leaf is "fresh key package over the pairing radio", and freshness is only meaningful if the importer enforces a bound.
Proposed fix
At both validation sites (import and cache re-read), after
validate, reject a package whosenot_after - not_beforeexceeds a cap. The natural cap is the OpenMLS declared constant (1h + 3 months), which both our 30-day default and the leaf's 28 days clear with margin. New error variant note: the FFI error enum is append-only (docs/bridges C2), and a new variant needs arms in both bridge mappers, so mapping to the existingInvalidKeyPackagevariant with a distinct message is the cheap correct shape.Provenance
Found during Stage 0 of the leaf payload crypto plan (roadmap item 1); recorded there as an SDK gap deserving its own issue, untouched by the Stage 2 relocation (PR #395).