Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(typescript/encryption): cap aggregate skippedKeys across DH ratchet steps#2184

Merged
imran-siddique merged 1 commit into
microsoft:mainfrom
aegis-initiative:fix/typescript-ratchet-skipped-keys-cap
May 12, 2026
Merged

fix(typescript/encryption): cap aggregate skippedKeys across DH ratchet steps#2184
imran-siddique merged 1 commit into
microsoft:mainfrom
aegis-initiative:fix/typescript-ratchet-skipped-keys-cap

Conversation

@finnoybu

Copy link
Copy Markdown
Contributor

Problem

DoubleRatchet.skipMessages enforced a per-call cap (maxSkip, default MAX_SKIP = 100) on the size of a single burst of skipped messages within one receive chain:

if (until - s.recvMessageNumber > this.maxSkip) {
    throw new Error(`Too many skipped messages (${until - s.recvMessageNumber} > ${this.maxSkip})`);
}
while (s.recvMessageNumber < until) {
    const [mk, next] = kdfChain(s.chainKeyRecv);
    s.skippedKeys.set(skippedKeyId(s.dhRemotePublic!, s.recvMessageNumber), mk);
    s.chainKeyRecv = next;
    s.recvMessageNumber++;
}

But the skippedKeys map is not reset on a DH ratchet step (it is intentionally kept so out-of-order messages from old chains can still be decrypted). A long-lived session whose peer repeatedly forces fresh DH rotations while leaving small out-of-order tails behind in each chain accumulates skipped message keys indefinitely — memory grows in O(total skipped across the lifetime of the ratchet) rather than O(per-chain burst).

The Python core implementation already pins this: agent-governance-python/agent-mesh/src/agentmesh/encryption/ratchet.py defines _MAX_SKIPPED_KEYS_TOTAL: int = 2000 and FIFO-evicts the oldest entries. The TypeScript implementation had no equivalent.

Fix

Add a module-level MAX_SKIPPED_KEYS_TOTAL = 2000 and FIFO-evict the oldest entries from state.skippedKeys whenever it exceeds the cap. Map iteration order is insertion order in ES2015+, so s.skippedKeys.keys().next().value returns the oldest surviving insertion — the same pattern Python's next(iter(d)) uses. The cap is intentionally kept identical to Python (2000) so the two SDKs have matching memory bounds for the same wire protocol.

Test

Added a regression test in tests/encryption.test.ts that:

  1. Raises maxSkip to 2051 via DoubleRatchet.fromState, so the per-chain cap does not gate the burst.
  2. Has Alice encrypt 2050 messages that Bob will skip, then a final one we deliver.
  3. Asserts the final decrypts correctly AND that bRatchet.getState().skippedKeys.size === 2000 after delivery (the oldest 50 were evicted FIFO).
PASS  tests/encryption.test.ts
  ...
  Tests:       26 passed, 26 total

Recipe: cd agent-governance-typescript && npx jest tests/encryption.test.ts --no-coverage.


Surfaced during independent audit conducted by @finnoybu (Ken Tannenbaum, AEGIS Initiative); [LOW, TypeScript].

…et steps

`DoubleRatchet.skipMessages` enforced a per-call cap (`maxSkip`) on the
size of a single burst of skipped messages within one receive chain, but
the `skippedKeys` map was unbounded across DH ratchet steps. A long-
lived session whose peer repeatedly forced fresh DH rotations while
leaving small out-of-order tails behind in each chain would accumulate
skipped message keys indefinitely — memory grows in O(total skipped
across the lifetime of the ratchet) rather than O(per-chain burst).

Add a module-level `MAX_SKIPPED_KEYS_TOTAL = 2000` and FIFO-evict the
oldest entries from `state.skippedKeys` whenever it exceeds the cap.
`Map` iteration order is insertion order in ES2015+, so the first key
the iterator returns is the oldest surviving insertion — same pattern
the Python core implementation uses (`_MAX_SKIPPED_KEYS_TOTAL: int = 2000`)
in `agent-governance-python/agent-mesh/src/agentmesh/encryption/ratchet.py`.

