feat: add request-state key rotation - #1128
Open
gocamille wants to merge 7 commits into
Open
Conversation
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.
Adds targeted key selection and rolling-safe signing-key rotation to
RequestStateCodecfor SEP-2322requestStatevalues.Closes #1096.
Motivation and Context
The existing
rs1.<body>.<tag>format identifies the wire format but not the key that produced the HMAC. Rotating a deployment-wide request-state key therefore either invalidates every in-flight multi-round exchange or requires applications to trial-open each token against every retained key.That is particularly difficult for multi-replica deployments, where every replica that may continue an MRTR exchange must share the same signing keys and cannot safely switch them atomically.
How
Added an
rs2.<base64url(kid)>.<base64url(expiry || payload)>.<base64url(tag)>format. The key id is included in the HMAC input, allowing opening to select one key directly while preventing key-id substitution.Added
RequestStateCodec::new_with_keyring(active_kid, keys)for keyed sealing and targeted verification.active_kidselects the signing key, while any configured key can verify anrs2value naming that key.Key ids are opaque, case-sensitive UTF-8 strings limited to 255 bytes. They are authenticated but visible to clients and are not confidential.
Preserved
RequestStateCodec::new()and the existingrs1wire format byte-for-byte.Added
with_rs1_signing()andwith_rs1_fallback()for a rolling-safe migration:rs1with the old key.rs2, retaining the old key as anrs1fallback.Legacy fallback verification evaluates every configured
rs1fallback before returning, so the HMAC count does not reveal which fallback matched. The rustdocs recommend keeping this set small and temporary.Added distinct HMAC domains for
rs1andrs2, with length-prefixed key ids and associated data.Added explicit errors for unknown key ids, invalid wire key ids, and invalid keyring configuration. Configuration messages are documented as diagnostics rather than values applications should match programmatically.
The rustdocs require applications to map all token-opening failures to one client-visible error.
Added wire-format, key-selection, migration, and key-retirement guidance to the codec rustdocs and README.
How Has This Been Tested?
cargo test -p rmcp --features request-state model::request_state::tests --lib— 34 tests passed.cargo test -p rmcp --features request-state --doc— 33 doctests passed, including the staged-rotation example.cargo clippy --all-targets --all-features -- -D warningsrs1output is unchanged and independently verify the newrs2format.Breaking Changes
None. Existing
RequestStateCodec::new()callers continue to emit and accept the samers1format. Keyrings andrs2are opt-in behind the existingrequest-statefeature.New public API:
RequestStateCodec::new_with_keyringRequestStateCodec::with_rs1_signingRequestStateCodec::with_rs1_fallbackRequestStateError::{UnknownKeyId, InvalidKeyId, InvalidKeyring}RequestStateErrorremains#[non_exhaustive], so adding these variants does not require downstream callers to update exhaustive matches.Types of changes
Checklist
Additional context
Note that the codec intentionally does not provide runtime key reload or a default TTL. Applications must retain old verification keys until the maximum request-state lifetime has elapsed before retiring them.