Skip to content

🌊 Wave-37 — KeyPackage lifetime grace-window expiry + ExternalCommit ResumptionPSK misbinding (RFC 9420 §10.1 + §11.2.1+§15.1) - #953

Merged
gHashTag merged 1 commit into
mainfrom
feat/trios-chat-wave37
May 22, 2026
Merged

Conversation

@gHashTag

Copy link
Copy Markdown
Owner

Closes #952

Lane A — key_package_lifetime_grace_window_expiry (CR-CHAT-01)

validate_key_package_lifetime enforcing 7 rules from RFC 9420 §10.1 (KeyPackage Lifetime) + §5.3 (Credential Validation):

  1. Canonical leaf_id length (16 bytes — MLS LeafID anchor)
  2. Monotone lifetime (not_after >= not_before)
  3. Declared window ≤ KPLGW_MAX_LIFETIME_SECS (90 days = 7,776,000 s — OpenMLS default + RFC §10.1 ceiling)
  4. Not-yet-valid rejection (now < not_before)
  5. Expiry rejection (now > not_after; boundary now == not_after accepted inclusively per RFC §10.1)
  6. Harvest grace floor (now - not_before <= KPLGW_GRACE_FLOOR_SECS, 30 days — defence against KeyPackage hoarding)
  7. Zero not_after sentinel rejected (degenerate value that crosses integer boundaries)

10 deterministic tests KPLGW-01..10 covering all rejection branches + canonical accept + 90-day boundary + now == not_after boundary.

Error enum KeyPackageLifetimeError:

  • NonCanonicalLeafIdLength
  • InvertedLifetime
  • LifetimeWindowTooLong
  • NotYetValid
  • Expired
  • GraceFloorViolation
  • ZeroNotAfter

Constants: KPLGW_LEAF_ID_LEN=16, KPLGW_MAX_LIFETIME_SECS=7_776_000, KPLGW_GRACE_FLOOR_SECS=2_592_000.

Lane B — external_commit_resumption_psk_misbinding (CR-CHAT-03)

validate_external_commit_resumption_psk enforcing 7 rules from RFC 9420 §11.2.1 (External Commits) + §15.1 (Resumption PSKs):

  1. Canonical group_id length (32 bytes — MLS GroupID)
  2. Exactly one PSKid in psk_ids (zero or more-than-one rejected)
  3. PSK type = Resumption (0x01); External (0x02) rejected
  4. PSKid group_id matches commit group_id (no cross-group splicing)
  5. PSKid epoch matches local_epoch (the immediately prior epoch)
  6. PSKid psk_nonce matches the rejoiner-declared declared_nonce
  7. Canonical 32-byte psk_nonce length

10 deterministic tests ECRPM-01..10 covering all rejection branches + canonical accept.

Error enum ResumptionPskMisbindingError:

  • NonCanonicalGroupIdLength
  • MissingResumptionPsk
  • NotResumptionPskType
  • ResumptionGroupIdMismatch
  • ResumptionEpochMismatch
  • ResumptionNonceMismatch
  • NonCanonicalPskNonceLength

Constants: ECRPM_GROUP_ID_LEN=32, ECRPM_PSK_NONCE_LEN=32.

Falsifier corpus — +100 / 3600 cumulative

  • PI-KPLGW-001..050 — key_package_lifetime_grace_window_expiry category (50)
  • PI-ECRPM-001..050 — external_commit_resumption_psk_misbinding category (50)
  • 100% deny-pattern coverage verified offline
  • 0 collisions with expected_block=false entries
  • 72 falsifier categories total

Coq Section TrinityChatWave37 — INV-CHAT-238..247 + 4 helpers

10 new theorems + 4 helper lemmas, 0 axioms / 0 admissions. Cumulative Qed: 341 → 351.

  • inv_chat_238_kplgw_non_canonical_leaf_id_len_rejected
  • inv_chat_239_kplgw_inverted_lifetime_rejected
  • inv_chat_240_kplgw_window_too_long_rejected
  • inv_chat_241_kplgw_zero_not_after_rejected
  • inv_chat_242_kplgw_max_window_accepted
  • inv_chat_243_ecrpm_external_psk_type_rejected
  • inv_chat_244_ecrpm_cross_group_splice_rejected
  • inv_chat_245_ecrpm_stale_resumption_rejected
  • inv_chat_246_ecrpm_short_psk_nonce_rejected
  • inv_chat_247_ecrpm_canonical_psk_accepted

Helpers: kplgw_canonical_leaf_id_accepted_37, kplgw_one_not_after_accepted_37, ecrpm_canonical_psk_nonce_accepted_37, ecrpm_identical_group_id_accepted_37.

Why this wave matters

KeyPackage lifetime is the main administrative knob for forward secrecy: a stack that accepts year-old harvested KeyPackages turns harvest-now-decrypt-later into an ever-green attack. RFC 9420 §10.1 mandates a lifetime window but most stacks omit a meaningful harvest floor — even within the declared window, a KeyPackage whose not_before is months stale should not be the basis of a new Welcome.

ResumptionPSK binding is the main administrative knob for Post-Compromise Security: a stack that accepts cross-group or stale ResumptionPSKs in ExternalCommits lets an attacker with one historical epoch_secret rejoin the group forever and undo PCS healing. RFC 9420 §11.2.1 + §15.1 specify the binding but leave each consumer to enforce it; OpenMLS pre-0.5 and MLS++ pre-1.2 have shipped looser checks.

trios-chat now constructively forbids both at the ring boundary with deterministic test suites and machine-checked Coq invariants.

Stats footer

~648 tests · 25/25 e2e · 3600/3600 falsifier · 72 categories · 351 Coq Qed / 0 Admitted · 5 axioms · 0 unsafe · 0 monoliths

🤖 Generated with [Perplexity Computer]

Co-Authored-By: Trinity Grandmaster admin@t27.ai

…+ external-commit-resumption-psk-misbinding (RFC 9420 §10.1 + §11.2.1+§15.1)

Closes #952

## Lane A — `key_package_lifetime_grace_window_expiry` (CR-CHAT-01)

`validate_key_package_lifetime` enforcing **7 rules** from RFC 9420 §10.1 (KeyPackage Lifetime) + §5.3 (Credential Validation):

1. Canonical `leaf_id` length (16 bytes — MLS LeafID anchor)
2. Monotone lifetime (`not_after >= not_before`)
3. Declared window ≤ `KPLGW_MAX_LIFETIME_SECS` (90 days = 7,776,000 s — OpenMLS default + RFC §10.1 ceiling)
4. Not-yet-valid rejection (`now < not_before`)
5. Expiry rejection (`now > not_after`; boundary `now == not_after` accepted inclusively per RFC §10.1)
6. Harvest grace floor (`now - not_before <= KPLGW_GRACE_FLOOR_SECS`, 30 days — defence against KeyPackage hoarding)
7. Zero `not_after` sentinel rejected (degenerate value that crosses integer boundaries)

10 deterministic tests **KPLGW-01..10** covering all rejection branches + canonical accept + 90-day boundary + `now == not_after` boundary.

Error enum `KeyPackageLifetimeError`:
- `NonCanonicalLeafIdLength`
- `InvertedLifetime`
- `LifetimeWindowTooLong`
- `NotYetValid`
- `Expired`
- `GraceFloorViolation`
- `ZeroNotAfter`

Constants: `KPLGW_LEAF_ID_LEN=16`, `KPLGW_MAX_LIFETIME_SECS=7_776_000`, `KPLGW_GRACE_FLOOR_SECS=2_592_000`.

## Lane B — `external_commit_resumption_psk_misbinding` (CR-CHAT-03)

`validate_external_commit_resumption_psk` enforcing **7 rules** from RFC 9420 §11.2.1 (External Commits) + §15.1 (Resumption PSKs):