Add a regression test that raises `maxSkip` above the global cap, skips
a burst large enough to overflow it, and asserts the cached
`skippedKeys.size` after delivery is exactly `MAX_SKIPPED_KEYS_TOTAL`
(not larger). The full `tests/encryption.test.ts` suite (26 tests) still
passes.
@github-actions github-actions Bot added the tests label May 12, 2026
@github-actions

Copy link
Copy Markdown
🤖 AI Agent: code-reviewer — View details

TL;DR: 0 blockers, 0 warnings. No issues found. Clean change.

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: security-scanner — View details

No security issues found.

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: test-generator — `ratchet.ts`

ratchet.ts

  • test skippedKeys cache is bounded by the aggregate cap -- verifies that the skippedKeys map does not exceed the maximum size after multiple DH ratchet steps.

@github-actions github-actions Bot added the size/M Medium PR (< 200 lines) label May 12, 2026
@github-actions

Copy link
Copy Markdown
🤖 AI Agent: docs-sync-checker — Docs Sync

Docs Sync

  • MAX_SKIPPED_KEYS_TOTAL in ratchet.ts -- missing docstring
  • README.md -- section on encryption needs update
  • CHANGELOG.md -- missing entry for behavioral change regarding skippedKeys cap

@github-actions

Copy link
Copy Markdown
🤖 AI Agent: breaking-change-detector — API Compatibility

API Compatibility

Severity Change Impact
Potentially Breaking Introduced a cap on the total number of cached skipped keys (MAX_SKIPPED_KEYS_TOTAL = 2000) with FIFO eviction Existing implementations relying on unlimited skipped keys may encounter unexpected behavior if they exceed the new limit.

@github-actions

Copy link
Copy Markdown

🟡 Contributor Check: MEDIUM

Check Result
Profile MEDIUM
Credential NONE
Overall MEDIUM

Automated check by AGT Contributor Check.

@github-actions github-actions Bot added the needs-review:MEDIUM Contributor check flagged MEDIUM risk label May 12, 2026
@github-actions

Copy link
Copy Markdown

PR Review Summary

Check Status Details
🔍 Code Review ✅ Passed No issues found
🛡️ Security Scan ✅ Passed No issues found
🔄 Breaking Changes ✅ Completed Analysis complete
📝 Docs Sync ✅ Completed Analysis complete
🧪 Test Coverage ✅ Completed Analysis complete

Verdict: ✅ Ready for human review

@imran-siddique imran-siddique merged commit 6e8eee1 into microsoft:main May 12, 2026
13 of 14 checks passed
MohammadHaroonAbuomar pushed a commit to MohammadHaroonAbuomar/agt-acs that referenced this pull request Jun 1, 2026
…et steps (microsoft#2184)

`DoubleRatchet.skipMessages` enforced a per-call cap (`maxSkip`) on the
size of a single burst of skipped messages within one receive chain, but
the `skippedKeys` map was unbounded across DH ratchet steps. A long-
lived session whose peer repeatedly forced fresh DH rotations while
leaving small out-of-order tails behind in each chain would accumulate
skipped message keys indefinitely — memory grows in O(total skipped
across the lifetime of the ratchet) rather than O(per-chain burst).

Add a module-level `MAX_SKIPPED_KEYS_TOTAL = 2000` and FIFO-evict the
oldest entries from `state.skippedKeys` whenever it exceeds the cap.
`Map` iteration order is insertion order in ES2015+, so the first key
the iterator returns is the oldest surviving insertion — same pattern
the Python core implementation uses (`_MAX_SKIPPED_KEYS_TOTAL: int = 2000`)
in `agent-governance-python/agent-mesh/src/agentmesh/encryption/ratchet.py`.

Add a regression test that raises `maxSkip` above the global cap, skips
a burst large enough to overflow it, and asserts the cached
`skippedKeys.size` after delivery is exactly `MAX_SKIPPED_KEYS_TOTAL`
(not larger). The full `tests/encryption.test.ts` suite (26 tests) still
passes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-review:MEDIUM Contributor check flagged MEDIUM risk size/M Medium PR (< 200 lines) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants