#![allow(clippy::redundant_pub_crate)]
use crate::error::{Error, Result};
use crate::types::mrtr::CanonicalDepthExceeded;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use ring::aead::{Aad, LessSafeKey, Nonce, UnboundKey, CHACHA20_POLY1305, NONCE_LEN};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::sync::Arc;
use std::time::Duration;
use zeroize::Zeroize;
pub(crate) const KEY_LEN: usize = 32;
const KEY_ID_LEN_U8: u8 = 8;
pub(crate) const KEY_ID_LEN: usize = KEY_ID_LEN_U8 as usize;
pub(crate) const DEFAULT_TTL_SECS: u64 = 300;
pub(crate) const ENV_REQUEST_STATE_KEY: &str = "PMCP_REQUEST_STATE_KEY";
pub(crate) const ENV_REQUEST_STATE_KEY_PREVIOUS: &str = "PMCP_REQUEST_STATE_KEY_PREVIOUS";
pub(crate) const ENV_REQUEST_STATE_TTL_SECS: &str = "PMCP_REQUEST_STATE_TTL_SECS";
pub(crate) type SecretKey = zeroize::Zeroizing<[u8; KEY_LEN]>;
fn env_var(name: &str) -> Option<String> {
#[cfg(test)]
{
let overridden: Option<Option<String>> =
tests::ENV_OVERRIDE.with(|o| o.borrow().as_ref().map(|m| m.get(name).cloned()));
if let Some(value) = overridden {
return value;
}
}
std::env::var(name).ok()
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) struct KeyId([u8; KEY_ID_LEN]);
impl KeyId {
pub(crate) const fn as_bytes(&self) -> &[u8; KEY_ID_LEN] {
&self.0
}
}
impl std::fmt::Display for KeyId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for byte in &self.0 {
write!(f, "{byte:02x}")?;
}
Ok(())
}
}
impl std::fmt::Debug for KeyId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "KeyId({self})")
}
}
pub(crate) fn key_id_of(key: &[u8]) -> KeyId {
let mut hasher = Sha256::new();
hasher.update(key);
let digest = hasher.finalize();
let mut id = [0u8; KEY_ID_LEN];
id.copy_from_slice(&digest[..KEY_ID_LEN]);
KeyId(id)
}
pub(crate) trait RequestStateClock: Send + Sync + std::fmt::Debug {
fn now_unix(&self) -> i64;
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct SystemClock;
impl RequestStateClock for SystemClock {
fn now_unix(&self) -> i64 {
chrono::Utc::now().timestamp()
}
}
#[cfg(test)]
#[derive(Debug, Clone, Copy)]
pub(crate) struct FixedClock(pub i64);
#[cfg(test)]
impl RequestStateClock for FixedClock {
fn now_unix(&self) -> i64 {
self.0
}
}
pub(crate) struct RequestStateCodec {
minting: (KeyId, LessSafeKey),
accepting: Vec<(KeyId, LessSafeKey)>,
ttl: Duration,
clock: Arc<dyn RequestStateClock>,
}
impl std::fmt::Debug for RequestStateCodec {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RequestStateCodec")
.field("minting_key_id", &self.minting.0)
.field("accepting_key_ids", &self.accepting_key_ids())
.field("ttl", &self.ttl)
.finish()
}
}
fn bind_key(key: &[u8]) -> Result<(KeyId, LessSafeKey)> {
if key.len() != KEY_LEN {
return Err(Error::validation(format!(
"a requestState key must be exactly {KEY_LEN} bytes, got {}",
key.len()
)));
}
let unbound = UnboundKey::new(&CHACHA20_POLY1305, key)
.map_err(|_| Error::internal("ring rejected a 32-byte CHACHA20_POLY1305 key"))?;
Ok((key_id_of(key), LessSafeKey::new(unbound)))
}
impl RequestStateCodec {
pub(crate) fn new(key: &[u8; KEY_LEN], ttl: Duration) -> Result<Self> {
Self::from_key_slice(key, ttl)
}
fn from_key_slice(key: &[u8], ttl: Duration) -> Result<Self> {
let minting = bind_key(key)?;
let accepting = vec![minting.clone()];
Ok(Self {
minting,
accepting,
ttl,
clock: Arc::new(SystemClock),
})
}
pub(crate) fn from_env() -> Result<Self> {
let ttl = ttl_from_env();
let codec = match env_var(ENV_REQUEST_STATE_KEY) {
Some(raw) => Self::from_configured_key(&raw, ttl)?,
None => Self::from_generated_key(ttl)?,
};
codec.with_env_previous_key()
}
fn with_env_previous_key(mut self) -> Result<Self> {
if let Some(raw) = env_var(ENV_REQUEST_STATE_KEY_PREVIOUS) {
self.accepting
.push(bind_scrubbed(&raw, ENV_REQUEST_STATE_KEY_PREVIOUS)?);
}
Ok(self)
}
fn from_configured_key(raw: &str, ttl: Duration) -> Result<Self> {
let mut scrubbed = raw.to_string();
let decoded = decode_key_material(&scrubbed, ENV_REQUEST_STATE_KEY);
scrubbed.zeroize();
let mut decoded = decoded?;
let built = Self::from_key_slice(&decoded, ttl);
decoded.zeroize();
built
}
fn from_generated_key(ttl: Duration) -> Result<Self> {
let mut key = random_key()?;
let built = Self::from_key_slice(&key, ttl);
key.zeroize();
let codec = built?;
tracing::warn!(
env_var = ENV_REQUEST_STATE_KEY,
key_id = %codec.minting.0,
"PMCP_REQUEST_STATE_KEY is not set — generated a per-process requestState \
key. Multi-round-trip requests whose follow-up lands on a DIFFERENT \
instance behind a load balancer cannot be resumed and will be \
re-elicited. Set PMCP_REQUEST_STATE_KEY to the SAME 32-byte \
base64url (or hex) value on every instance to enable resumption."
);
Ok(codec)
}
pub(crate) fn with_previous_keys(
mut self,
keys: impl IntoIterator<Item = [u8; KEY_LEN]>,
) -> Result<Self> {
for mut key in keys {
let bound = bind_key(&key);
key.zeroize();
self.accepting.push(bound?);
}
Ok(self)
}
#[cfg(test)]
#[must_use]
pub(crate) fn with_clock(mut self, clock: Arc<dyn RequestStateClock>) -> Self {
self.clock = clock;
self
}
#[must_use]
pub(crate) fn with_ttl(mut self, ttl: Duration) -> Self {
self.ttl = ttl;
self
}
#[cfg(test)]
pub(crate) const fn minting_key_id(&self) -> KeyId {
self.minting.0
}
pub(crate) fn accepting_key_ids(&self) -> Vec<KeyId> {
self.accepting.iter().map(|(id, _)| *id).collect()
}
#[cfg(test)]
pub(crate) const fn ttl(&self) -> Duration {
self.ttl
}
#[cfg(test)]
pub(crate) fn now_unix(&self) -> i64 {
self.clock.now_unix()
}
#[cfg(test)]
fn with_forced_minting_key_id(mut self, id: KeyId) -> Self {
self.minting.0 = id;
if let Some(first) = self.accepting.first_mut() {
first.0 = id;
}
self
}
#[cfg(test)]
fn with_forced_accepting_key(mut self, id: KeyId, key: &[u8; KEY_LEN]) -> Result<Self> {
let (_, bound) = bind_key(key)?;
self.accepting.push((id, bound));
Ok(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub(crate) struct Continuation {
pub state: serde_json::Value,
pub exp: i64,
pub round: u8,
#[serde(default)]
pub kinds: Option<crate::types::mrtr::InputRequestKinds>,
}
#[derive(Debug, Clone)]
pub(crate) struct RequestBinding<'a> {
pub principal: &'a str,
pub method: &'a str,
pub param_digest: [u8; 32],
}
impl<'a> RequestBinding<'a> {
pub(crate) fn from_request(
principal: &'a str,
method: &'a str,
params: &serde_json::Value,
) -> std::result::Result<Self, CanonicalDepthExceeded> {
Ok(Self {
principal,
method,
param_digest: crate::types::mrtr::salient_param_digest(method, params)?,
})
}
fn aad(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(
self.principal.len() + self.method.len() + 2 + self.param_digest.len(),
);
out.extend_from_slice(self.principal.as_bytes());
out.push(0x00);
out.extend_from_slice(self.method.as_bytes());
out.push(0x00);
out.extend_from_slice(&self.param_digest);
out
}
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Verdict {
Ok(Continuation),
Expired(Continuation),
UnknownKey,
AuthFailed,
}
struct TokenParts<'a> {
key_id: KeyId,
nonce: [u8; NONCE_LEN],
sealed: &'a [u8],
}
fn decode_token(token: &str) -> Option<Vec<u8>> {
if token.is_empty() || token.len() > crate::types::mrtr::MAX_REQUEST_STATE_LEN {
return None;
}
URL_SAFE_NO_PAD.decode(token.as_bytes()).ok()
}
fn split_key_id(raw: &[u8]) -> Option<TokenParts<'_>> {
let (&declared_len, rest) = raw.split_first()?;
if usize::from(declared_len) != KEY_ID_LEN {
return None;
}
if rest.len() <= KEY_ID_LEN + NONCE_LEN {
return None;
}
let (id_bytes, rest) = rest.split_at(KEY_ID_LEN);
let (nonce_bytes, sealed) = rest.split_at(NONCE_LEN);
let mut key_id = [0u8; KEY_ID_LEN];
key_id.copy_from_slice(id_bytes);
let mut nonce = [0u8; NONCE_LEN];
nonce.copy_from_slice(nonce_bytes);
Some(TokenParts {
key_id: KeyId(key_id),
nonce,
sealed,
})
}
fn open_sealed(
key: &LessSafeKey,
nonce: [u8; NONCE_LEN],
aad: &[u8],
sealed: &[u8],
) -> Option<Continuation> {
let mut buffer = sealed.to_vec();
let opened = key
.open_in_place(
Nonce::assume_unique_for_key(nonce),
Aad::from(aad),
&mut buffer,
)
.ok()
.and_then(|plaintext| serde_json::from_slice::<Continuation>(plaintext).ok());
buffer.zeroize();
opened
}
impl RequestStateCodec {
pub(crate) fn mint(
&self,
state: &serde_json::Value,
binding: &RequestBinding<'_>,
round: u8,
kinds: Option<crate::types::mrtr::InputRequestKinds>,
) -> Result<String> {
let ttl_secs = i64::try_from(self.ttl.as_secs()).unwrap_or(i64::MAX);
let continuation = Continuation {
state: state.clone(),
exp: self.clock.now_unix().saturating_add(ttl_secs),
round,
kinds,
};
let mut sealed = serde_json::to_vec(&continuation).map_err(|e| {
Error::internal(format!(
"requestState continuation is not serializable: {e}"
))
})?;
sealed.reserve(CHACHA20_POLY1305.tag_len());
let mut nonce = [0u8; NONCE_LEN];
getrandom::fill(&mut nonce)
.map_err(|e| Error::internal(format!("CSPRNG (getrandom) failed: {e}")))?;
let aad = binding.aad();
self.minting
.1
.seal_in_place_append_tag(
Nonce::assume_unique_for_key(nonce),
Aad::from(aad.as_slice()),
&mut sealed,
)
.map_err(|_| Error::internal("requestState AEAD sealing failed"))?;
let mut raw = Vec::with_capacity(1 + KEY_ID_LEN + NONCE_LEN + sealed.len());
raw.push(KEY_ID_LEN_U8);
raw.extend_from_slice(self.minting.0.as_bytes());
raw.extend_from_slice(&nonce);
raw.extend_from_slice(&sealed);
let token = URL_SAFE_NO_PAD.encode(&raw);
if token.len() > crate::types::mrtr::MAX_REQUEST_STATE_LEN {
return Err(Error::validation(format!(
"minted requestState token is {} bytes, over the {} byte accepted \
bound — the continuation state is too large to be self-contained",
token.len(),
crate::types::mrtr::MAX_REQUEST_STATE_LEN
)));
}
Ok(token)
}
pub(crate) fn verify(&self, token: &str, binding: &RequestBinding<'_>) -> Verdict {
let Some(raw) = decode_token(token) else {
return Verdict::AuthFailed;
};
let Some(parts) = split_key_id(&raw) else {
return Verdict::AuthFailed;
};
if !self.has_candidate_key(parts.key_id) {
return Verdict::UnknownKey;
}
let aad = binding.aad();
for (candidate, key) in &self.accepting {
if *candidate != parts.key_id {
continue;
}
if let Some(continuation) = open_sealed(key, parts.nonce, &aad, parts.sealed) {
return self.check_expiry(continuation);
}
}
Verdict::AuthFailed
}
fn has_candidate_key(&self, id: KeyId) -> bool {
self.accepting.iter().any(|(candidate, _)| *candidate == id)
}
fn check_expiry(&self, continuation: Continuation) -> Verdict {
if continuation.exp <= self.clock.now_unix() {
Verdict::Expired(continuation)
} else {
Verdict::Ok(continuation)
}
}
}
fn bind_scrubbed(raw: &str, var: &str) -> Result<(KeyId, LessSafeKey)> {
let mut scrubbed = raw.to_string();
let decoded = decode_key_material(&scrubbed, var);
scrubbed.zeroize();
let mut decoded = decoded?;
let bound = bind_key(&decoded);
decoded.zeroize();
bound
}
fn decode_key_material(raw: &str, var: &str) -> Result<Vec<u8>> {
let trimmed = raw.trim();
let attempts = [
URL_SAFE_NO_PAD.decode(trimmed.as_bytes()).ok(),
decode_hex(trimmed),
];
let mut accepted: Option<Vec<u8>> = None;
for mut bytes in attempts.into_iter().flatten() {
if accepted.is_none() && bytes.len() == KEY_LEN {
accepted = Some(bytes);
continue;
}
bytes.zeroize();
}
if let Some(bytes) = accepted {
return Ok(bytes);
}
Err(Error::validation(format!(
"{var} must decode to exactly {KEY_LEN} bytes as base64url-no-pad or hex; \
the configured value does not. Generate one with: \
`head -c 32 /dev/urandom | base64 | tr '+/' '-_' | tr -d '='`"
)))
}
fn decode_hex(s: &str) -> Option<Vec<u8>> {
if s.is_empty() || !s.len().is_multiple_of(2) || !s.is_ascii() {
return None;
}
let mut out = Vec::with_capacity(s.len() / 2);
for pair in s.as_bytes().chunks_exact(2) {
let hi = char::from(pair[0]).to_digit(16)?;
let lo = char::from(pair[1]).to_digit(16)?;
out.push(u8::try_from(hi * 16 + lo).ok()?);
}
Some(out)
}
fn random_key() -> Result<[u8; KEY_LEN]> {
crate::shared::pkce::random_bytes()
}
fn ttl_from_env() -> Duration {
env_var(ENV_REQUEST_STATE_TTL_SECS)
.and_then(|raw| raw.trim().parse::<u64>().ok())
.map_or_else(
|| Duration::from_secs(DEFAULT_TTL_SECS),
Duration::from_secs,
)
}
pub(crate) fn resolve_codec_at_build(
accept_list: &[crate::types::ProtocolVersion],
key: Option<&SecretKey>,
previous_keys: &[SecretKey],
ttl: Option<Duration>,
) -> Result<Option<Arc<RequestStateCodec>>> {
if !crate::types::protocol::context::is_v2_opted_in(accept_list) {
return Ok(None);
}
let effective_ttl = ttl.unwrap_or_else(ttl_from_env);
let codec = match key {
Some(explicit) => {
RequestStateCodec::new(explicit, effective_ttl)?.with_env_previous_key()?
},
None => RequestStateCodec::from_env()?.with_ttl(effective_ttl),
};
let codec = codec.with_previous_keys(previous_keys.iter().map(|k| **k))?;
Ok(Some(Arc::new(codec)))
}
#[cfg(feature = "fuzzing")]
pub mod fuzz_support {
use super::{RequestBinding, RequestStateCodec, Verdict, DEFAULT_TTL_SECS, KEY_LEN};
use std::time::Duration;
pub const VERDICT_OK: u8 = 0;
pub const VERDICT_EXPIRED: u8 = 1;
pub const VERDICT_UNKNOWN_KEY: u8 = 2;
pub const VERDICT_AUTH_FAILED: u8 = 3;
pub const VERDICT_UNAVAILABLE: u8 = 4;
const FIXED_KEY: [u8; KEY_LEN] = [0x5a; KEY_LEN];
#[must_use]
pub fn verify_bytes(input: &[u8]) -> u8 {
let Ok(codec) = RequestStateCodec::new(&FIXED_KEY, Duration::from_secs(DEFAULT_TTL_SECS))
else {
return VERDICT_UNAVAILABLE;
};
let token = String::from_utf8_lossy(input);
let params = serde_json::json!({ "name": "fuzz", "arguments": {} });
let Ok(binding) = RequestBinding::from_request("fuzz-principal", "tools/call", ¶ms)
else {
return VERDICT_UNAVAILABLE;
};
match codec.verify(&token, &binding) {
Verdict::Ok(_) => VERDICT_OK,
Verdict::Expired(_) => VERDICT_EXPIRED,
Verdict::UnknownKey => VERDICT_UNKNOWN_KEY,
Verdict::AuthFailed => VERDICT_AUTH_FAILED,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::sync::{Arc as StdArc, Mutex};
thread_local! {
pub(super) static ENV_OVERRIDE: RefCell<Option<HashMap<String, String>>> =
const { RefCell::new(None) };
}
static ENV_LOCK: Mutex<()> = Mutex::new(());
const KEY_A: [u8; KEY_LEN] = [0x11; KEY_LEN];
const KEY_B: [u8; KEY_LEN] = [0x22; KEY_LEN];
fn with_env<T>(pairs: &[(&str, &str)], f: impl FnOnce() -> T) -> T {
struct Restore;
impl Drop for Restore {
fn drop(&mut self) {
ENV_OVERRIDE.with(|o| *o.borrow_mut() = None);
}
}
let map: HashMap<String, String> = pairs
.iter()
.map(|(k, v)| ((*k).to_string(), (*v).to_string()))
.collect();
ENV_OVERRIDE.with(|o| *o.borrow_mut() = Some(map));
let _restore = Restore;
f()
}
fn b64(key: &[u8]) -> String {
URL_SAFE_NO_PAD.encode(key)
}
fn hex(key: &[u8]) -> String {
use std::fmt::Write as _;
key.iter().fold(String::new(), |mut out, b| {
let _ = write!(out, "{b:02x}");
out
})
}
#[derive(Clone, Default)]
struct WarnCounter {
warns: StdArc<Mutex<Vec<String>>>,
}
struct MessageVisitor<'a>(&'a mut String);
impl tracing::field::Visit for MessageVisitor<'_> {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
*self.0 = format!("{value:?}");
}
}
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
if field.name() == "message" {
*self.0 = value.to_string();
}
}
}
impl tracing::Subscriber for WarnCounter {
fn enabled(&self, _metadata: &tracing::Metadata<'_>) -> bool {
true
}
fn new_span(&self, _span: &tracing::span::Attributes<'_>) -> tracing::span::Id {
tracing::span::Id::from_u64(1)
}
fn record(&self, _span: &tracing::span::Id, _values: &tracing::span::Record<'_>) {}
fn record_follows_from(&self, _span: &tracing::span::Id, _follows: &tracing::span::Id) {}
fn event(&self, event: &tracing::Event<'_>) {
if *event.metadata().level() != tracing::Level::WARN {
return;
}
let mut message = String::new();
event.record(&mut MessageVisitor(&mut message));
if let Ok(mut warns) = self.warns.lock() {
warns.push(message);
}
}
fn enter(&self, _span: &tracing::span::Id) {}
fn exit(&self, _span: &tracing::span::Id) {}
}
fn capture_warns<T>(f: impl FnOnce() -> T) -> (T, Vec<String>) {
let _warm_up = with_env(&[], RequestStateCodec::from_env);
let counter = WarnCounter::default();
let sink = counter.warns.clone();
let out = tracing::subscriber::with_default(counter, || {
tracing::callsite::rebuild_interest_cache();
f()
});
let warns = sink.lock().map(|w| w.clone()).unwrap_or_default();
(out, warns)
}
fn v2_versions() -> Vec<crate::types::ProtocolVersion> {
vec![
crate::types::ProtocolVersion("2026-07-28".to_string()),
crate::types::ProtocolVersion("2025-11-25".to_string()),
]
}
#[test]
fn from_env_with_valid_base64url_key_succeeds_and_key_id_is_deterministic() {
let codec = with_env(&[(ENV_REQUEST_STATE_KEY, &b64(&KEY_A))], || {
RequestStateCodec::from_env().expect("a valid 32-byte key must be accepted")
});
assert_eq!(codec.minting_key_id(), key_id_of(&KEY_A));
let again = with_env(&[(ENV_REQUEST_STATE_KEY, &b64(&KEY_A))], || {
RequestStateCodec::from_env().expect("a valid 32-byte key must be accepted")
});
assert_eq!(
codec.minting_key_id(),
again.minting_key_id(),
"key-id must be deterministic for a given key"
);
}
#[test]
fn from_env_accepts_a_hex_encoded_key() {
let codec = with_env(&[(ENV_REQUEST_STATE_KEY, &hex(&KEY_A))], || {
RequestStateCodec::from_env().expect("a hex 32-byte key must be accepted")
});
assert_eq!(codec.minting_key_id(), key_id_of(&KEY_A));
}
#[test]
fn from_env_unset_generates_a_key_and_warns_exactly_once() {
let (codec, warns) = capture_warns(|| with_env(&[], RequestStateCodec::from_env));
assert!(codec.is_ok(), "an unset key must NOT fail the build (D-04)");
assert_eq!(
warns.len(),
1,
"exactly one WARN must be emitted, got {warns:?}"
);
assert!(
warns[0].contains(ENV_REQUEST_STATE_KEY),
"the WARN must name the env var: {}",
warns[0]
);
}
#[test]
fn from_env_with_malformed_key_errors_naming_the_expected_length() {
let err = with_env(&[(ENV_REQUEST_STATE_KEY, "not-a-valid-key")], || {
RequestStateCodec::from_env()
.expect_err("a malformed CONFIGURED key must fail, not silently fall back")
});
let rendered = err.to_string();
assert!(
rendered.contains("32"),
"the error must name the expected byte length: {rendered}"
);
assert!(
rendered.contains(ENV_REQUEST_STATE_KEY),
"the error must name the offending variable: {rendered}"
);
}
#[test]
fn from_env_fallback_keys_are_distinct_across_calls() {
let (a, b) = with_env(&[], || {
(
RequestStateCodec::from_env().expect("fallback key"),
RequestStateCodec::from_env().expect("fallback key"),
)
});
assert_ne!(
a.minting_key_id(),
b.minting_key_id(),
"two CSPRNG fallback draws must not collide"
);
}
#[test]
fn from_env_reads_the_real_process_environment() {
let _guard = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let previous = std::env::var(ENV_REQUEST_STATE_KEY).ok();
std::env::set_var(ENV_REQUEST_STATE_KEY, b64(&KEY_B));
let resolved = RequestStateCodec::from_env();
match previous {
Some(value) => std::env::set_var(ENV_REQUEST_STATE_KEY, value),
None => std::env::remove_var(ENV_REQUEST_STATE_KEY),
}
assert_eq!(
resolved
.expect("valid key in the real env")
.minting_key_id(),
key_id_of(&KEY_B),
"production must read std::env::var, not only the test seam"
);
}
#[test]
fn previous_key_is_accepted_for_verification_but_never_for_minting() {
let codec = with_env(
&[
(ENV_REQUEST_STATE_KEY, &b64(&KEY_A)),
(ENV_REQUEST_STATE_KEY_PREVIOUS, &b64(&KEY_B)),
],
|| RequestStateCodec::from_env().expect("both keys valid"),
);
assert_eq!(codec.minting_key_id(), key_id_of(&KEY_A));
let accepting = codec.accepting_key_ids();
assert!(accepting.contains(&key_id_of(&KEY_A)));
assert!(accepting.contains(&key_id_of(&KEY_B)));
assert_ne!(
codec.minting_key_id(),
key_id_of(&KEY_B),
"the previous key must never become the minting key"
);
}
#[test]
fn ttl_env_overrides_the_default() {
let codec = with_env(
&[
(ENV_REQUEST_STATE_KEY, &b64(&KEY_A)),
(ENV_REQUEST_STATE_TTL_SECS, "42"),
],
|| RequestStateCodec::from_env().expect("valid key"),
);
assert_eq!(codec.ttl(), Duration::from_secs(42));
}
#[test]
fn ttl_env_unparseable_falls_back_to_default_without_erroring() {
let codec = with_env(
&[
(ENV_REQUEST_STATE_KEY, &b64(&KEY_A)),
(ENV_REQUEST_STATE_TTL_SECS, "five minutes"),
],
|| RequestStateCodec::from_env().expect("an unparseable ttl must not error"),
);
assert_eq!(codec.ttl(), Duration::from_secs(DEFAULT_TTL_SECS));
}
#[test]
fn key_id_is_the_first_eight_bytes_of_sha256() {
let mut hasher = Sha256::new();
hasher.update(KEY_A);
let digest = hasher.finalize();
assert_eq!(key_id_of(&KEY_A).as_bytes()[..], digest[..KEY_ID_LEN]);
}
#[test]
fn debug_never_renders_key_material() {
let codec = RequestStateCodec::new(&KEY_A, Duration::from_secs(45)).expect("valid key");
let rendered = format!("{codec:?}");
assert!(rendered.contains("ttl"));
assert!(rendered.contains(&key_id_of(&KEY_A).to_string()));
assert!(
!rendered.contains(&hex(&KEY_A)),
"key material must never appear in Debug output: {rendered}"
);
}
#[test]
fn fixed_clock_makes_now_deterministic() {
let codec = RequestStateCodec::new(&KEY_A, Duration::from_secs(45))
.expect("valid key")
.with_clock(Arc::new(FixedClock(1_700_000_000)));
assert_eq!(codec.now_unix(), 1_700_000_000);
assert_eq!(codec.now_unix(), 1_700_000_000);
}
#[test]
fn server_builder_malformed_request_state_key_fails_the_build() {
let result = with_env(&[(ENV_REQUEST_STATE_KEY, "bogus")], || {
crate::server::Server::builder()
.name("t")
.version("1")
.with_supported_protocol_versions(v2_versions())
.build()
});
assert!(
result.is_err(),
"a malformed CONFIGURED key must fail the server build (T-113-17)"
);
}
#[test]
fn server_builder_unset_key_warns_once_at_startup() {
let (server, warns) = capture_warns(|| {
with_env(&[], || {
crate::server::Server::builder()
.name("t")
.version("1")
.with_supported_protocol_versions(v2_versions())
.build()
})
});
assert!(server.is_ok(), "an unset key must still serve (D-04)");
assert_eq!(
warns.len(),
1,
"the D-04 warning must be emitted exactly once, at BUILD time: {warns:?}"
);
}
#[test]
fn server_builder_with_request_state_key_overrides_env() {
let server = with_env(&[(ENV_REQUEST_STATE_KEY, &b64(&KEY_A))], || {
crate::server::Server::builder()
.name("t")
.version("1")
.with_supported_protocol_versions(v2_versions())
.with_request_state_key(KEY_B)
.build()
.expect("builder key must win")
});
assert_eq!(
server
.request_state_codec()
.expect("v2 server has a codec")
.minting_key_id(),
key_id_of(&KEY_B)
);
}
#[test]
fn server_builder_with_request_state_ttl_overrides_default_and_env() {
let server = with_env(
&[
(ENV_REQUEST_STATE_KEY, &b64(&KEY_A)),
(ENV_REQUEST_STATE_TTL_SECS, "42"),
],
|| {
crate::server::Server::builder()
.name("t")
.version("1")
.with_supported_protocol_versions(v2_versions())
.with_request_state_ttl(Duration::from_secs(7))
.build()
.expect("builder ttl must win")
},
);
assert_eq!(
server.request_state_codec().expect("codec").ttl(),
Duration::from_secs(7)
);
}
#[test]
fn two_servers_with_different_keys_have_different_key_ids() {
let first = crate::server::Server::builder()
.name("a")
.version("1")
.with_supported_protocol_versions(v2_versions())
.with_request_state_key(KEY_A)
.build()
.expect("first server");
let second = crate::server::Server::builder()
.name("b")
.version("1")
.with_supported_protocol_versions(v2_versions())
.with_request_state_key(KEY_B)
.build()
.expect("second server");
assert_ne!(
first.request_state_codec().expect("codec").minting_key_id(),
second
.request_state_codec()
.expect("codec")
.minting_key_id(),
);
}
#[test]
fn v1_only_server_constructs_no_codec_and_reads_no_env() {
let server = with_env(&[(ENV_REQUEST_STATE_KEY, "bogus")], || {
crate::server::Server::builder()
.name("t")
.version("1")
.build()
.expect("a v1-only server must pay nothing for MRTR (D-04)")
});
assert!(server.request_state_codec().is_none());
}
#[test]
fn server_core_builder_carries_the_codec() {
let core = crate::server::builder::ServerCoreBuilder::new()
.name("t")
.version("1")
.with_supported_protocol_versions(v2_versions())
.with_request_state_key(KEY_A)
.build()
.expect("core builds");
assert_eq!(
core.request_state_codec()
.expect("v2 core has a codec")
.minting_key_id(),
key_id_of(&KEY_A)
);
}
const KEY_C: [u8; KEY_LEN] = [0x33; KEY_LEN];
fn codec_at(key: &[u8; KEY_LEN], now: i64, ttl_secs: u64) -> RequestStateCodec {
RequestStateCodec::new(key, Duration::from_secs(ttl_secs))
.expect("valid key")
.with_clock(Arc::new(FixedClock(now)))
}
fn tool_params(path: &str) -> serde_json::Value {
serde_json::json!({ "name": "read_file", "arguments": { "path": path } })
}
fn binding<'a>(
principal: &'a str,
method: &'a str,
params: &serde_json::Value,
) -> RequestBinding<'a> {
RequestBinding::from_request(principal, method, params)
.expect("shallow fixture params bind")
}
#[test]
fn mint_then_verify_round_trips_the_continuation_state() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let state = serde_json::json!({ "collected": { "path": "/safe" }, "step": 2 });
let token = codec.mint(&state, &bind, 1, None).expect("mint");
match codec.verify(&token, &bind) {
Verdict::Ok(continuation) => {
assert_eq!(continuation.state, state);
assert_eq!(continuation.round, 1);
assert_eq!(continuation.exp, 1_300);
},
other => panic!("expected Ok, got {other:?}"),
}
}
fn kinds_map(count: usize, key_len: usize) -> crate::types::mrtr::InputRequestKinds {
use crate::types::mrtr::InputRequestKind;
let cycle = [
InputRequestKind::Elicitation,
InputRequestKind::Sampling,
InputRequestKind::Roots,
];
(0..count)
.map(|index| {
let key = format!("{index:0>width$}", width = key_len);
(key, cycle[index % cycle.len()])
})
.collect()
}
#[test]
fn mint_then_verify_round_trips_the_requested_kinds() {
use crate::types::mrtr::InputRequestKind;
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let kinds: crate::types::mrtr::InputRequestKinds = [
("user_name".to_string(), InputRequestKind::Elicitation),
("model_says".to_string(), InputRequestKind::Sampling),
("workspace".to_string(), InputRequestKind::Roots),
]
.into_iter()
.collect();
let token = codec
.mint(
&serde_json::json!({ "step": 1 }),
&bind,
0,
Some(kinds.clone()),
)
.expect("mint");
match codec.verify(&token, &bind) {
Verdict::Ok(continuation) => assert_eq!(continuation.kinds, Some(kinds)),
other => panic!("expected Ok, got {other:?}"),
}
}
#[test]
fn a_continuation_serialized_without_kinds_still_deserializes_as_none() {
let legacy = serde_json::json!({ "state": { "step": 1 }, "exp": 1_300, "round": 2 });
let continuation: Continuation =
serde_json::from_value(legacy).expect("a pre-kinds continuation must still decode");
assert_eq!(continuation.round, 2);
assert_eq!(
continuation.kinds, None,
"an ABSENT kinds field must be None — the pre-kinds marker — and must never \
be conflated with an empty map, which means \"this build asked for nothing\""
);
}
#[test]
fn an_empty_kinds_map_survives_as_some_not_none() {
let continuation = Continuation {
state: serde_json::json!({}),
exp: 1_300,
round: 0,
kinds: Some(crate::types::mrtr::InputRequestKinds::new()),
};
let bytes = serde_json::to_vec(&continuation).expect("serializable");
let decoded: Continuation = serde_json::from_slice(&bytes).expect("deserializable");
assert_eq!(
decoded.kinds,
Some(crate::types::mrtr::InputRequestKinds::new())
);
assert_ne!(decoded.kinds, None);
}
#[test]
fn a_full_width_kinds_map_stays_within_the_accepted_token_bound() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let state = serde_json::json!({ "collected": {}, "step": 1 });
let bare = codec.mint(&state, &bind, 0, None).expect("mint");
let full = codec
.mint(
&state,
&bind,
0,
Some(kinds_map(
crate::types::mrtr::MAX_INPUT_RESPONSES,
"user_name_00".len(),
)),
)
.expect("a 64-entry kinds map must still mint");
println!(
"D-113-O minted token bytes: bare = {}, with {} kinds entries = {} (bound {})",
bare.len(),
crate::types::mrtr::MAX_INPUT_RESPONSES,
full.len(),
crate::types::mrtr::MAX_REQUEST_STATE_LEN
);
assert!(
full.len() <= crate::types::mrtr::MAX_REQUEST_STATE_LEN,
"a token carrying the widest kinds map ingress can ever be answered with \
must not exceed the bound ingress applies: {} > {}",
full.len(),
crate::types::mrtr::MAX_REQUEST_STATE_LEN
);
}
#[test]
fn an_absurd_kinds_map_is_refused_at_the_mint_rather_than_minted() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
assert!(
codec
.mint(&serde_json::json!({}), &bind, 0, Some(kinds_map(512, 64)),)
.is_err(),
"a kinds map that would burst MAX_REQUEST_STATE_LEN must fail the mint"
);
}
#[test]
fn two_mints_of_identical_input_produce_different_tokens() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let state = serde_json::json!({ "a": 1 });
let first = codec.mint(&state, &bind, 0, None).expect("mint");
let second = codec.mint(&state, &bind, 0, None).expect("mint");
assert_ne!(first, second, "a fresh nonce must be drawn per mint");
}
#[test]
fn token_layout_is_key_id_len_then_key_id_then_nonce() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let token = codec
.mint(&serde_json::json!({}), &bind, 0, None)
.expect("mint");
let raw = URL_SAFE_NO_PAD.decode(token.as_bytes()).expect("base64url");
assert_eq!(raw[0], KEY_ID_LEN_U8, "leading length byte");
assert_eq!(
&raw[1..=KEY_ID_LEN],
key_id_of(&KEY_A).as_bytes(),
"cleartext key-id prefix"
);
assert!(
raw.len() > 1 + KEY_ID_LEN + NONCE_LEN,
"a nonce plus a non-empty sealed body must follow"
);
}
#[test]
fn flipping_a_ciphertext_byte_yields_auth_failed() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let token = codec
.mint(&serde_json::json!({ "a": 1 }), &bind, 0, None)
.expect("mint");
let mut raw = URL_SAFE_NO_PAD.decode(token.as_bytes()).expect("base64url");
let last = raw.len() - 1;
raw[last] ^= 0xff;
let mutated = URL_SAFE_NO_PAD.encode(&raw);
assert_eq!(codec.verify(&mutated, &bind), Verdict::AuthFailed);
}
#[test]
fn sep_2322_reject_tampered_state_suffix_mutation_yields_auth_failed() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let token = codec
.mint(&serde_json::json!({ "a": 1 }), &bind, 0, None)
.expect("mint");
let tampered = format!("{token}-TAMPERED");
assert_eq!(codec.verify(&tampered, &bind), Verdict::AuthFailed);
}
#[test]
fn a_token_minted_for_another_principal_yields_auth_failed() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let alice = binding("alice", "tools/call", ¶ms);
let bob = binding("bob", "tools/call", ¶ms);
let token = codec
.mint(&serde_json::json!({ "a": 1 }), &alice, 0, None)
.expect("mint");
assert_eq!(codec.verify(&token, &bob), Verdict::AuthFailed);
}
#[test]
fn replaying_a_token_onto_different_arguments_yields_auth_failed() {
let codec = codec_at(&KEY_A, 1_000, 300);
let safe = tool_params("/safe");
let shadow = tool_params("/etc/shadow");
let minted_for = binding("alice", "tools/call", &safe);
let replayed_onto = binding("alice", "tools/call", &shadow);
let token = codec
.mint(&serde_json::json!({ "a": 1 }), &minted_for, 0, None)
.expect("mint");
assert_eq!(codec.verify(&token, &replayed_onto), Verdict::AuthFailed);
}
#[test]
fn replaying_a_token_onto_a_different_method_yields_auth_failed() {
let codec = codec_at(&KEY_A, 1_000, 300);
let call = serde_json::json!({ "name": "x", "arguments": {} });
let prompt = serde_json::json!({ "name": "x", "arguments": {} });
let minted_for = binding("alice", "tools/call", &call);
let replayed_onto = binding("alice", "prompts/get", &prompt);
let token = codec
.mint(&serde_json::json!({ "a": 1 }), &minted_for, 0, None)
.expect("mint");
assert_eq!(codec.verify(&token, &replayed_onto), Verdict::AuthFailed);
}
#[test]
fn an_expired_token_yields_expired_carrying_a_readable_continuation() {
let minter = codec_at(&KEY_A, 1_000, 60);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let state = serde_json::json!({ "collected": { "path": "/safe" } });
let token = minter.mint(&state, &bind, 3, None).expect("mint");
let verifier = codec_at(&KEY_A, 5_000, 60);
match verifier.verify(&token, &bind) {
Verdict::Expired(continuation) => {
assert_eq!(continuation.state, state, "state must be READABLE");
assert_eq!(
continuation.round, 3,
"round must survive so D-09 is not reset"
);
},
other => panic!("expected Expired, got {other:?}"),
}
}
#[test]
fn a_token_from_an_unknown_key_id_yields_unknown_key() {
let minter = codec_at(&KEY_A, 1_000, 300);
let verifier = codec_at(&KEY_B, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let token = minter
.mint(&serde_json::json!({ "a": 1 }), &bind, 0, None)
.expect("mint");
assert_eq!(
verifier.verify(&token, &bind),
Verdict::UnknownKey,
"an unshared key must be DISTINGUISHABLE from tampering (D-04)"
);
}
#[test]
fn a_token_minted_under_the_previous_key_still_verifies() {
let old = codec_at(&KEY_B, 1_000, 300);
let rotated = codec_at(&KEY_A, 1_000, 300)
.with_previous_keys([KEY_B])
.expect("previous key");
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let token = old
.mint(&serde_json::json!({ "a": 1 }), &bind, 0, None)
.expect("mint");
assert!(matches!(rotated.verify(&token, &bind), Verdict::Ok(_)));
}
#[test]
fn colliding_key_ids_resolve_to_ok_or_auth_failed_never_unknown_key() {
let forced = key_id_of(b"a deliberately forced key id");
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let verifier = codec_at(&KEY_A, 1_000, 300)
.with_forced_minting_key_id(forced)
.with_forced_accepting_key(forced, &KEY_B)
.expect("second entry");
let minter_a = codec_at(&KEY_A, 1_000, 300).with_forced_minting_key_id(forced);
let token_a = minter_a
.mint(&serde_json::json!({ "a": 1 }), &bind, 0, None)
.expect("mint");
assert!(matches!(verifier.verify(&token_a, &bind), Verdict::Ok(_)));
let minter_b = codec_at(&KEY_B, 1_000, 300).with_forced_minting_key_id(forced);
let token_b = minter_b
.mint(&serde_json::json!({ "b": 2 }), &bind, 0, None)
.expect("mint");
assert!(matches!(verifier.verify(&token_b, &bind), Verdict::Ok(_)));
let minter_c = codec_at(&KEY_C, 1_000, 300).with_forced_minting_key_id(forced);
let token_c = minter_c
.mint(&serde_json::json!({ "c": 3 }), &bind, 0, None)
.expect("mint");
assert_eq!(verifier.verify(&token_c, &bind), Verdict::AuthFailed);
}
#[test]
fn malformed_tokens_yield_auth_failed_and_never_panic() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let oversized = "A".repeat(crate::types::mrtr::MAX_REQUEST_STATE_LEN + 1);
let too_short = URL_SAFE_NO_PAD.encode([KEY_ID_LEN_U8, 1, 2, 3]);
for candidate in [
"",
"!!!not base64!!!",
"AAAA",
oversized.as_str(),
too_short.as_str(),
] {
assert_eq!(
codec.verify(candidate, &bind),
Verdict::AuthFailed,
"malformed input {candidate:?} must be a verdict, not a panic"
);
}
}
#[test]
fn a_minted_token_fits_inside_the_accepted_bound() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let token = codec
.mint(&serde_json::json!({ "a": "x".repeat(64) }), &bind, 0, None)
.expect("mint");
assert!(token.len() <= crate::types::mrtr::MAX_REQUEST_STATE_LEN);
}
#[test]
fn minting_an_oversized_state_errors_rather_than_producing_a_self_rejecting_token() {
let codec = codec_at(&KEY_A, 1_000, 300);
let params = tool_params("/safe");
let bind = binding("alice", "tools/call", ¶ms);
let huge =
serde_json::json!({ "blob": "x".repeat(crate::types::mrtr::MAX_REQUEST_STATE_LEN) });
assert!(
codec.mint(&huge, &bind, 0, None).is_err(),
"a token the server would itself reject must never be minted"
);
}
#[test]
fn secret_key_zeroize_replaces_the_key_bytes_with_zeroes() {
let secret = SecretKey::new(KEY_A);
let before: [u8; KEY_LEN] = *secret;
assert_eq!(before, KEY_A, "the wrapper must not alter the key it holds");
let mut scrubbed: [u8; KEY_LEN] = *secret;
scrubbed.zeroize();
assert_eq!(scrubbed, [0u8; KEY_LEN]);
assert_ne!(
scrubbed, before,
"a fixture key of all zeroes would make this test vacuous"
);
}
#[test]
fn resolve_codec_at_build_returns_none_for_a_v1_only_accept_list() {
let resolved = with_env(&[(ENV_REQUEST_STATE_KEY, "bogus")], || {
resolve_codec_at_build(
&crate::types::protocol::context::default_accept_list(),
None,
&[],
None,
)
})
.expect("a v1-only accept-list reads no env var");
assert!(resolved.is_none());
}
#[test]
fn resolve_codec_at_build_prefers_the_by_reference_key_over_the_environment() {
let key = SecretKey::new(KEY_B);
let codec = with_env(&[(ENV_REQUEST_STATE_KEY, &b64(&KEY_A))], || {
resolve_codec_at_build(&v2_versions(), Some(&key), &[], None)
})
.expect("resolves")
.expect("a v2 accept-list has a codec");
assert_eq!(
codec.minting_key_id(),
key_id_of(&KEY_B),
"a builder-supplied key must beat PMCP_REQUEST_STATE_KEY"
);
assert_eq!(
*key, KEY_B,
"taking the key by reference must not consume or alter the caller's copy"
);
}
#[test]
fn resolve_codec_at_build_appends_previous_keys_to_the_env_derived_set() {
let previous = [SecretKey::new(KEY_C)];
let codec = with_env(
&[
(ENV_REQUEST_STATE_KEY, &b64(&KEY_A)),
(ENV_REQUEST_STATE_KEY_PREVIOUS, &b64(&KEY_B)),
],
|| resolve_codec_at_build(&v2_versions(), None, &previous, None),
)
.expect("resolves")
.expect("codec");
let accepting = codec.accepting_key_ids();
assert_eq!(codec.minting_key_id(), key_id_of(&KEY_A));
assert!(
accepting.contains(&key_id_of(&KEY_B)),
"the env-derived rotated-out key must survive: {accepting:?}"
);
assert!(
accepting.contains(&key_id_of(&KEY_C)),
"builder previous keys are APPENDED, not substituted: {accepting:?}"
);
}
#[test]
fn resolve_codec_at_build_honours_env_previous_key_even_with_an_explicit_key() {
let key = SecretKey::new(KEY_A);
let codec = with_env(&[(ENV_REQUEST_STATE_KEY_PREVIOUS, &b64(&KEY_B))], || {
resolve_codec_at_build(&v2_versions(), Some(&key), &[], None)
})
.expect("resolves")
.expect("codec");
assert!(
codec.accepting_key_ids().contains(&key_id_of(&KEY_B)),
"a builder key overrides PMCP_REQUEST_STATE_KEY but NOT the separate \
rotation setting"
);
}
#[test]
fn resolve_codec_at_build_fails_the_build_on_a_malformed_env_key() {
let result = with_env(&[(ENV_REQUEST_STATE_KEY, "bogus")], || {
resolve_codec_at_build(&v2_versions(), None, &[], None)
});
assert!(
result.is_err(),
"a malformed CONFIGURED key must fail the BUILD (T-113-17)"
);
}
#[test]
fn resolve_codec_at_build_prefers_an_explicit_ttl_over_the_environment() {
let codec = with_env(&[(ENV_REQUEST_STATE_TTL_SECS, "42")], || {
resolve_codec_at_build(
&v2_versions(),
Some(&SecretKey::new(KEY_A)),
&[],
Some(Duration::from_secs(7)),
)
})
.expect("resolves")
.expect("codec");
assert_eq!(codec.ttl(), Duration::from_secs(7));
}
#[test]
fn server_core_builder_previous_keys_reach_the_accepting_set() {
let core = crate::server::builder::ServerCoreBuilder::new()
.name("t")
.version("1")
.with_supported_protocol_versions(v2_versions())
.with_request_state_key(KEY_A)
.with_request_state_previous_keys(vec![KEY_B])
.build()
.expect("core builds");
let accepting = core
.request_state_codec()
.expect("codec")
.accepting_key_ids();
assert!(accepting.contains(&key_id_of(&KEY_A)));
assert!(accepting.contains(&key_id_of(&KEY_B)));
}
#[cfg(feature = "fuzzing")]
#[test]
fn fuzz_support_seam_rejects_garbage() {
assert_eq!(
super::fuzz_support::verify_bytes(b"garbage"),
super::fuzz_support::VERDICT_AUTH_FAILED
);
assert_ne!(
super::fuzz_support::verify_bytes(&[0xff, 0xfe, 0xfd]),
super::fuzz_support::VERDICT_OK
);
}
fn arb_state() -> impl proptest::strategy::Strategy<Value = serde_json::Value> {
use proptest::prelude::*;
prop_oneof![
Just(serde_json::Value::Null),
any::<bool>().prop_map(serde_json::Value::from),
any::<i32>().prop_map(serde_json::Value::from),
"[ -~]{0,64}".prop_map(serde_json::Value::from),
proptest::collection::vec("[ -~]{0,16}", 0..6).prop_map(|v| serde_json::json!(v)),
proptest::collection::hash_map("[a-z]{1,8}", "[ -~]{0,16}", 0..6)
.prop_map(|m| serde_json::json!(m)),
]
}
proptest::proptest! {
#[test]
fn property_request_state_roundtrip(
state in arb_state(),
principal in "[ -~]{0,48}",
method in "[a-z/]{1,24}",
ttl_secs in 1u64..3600,
round in 0u8..=255,
) {
let codec = RequestStateCodec::new(&KEY_A, Duration::from_secs(ttl_secs))
.expect("valid key")
.with_clock(Arc::new(FixedClock(1_000)));
let params = serde_json::json!({ "name": "n", "arguments": { "k": "v" } });
let bind = RequestBinding::from_request(&principal, &method, ¶ms)
.expect("a two-level fixture is far inside the canonical depth cap");
let token = codec.mint(&state, &bind, round, None).expect("mint");
match codec.verify(&token, &bind) {
Verdict::Ok(continuation) => {
proptest::prop_assert_eq!(continuation.state, state);
proptest::prop_assert_eq!(continuation.round, round);
},
other => proptest::prop_assert!(false, "expected Ok, got {:?}", other),
}
}
#[test]
fn property_request_state_binding_is_total(
principal_a in "[ -~]{0,24}",
principal_b in "[ -~]{0,24}",
method_a in "[a-z/]{1,16}",
method_b in "[a-z/]{1,16}",
arg_a in "[ -~]{0,24}",
arg_b in "[ -~]{0,24}",
) {
let params_a = serde_json::json!({ "name": "n", "arguments": { "k": arg_a } });
let params_b = serde_json::json!({ "name": "n", "arguments": { "k": arg_b } });
let bind_a = RequestBinding::from_request(&principal_a, &method_a, ¶ms_a)
.expect("a two-level fixture is far inside the canonical depth cap");
let bind_b = RequestBinding::from_request(&principal_b, &method_b, ¶ms_b)
.expect("a two-level fixture is far inside the canonical depth cap");
proptest::prop_assume!(
principal_a != principal_b || method_a != method_b || arg_a != arg_b
);
let codec = RequestStateCodec::new(&KEY_A, Duration::from_secs(DEFAULT_TTL_SECS))
.expect("valid key")
.with_clock(Arc::new(FixedClock(1_000)));
let token = codec.mint(&serde_json::json!({ "s": 1 }), &bind_a, 0, None).expect("mint");
proptest::prop_assert_eq!(codec.verify(&token, &bind_b), Verdict::AuthFailed);
}
#[test]
fn property_request_state_never_panics(token in ".{0,512}") {
let codec = RequestStateCodec::new(&KEY_A, Duration::from_secs(DEFAULT_TTL_SECS))
.expect("valid key")
.with_clock(Arc::new(FixedClock(1_000)));
let params = serde_json::json!({ "name": "n", "arguments": {} });
let bind = RequestBinding::from_request("alice", "tools/call", ¶ms)
.expect("a two-level fixture is far inside the canonical depth cap");
proptest::prop_assert!(!matches!(codec.verify(&token, &bind), Verdict::Ok(_)));
}
}
}