1. Canonical `group_id` length (32 bytes — MLS GroupID)
2. Exactly one PSKid in `psk_ids` (zero or more-than-one rejected)
3. PSK type = `Resumption` (0x01); `External` (0x02) rejected
4. PSKid `group_id` matches commit `group_id` (no cross-group splicing)
5. PSKid `epoch` matches `local_epoch` (the immediately prior epoch)
6. PSKid `psk_nonce` matches the rejoiner-declared `declared_nonce`
7. Canonical 32-byte `psk_nonce` length

10 deterministic tests **ECRPM-01..10** covering all rejection branches + canonical accept.

Error enum `ResumptionPskMisbindingError`:
- `NonCanonicalGroupIdLength`
- `MissingResumptionPsk`
- `NotResumptionPskType`
- `ResumptionGroupIdMismatch`
- `ResumptionEpochMismatch`
- `ResumptionNonceMismatch`
- `NonCanonicalPskNonceLength`

Constants: `ECRPM_GROUP_ID_LEN=32`, `ECRPM_PSK_NONCE_LEN=32`.

## Falsifier corpus — +100 / 3600 cumulative

- **PI-KPLGW-001..050** — `key_package_lifetime_grace_window_expiry` category (50)
- **PI-ECRPM-001..050** — `external_commit_resumption_psk_misbinding` category (50)
- 100% deny-pattern coverage verified offline
- 0 collisions with `expected_block=false` entries
- 72 falsifier categories total

## Coq Section TrinityChatWave37 — INV-CHAT-238..247 + 4 helpers

10 new theorems + 4 helper lemmas, **0 axioms / 0 admissions**. Cumulative Qed: 341 → 351.

- `inv_chat_238_kplgw_non_canonical_leaf_id_len_rejected`
- `inv_chat_239_kplgw_inverted_lifetime_rejected`
- `inv_chat_240_kplgw_window_too_long_rejected`
- `inv_chat_241_kplgw_zero_not_after_rejected`
- `inv_chat_242_kplgw_max_window_accepted`
- `inv_chat_243_ecrpm_external_psk_type_rejected`
- `inv_chat_244_ecrpm_cross_group_splice_rejected`
- `inv_chat_245_ecrpm_stale_resumption_rejected`
- `inv_chat_246_ecrpm_short_psk_nonce_rejected`
- `inv_chat_247_ecrpm_canonical_psk_accepted`

Helpers: `kplgw_canonical_leaf_id_accepted_37`, `kplgw_one_not_after_accepted_37`, `ecrpm_canonical_psk_nonce_accepted_37`, `ecrpm_identical_group_id_accepted_37`.

## Why this wave matters

KeyPackage lifetime is the main administrative knob for forward secrecy: a stack that accepts year-old harvested KeyPackages turns harvest-now-decrypt-later into an ever-green attack. RFC 9420 §10.1 mandates a lifetime window but most stacks omit a meaningful **harvest floor** — even within the declared window, a KeyPackage whose `not_before` is months stale should not be the basis of a new Welcome.

ResumptionPSK binding is the main administrative knob for Post-Compromise Security: a stack that accepts cross-group or stale ResumptionPSKs in ExternalCommits lets an attacker with one historical `epoch_secret` rejoin the group forever and undo PCS healing. RFC 9420 §11.2.1 + §15.1 specify the binding but leave each consumer to enforce it; OpenMLS pre-0.5 and MLS++ pre-1.2 have shipped looser checks.

trios-chat now constructively forbids both at the ring boundary with deterministic test suites and machine-checked Coq invariants.

## Stats footer

~648 tests · 25/25 e2e · 3600/3600 falsifier · 72 categories · 351 Coq Qed / 0 Admitted · 5 axioms · 0 unsafe · 0 monoliths

🤖 Generated with [Perplexity Computer]

Co-Authored-By: Trinity Grandmaster <admin@t27.ai>
@gHashTag
gHashTag merged commit d92feb6 into main May 22, 2026
9 of 13 checks passed
@gHashTag
gHashTag deleted the feat/trios-chat-wave37 branch May 22, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🌊 Wave-37 — KeyPackage lifetime grace-window expiry + ExternalCommit ResumptionPSK misbinding (RFC 9420 §10.1 + §11.2.1+§15.1)

1 participant