Skip to main content

agent_protocols/
identity.rs

1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
2use base64::Engine as _;
3use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
4use serde::de::Error as DeError;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::Value;
7use sha3::{Digest, Sha3_256};
8use std::collections::{BTreeMap, HashMap};
9use std::fmt;
10use std::str::FromStr;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use crate::error::{Result, SdkError};
14
15pub const AGENT_ID_PREFIX: &str = "did:agent:";
16pub const DEFAULT_LIVE_WRITE_WINDOW_MS: i64 = 300_000;
17/// Nonce cache validity (Agent Identity Section 6.2): at least twice the
18/// live-write window, because an envelope signed up to one window ahead of the
19/// receiver's clock stays inside the window for two windows after acceptance.
20pub const DEFAULT_NONCE_TTL_MS: i64 = 2 * DEFAULT_LIVE_WRITE_WINDOW_MS;
21pub const DEFAULT_REQUEST_JWT_TTL_SECS: i64 = 300;
22pub const MAX_NONCE_HEADER: &str = "Max-Seen-Nonce";
23pub const MAX_SAFE_NONCE: u64 = 0x1FFFFFFFFFFFFF;
24/// Largest jump a single `Max-Seen-Nonce` header may cause beyond
25/// `max(next_nonce, now)` (Agent Identity Section 6.2). The nonce sequence is
26/// shared by every service an agent uses, so one hostile service must not be
27/// able to exhaust it.
28pub const MAX_NONCE_JUMP: u64 = 1 << 32;
29
30/// The six Agent Identity event fields; protocols add their own on top.
31pub const IDENTITY_EVENT_FIELDS: [&str; 6] = [
32    "protocol",
33    "type",
34    "actor",
35    "created_at",
36    "nonce",
37    "payload",
38];
39
40/// Error codes shared by every Agent Protocols service (Section 8.1).
41pub const SHARED_ERROR_CODES: [&str; 12] = [
42    "invalid_request",
43    "invalid_event",
44    "invalid_event_hash",
45    "invalid_signature",
46    "invalid_actor",
47    "timestamp_out_of_window",
48    "nonce_not_greater",
49    "invalid_token",
50    "permission_denied",
51    "not_found",
52    "rate_limited",
53    "payload_too_large",
54];
55
56#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
57pub struct AgentId(String);
58
59impl AgentId {
60    pub fn from_public_key(public_key: &[u8; 32]) -> Self {
61        Self(format!(
62            "{}{}",
63            AGENT_ID_PREFIX,
64            URL_SAFE_NO_PAD.encode(public_key)
65        ))
66    }
67
68    pub fn as_str(&self) -> &str {
69        &self.0
70    }
71
72    pub fn public_key_bytes(&self) -> Result<[u8; 32]> {
73        let encoded = self
74            .0
75            .strip_prefix(AGENT_ID_PREFIX)
76            .ok_or(SdkError::InvalidAgentIdPrefix)?;
77        // URL_SAFE_NO_PAD rejects padding characters and non-zero trailing
78        // bits, so non-canonical encodings of the same key are rejected.
79        let bytes = URL_SAFE_NO_PAD.decode(encoded)?;
80        bytes
81            .try_into()
82            .map_err(|bytes: Vec<u8>| SdkError::InvalidPublicKeyLength(bytes.len()))
83    }
84
85    pub fn verifying_key(&self) -> Result<VerifyingKey> {
86        Ok(VerifyingKey::from_bytes(&self.public_key_bytes()?)?)
87    }
88}
89
90impl fmt::Debug for AgentId {
91    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92        formatter.debug_tuple("AgentId").field(&self.0).finish()
93    }
94}
95
96impl fmt::Display for AgentId {
97    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98        formatter.write_str(&self.0)
99    }
100}
101
102impl FromStr for AgentId {
103    type Err = SdkError;
104
105    fn from_str(value: &str) -> Result<Self> {
106        if !value.starts_with(AGENT_ID_PREFIX) {
107            return Err(SdkError::InvalidAgentIdPrefix);
108        }
109        let agent_id = Self(value.to_owned());
110        agent_id.public_key_bytes()?;
111        Ok(agent_id)
112    }
113}
114
115impl TryFrom<String> for AgentId {
116    type Error = SdkError;
117
118    fn try_from(value: String) -> Result<Self> {
119        Self::from_str(&value)
120    }
121}
122
123impl Serialize for AgentId {
124    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
125    where
126        S: Serializer,
127    {
128        serializer.serialize_str(&self.0)
129    }
130}
131
132impl<'de> Deserialize<'de> for AgentId {
133    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
134    where
135        D: Deserializer<'de>,
136    {
137        let value = String::deserialize(deserializer)?;
138        Self::from_str(&value).map_err(D::Error::custom)
139    }
140}
141
142pub struct AgentSigner {
143    signing_key: SigningKey,
144}
145
146impl AgentSigner {
147    pub fn generate() -> Self {
148        let mut seed = [0_u8; 32];
149        rand::fill(&mut seed);
150        Self::from_seed(seed)
151    }
152
153    pub fn from_seed(seed: [u8; 32]) -> Self {
154        Self {
155            signing_key: SigningKey::from_bytes(&seed),
156        }
157    }
158
159    pub fn agent_id(&self) -> AgentId {
160        AgentId::from_public_key(&self.signing_key.verifying_key().to_bytes())
161    }
162
163    pub fn verifying_key(&self) -> VerifyingKey {
164        self.signing_key.verifying_key()
165    }
166
167    pub fn sign_event<P>(&self, event: Event<P>) -> Result<Envelope<P>>
168    where
169        P: Serialize,
170    {
171        let event_hash = event_hash_bytes(&event)?;
172        let hash = URL_SAFE_NO_PAD.encode(event_hash);
173        let signature = sign_event_hash(&self.signing_key, &event_hash)
174            .expect("event hashes are always 32 bytes");
175        Ok(Envelope {
176            hash,
177            event,
178            signature,
179        })
180    }
181
182    pub fn sign_request_jwt(&self, claims: &RequestJwtClaims) -> Result<String> {
183        let agent_id = self.agent_id();
184        if claims.iss != agent_id || claims.sub != agent_id {
185            return Err(SdkError::InvalidJwtClaim("iss/sub"));
186        }
187
188        let header = RequestJwtHeader {
189            alg: "EdDSA".to_owned(),
190            typ: "JWT".to_owned(),
191            kid: agent_id,
192        };
193        let header =
194            URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).expect("JWT header is JSON-safe"));
195        let payload =
196            URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims).expect("JWT claims are JSON-safe"));
197        let signing_input = format!("{header}.{payload}");
198        let signature = self.signing_key.sign(signing_input.as_bytes());
199        Ok(format!(
200            "{signing_input}.{}",
201            URL_SAFE_NO_PAD.encode(signature.to_bytes())
202        ))
203    }
204
205    /// Signs an Agent Delegation controller registration challenge (Section
206    /// 4.3) over its exact UTF-8 bytes. Any other string is refused, so the
207    /// signer never becomes an arbitrary-message signing oracle.
208    pub fn sign_controller_challenge(&self, challenge: &str) -> Result<String> {
209        validate_controller_challenge(challenge)?;
210        let signature = self.signing_key.sign(challenge.as_bytes());
211        Ok(URL_SAFE_NO_PAD.encode(signature.to_bytes()))
212    }
213}
214
215/// Prefix of an Agent Delegation controller registration challenge (Section 4.3).
216pub const CONTROLLER_CHALLENGE_PREFIX: &str = "agent-delegation/1.0:controller-registration:";
217
218/// The prefix followed by 43 opaque base64url characters, 88 in all.
219pub fn is_controller_challenge(value: &str) -> bool {
220    value
221        .strip_prefix(CONTROLLER_CHALLENGE_PREFIX)
222        .is_some_and(|suffix| {
223            suffix.len() == 43
224                && suffix
225                    .bytes()
226                    .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
227        })
228}
229
230fn validate_controller_challenge(value: &str) -> Result<()> {
231    if is_controller_challenge(value) {
232        Ok(())
233    } else {
234        Err(SdkError::protocol(
235            "invalid_request",
236            "not an agent-delegation/1.0 controller registration challenge",
237        ))
238    }
239}
240
241/// Verifies a controller registration proof (Agent Delegation Section 4.3):
242/// `signature` over the challenge's exact UTF-8 bytes under the strict rules of
243/// Section 3.1. Matching the challenge to what the provider issued, and its
244/// expiry, remain the provider's checks.
245pub fn verify_controller_challenge(
246    agent_id: &AgentId,
247    challenge: &str,
248    signature: &str,
249) -> Result<()> {
250    validate_controller_challenge(challenge)?;
251    let signature: [u8; 64] = URL_SAFE_NO_PAD
252        .decode(signature)?
253        .try_into()
254        .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
255    verify_ed25519_strict_bytes(
256        &agent_id.public_key_bytes()?,
257        challenge.as_bytes(),
258        &signature,
259    )
260}
261
262#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
263pub struct Event<P = Value> {
264    pub protocol: String,
265    #[serde(rename = "type")]
266    pub kind: String,
267    pub actor: AgentId,
268    pub created_at: i64,
269    pub nonce: u64,
270    #[serde(skip_serializing_if = "Option::is_none")]
271    pub room_id: Option<String>,
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub base_seq: Option<u64>,
274    #[serde(skip_serializing_if = "Option::is_none")]
275    pub base_hash: Option<String>,
276    /// `None` when the field is absent. An explicit empty list is kept, so a
277    /// deserialized event re-serializes to the bytes that were signed.
278    #[serde(default, skip_serializing_if = "Option::is_none")]
279    pub mentions: Option<Vec<AgentId>>,
280    pub payload: P,
281    /// Unknown top-level fields, kept so the event hashes as signed. Protocol
282    /// validators reject them (the closed event object, Section 5.1).
283    #[serde(default, flatten)]
284    pub extra: BTreeMap<String, Value>,
285}
286
287impl<P> Event<P> {
288    pub fn new(
289        protocol: impl Into<String>,
290        kind: impl Into<String>,
291        actor: AgentId,
292        created_at: i64,
293        nonce: u64,
294        payload: P,
295    ) -> Self {
296        Self {
297            protocol: protocol.into(),
298            kind: kind.into(),
299            actor,
300            created_at,
301            nonce,
302            room_id: None,
303            base_seq: None,
304            base_hash: None,
305            mentions: None,
306            payload,
307            extra: BTreeMap::new(),
308        }
309    }
310
311    pub fn with_room_id(mut self, room_id: impl Into<String>) -> Self {
312        self.room_id = Some(room_id.into());
313        self
314    }
315
316    pub fn with_room_head(mut self, base_seq: u64, base_hash: impl Into<String>) -> Self {
317        self.base_seq = Some(base_seq);
318        self.base_hash = Some(base_hash.into());
319        self
320    }
321
322    pub fn with_mentions(mut self, mentions: Vec<AgentId>) -> Self {
323        self.mentions = Some(mentions);
324        self
325    }
326
327    pub fn with_mention(mut self, agent_id: AgentId) -> Self {
328        self.mentions.get_or_insert_with(Vec::new).push(agent_id);
329        self
330    }
331
332    /// The event's mentions, empty when the field is absent.
333    pub fn mentions(&self) -> &[AgentId] {
334        self.mentions.as_deref().unwrap_or(&[])
335    }
336}
337
338/// Enforces the closed event object (Section 5.1): the event may carry only
339/// the six Agent Identity fields plus `extra_fields` the protocol defines.
340pub fn validate_event_fields<P>(event: &Event<P>, extra_fields: &[&str]) -> Result<()> {
341    let present = [
342        ("room_id", event.room_id.is_some()),
343        ("base_seq", event.base_seq.is_some()),
344        ("base_hash", event.base_hash.is_some()),
345        ("mentions", event.mentions.is_some()),
346    ];
347    for (field, is_present) in present {
348        if is_present && !extra_fields.contains(&field) {
349            return Err(SdkError::protocol(
350                "invalid_event",
351                format!("unknown event field: {field}"),
352            ));
353        }
354    }
355    if let Some(field) = event
356        .extra
357        .keys()
358        .find(|key| !extra_fields.contains(&key.as_str()))
359    {
360        return Err(SdkError::protocol(
361            "invalid_event",
362            format!("unknown event field: {field}"),
363        ));
364    }
365    Ok(())
366}
367
368/// Error response body shared by every Agent Protocols service (Section 8.1).
369#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
370pub struct ErrorResponse {
371    pub error: ErrorBody,
372}
373
374#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
375pub struct ErrorBody {
376    pub code: String,
377    pub message: String,
378    #[serde(default, skip_serializing_if = "Option::is_none")]
379    pub data: Option<Value>,
380}
381
382/// List response shape (Section 8.2). `next_cursor` is present exactly when
383/// more items follow.
384#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
385pub struct ListResponse<T> {
386    pub result: Vec<T>,
387    #[serde(default, skip_serializing_if = "Option::is_none")]
388    pub next_cursor: Option<String>,
389}
390
391/// An accepted envelope with the service's acceptance time (Section 8.3).
392#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
393pub struct AcceptedRecord<P = Value> {
394    pub envelope: Envelope<P>,
395    pub accepted_at: i64,
396}
397
398/// Discovery document skeleton served at `/.well-known/{protocol-name}`
399/// (Section 8.4). Protocols add members, kept in `extra`.
400#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
401pub struct DiscoveryDocument {
402    pub protocol: String,
403    pub service: String,
404    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
405    pub endpoints: BTreeMap<String, String>,
406    #[serde(default, skip_serializing_if = "Vec::is_empty")]
407    pub features: Vec<String>,
408    #[serde(default, flatten)]
409    pub extra: BTreeMap<String, Value>,
410}
411
412#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
413pub struct Envelope<P = Value> {
414    pub hash: String,
415    pub event: Event<P>,
416    pub signature: String,
417}
418
419pub fn canonical_event_bytes<P>(event: &Event<P>) -> Result<Vec<u8>>
420where
421    P: Serialize,
422{
423    serde_jcs::to_vec(event).map_err(|err| SdkError::CanonicalJson(err.to_string()))
424}
425
426pub fn event_hash<P>(event: &Event<P>) -> Result<String>
427where
428    P: Serialize,
429{
430    Ok(URL_SAFE_NO_PAD.encode(event_hash_bytes(event)?))
431}
432
433pub fn event_hash_bytes<P>(event: &Event<P>) -> Result<[u8; 32]>
434where
435    P: Serialize,
436{
437    validate_nonce(event.nonce)?;
438    let digest = Sha3_256::digest(canonical_event_bytes(event)?);
439    let mut bytes = [0_u8; 32];
440    bytes.copy_from_slice(&digest);
441    Ok(bytes)
442}
443
444pub fn sign_event<P>(signing_key: &SigningKey, event: &Event<P>) -> Result<String>
445where
446    P: Serialize,
447{
448    let event_hash = event_hash_bytes(event)?;
449    Ok(sign_event_hash(signing_key, &event_hash).expect("event hashes are always 32 bytes"))
450}
451
452/// Signs a precomputed 32-byte event hash. Signing a digest supplied by
453/// another component without seeing the event it commits to (blind signing) is
454/// NOT RECOMMENDED: `actor` and all event content are inside the digest, so a
455/// blind signer can be tricked into signing arbitrary events attributed to its
456/// key. Prefer [`sign_event`], which canonicalizes and hashes the event.
457pub fn sign_event_hash(signing_key: &SigningKey, event_hash: &[u8]) -> Result<String> {
458    let signature = signing_key.sign(valid_event_hash_bytes(event_hash)?);
459    Ok(URL_SAFE_NO_PAD.encode(signature.to_bytes()))
460}
461
462pub fn verify_event_hash<P>(envelope: &Envelope<P>) -> Result<()>
463where
464    P: Serialize,
465{
466    let expected = event_hash(&envelope.event)?;
467    if expected == envelope.hash {
468        Ok(())
469    } else {
470        Err(SdkError::InvalidEventHash {
471            expected,
472            actual: envelope.hash.clone(),
473        })
474    }
475}
476
477pub fn verify_signature<P>(envelope: &Envelope<P>) -> Result<()>
478where
479    P: Serialize,
480{
481    let event_hash = event_hash_bytes(&envelope.event)?;
482    let verifying_key = envelope.event.actor.verifying_key()?;
483    verify_event_hash_signature(&verifying_key, &event_hash, &envelope.signature)
484}
485
486pub fn verify_event_hash_signature(
487    verifying_key: &VerifyingKey,
488    event_hash: &[u8],
489    encoded_signature: &str,
490) -> Result<()> {
491    let signature_bytes = URL_SAFE_NO_PAD.decode(encoded_signature)?;
492    let signature_bytes: [u8; 64] = signature_bytes
493        .try_into()
494        .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
495    verify_ed25519_strict(
496        verifying_key,
497        valid_event_hash_bytes(event_hash)?,
498        &signature_bytes,
499    )
500}
501
502/// p = 2^255 - 19, little-endian.
503const FIELD_P_LE: [u8; 32] = [
504    0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
505    0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
506];
507/// L = 2^252 + 27742317777372353535851937790883648493, little-endian.
508const GROUP_L_LE: [u8; 32] = [
509    0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
510    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
511];
512/// y-coordinates (sign bit cleared) of every small-order point (Appendix B).
513const SMALL_ORDER_Y: [[u8; 32]; 5] = [
514    [0; 32],
515    [
516        0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
517        0, 0, 0,
518    ],
519    [
520        0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
521        0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
522        0xff, 0x7f,
523    ],
524    [
525        0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98,
526        0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53,
527        0xfc, 0x05,
528    ],
529    [
530        0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67,
531        0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac,
532        0x03, 0x7a,
533    ],
534];
535
536/// Little-endian `a < b`.
537fn less_than_le(a: &[u8; 32], b: &[u8; 32]) -> bool {
538    for i in (0..32).rev() {
539        if a[i] != b[i] {
540            return a[i] < b[i];
541        }
542    }
543    false
544}
545
546/// Canonical encoding (y < p) of a point that is not of small order.
547fn is_strict_point(encoding: &[u8; 32]) -> bool {
548    let mut y = *encoding;
549    y[31] &= 0x7f;
550    less_than_le(&y, &FIELD_P_LE) && !SMALL_ORDER_Y.contains(&y)
551}
552
553/// Ed25519 verification under the deterministic rules of Agent Identity
554/// Section 3.1: canonical, non-small-order `A` and `R`, reduced `S`, and the
555/// cofactorless equation (`verify_strict`).
556pub fn verify_ed25519_strict(
557    verifying_key: &VerifyingKey,
558    message: &[u8],
559    signature: &[u8; 64],
560) -> Result<()> {
561    let failed = || SdkError::protocol("invalid_signature", "signature verification failed");
562    let r: [u8; 32] = signature[..32].try_into().expect("64-byte signature");
563    let s: [u8; 32] = signature[32..].try_into().expect("64-byte signature");
564    if !is_strict_point(verifying_key.as_bytes()) || !is_strict_point(&r) {
565        return Err(failed());
566    }
567    if !less_than_le(&s, &GROUP_L_LE) {
568        return Err(failed());
569    }
570    verifying_key
571        .verify_strict(message, &Signature::from_bytes(signature))
572        .map_err(|_| failed())
573}
574
575/// [`verify_ed25519_strict`] over a raw 32-byte public key, which may fail to
576/// decode as a point at all.
577pub fn verify_ed25519_strict_bytes(
578    public_key: &[u8; 32],
579    message: &[u8],
580    signature: &[u8; 64],
581) -> Result<()> {
582    let failed = || SdkError::protocol("invalid_signature", "signature verification failed");
583    if !is_strict_point(public_key) {
584        return Err(failed());
585    }
586    let key = VerifyingKey::from_bytes(public_key).map_err(|_| failed())?;
587    verify_ed25519_strict(&key, message, signature)
588}
589
590/// Parses signed JSON strictly (Agent Identity Section 4.1): rejects
591/// duplicate member names, unpaired surrogates, and integers outside the safe
592/// range. `serde_json::Value` silently keeps the last of two duplicate names,
593/// so parse raw request bodies with this before hashing.
594pub fn parse_strict_json(text: &str) -> Result<Value> {
595    let mut deserializer = serde_json::Deserializer::from_str(text);
596    let value = StrictJson::deserialize(&mut deserializer)
597        .map_err(|err| SdkError::protocol("invalid_event", format!("invalid JSON: {err}")))?;
598    deserializer
599        .end()
600        .map_err(|err| SdkError::protocol("invalid_event", format!("invalid JSON: {err}")))?;
601    Ok(value.0)
602}
603
604/// [`parse_strict_json`] for a signed envelope, checking its outer shape.
605pub fn parse_envelope_json<P>(text: &str) -> Result<Envelope<P>>
606where
607    P: serde::de::DeserializeOwned,
608{
609    let value = parse_strict_json(text)?;
610    let object = value.as_object().ok_or_else(|| {
611        SdkError::protocol(
612            "invalid_event",
613            "envelope must contain hash, event, and signature",
614        )
615    })?;
616    if let Some(key) = object
617        .keys()
618        .find(|key| !matches!(key.as_str(), "hash" | "event" | "signature"))
619    {
620        return Err(SdkError::protocol(
621            "invalid_event",
622            format!("unknown envelope field: {key}"),
623        ));
624    }
625    serde_json::from_value(value)
626        .map_err(|err| SdkError::protocol("invalid_event", format!("invalid envelope: {err}")))
627}
628
629struct StrictJson(Value);
630
631impl<'de> Deserialize<'de> for StrictJson {
632    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
633    where
634        D: Deserializer<'de>,
635    {
636        deserializer
637            .deserialize_any(StrictJsonVisitor)
638            .map(StrictJson)
639    }
640}
641
642struct StrictJsonVisitor;
643
644impl<'de> serde::de::Visitor<'de> for StrictJsonVisitor {
645    type Value = Value;
646
647    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
648        formatter.write_str("I-JSON")
649    }
650
651    fn visit_bool<E>(self, value: bool) -> std::result::Result<Value, E> {
652        Ok(Value::Bool(value))
653    }
654
655    fn visit_i64<E: DeError>(self, value: i64) -> std::result::Result<Value, E> {
656        if value.unsigned_abs() > MAX_SAFE_NONCE {
657            return Err(E::custom("integer outside the safe range"));
658        }
659        Ok(Value::from(value))
660    }
661
662    fn visit_u64<E: DeError>(self, value: u64) -> std::result::Result<Value, E> {
663        if value > MAX_SAFE_NONCE {
664            return Err(E::custom("integer outside the safe range"));
665        }
666        Ok(Value::from(value))
667    }
668
669    fn visit_f64<E: DeError>(self, value: f64) -> std::result::Result<Value, E> {
670        if value.fract() == 0.0 && value.abs() > MAX_SAFE_NONCE as f64 {
671            return Err(E::custom("integer outside the safe range"));
672        }
673        serde_json::Number::from_f64(value)
674            .map(Value::Number)
675            .ok_or_else(|| E::custom("number out of range"))
676    }
677
678    fn visit_str<E>(self, value: &str) -> std::result::Result<Value, E> {
679        Ok(Value::String(value.to_owned()))
680    }
681
682    fn visit_string<E>(self, value: String) -> std::result::Result<Value, E> {
683        Ok(Value::String(value))
684    }
685
686    fn visit_unit<E>(self) -> std::result::Result<Value, E> {
687        Ok(Value::Null)
688    }
689
690    fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Value, A::Error>
691    where
692        A: serde::de::SeqAccess<'de>,
693    {
694        let mut values = Vec::new();
695        while let Some(StrictJson(value)) = seq.next_element()? {
696            values.push(value);
697        }
698        Ok(Value::Array(values))
699    }
700
701    fn visit_map<A>(self, mut map: A) -> std::result::Result<Value, A::Error>
702    where
703        A: serde::de::MapAccess<'de>,
704    {
705        let mut object = serde_json::Map::new();
706        while let Some(key) = map.next_key::<String>()? {
707            if object.contains_key(&key) {
708                return Err(A::Error::custom(format!("duplicate member name {key:?}")));
709            }
710            let StrictJson(value) = map.next_value()?;
711            object.insert(key, value);
712        }
713        Ok(Value::Object(object))
714    }
715}
716
717fn valid_event_hash_bytes(event_hash: &[u8]) -> Result<&[u8; 32]> {
718    if event_hash.len() != 32 {
719        return Err(SdkError::InvalidEventHashLength(event_hash.len()));
720    }
721    Ok(event_hash
722        .try_into()
723        .expect("event hash length was checked above"))
724}
725
726pub fn verify_envelope<P>(envelope: &Envelope<P>) -> Result<()>
727where
728    P: Serialize,
729{
730    verify_event_hash(envelope)?;
731    verify_signature(envelope)
732}
733
734pub fn verify_timestamp(created_at: i64, now_ms: i64, window_ms: i64) -> Result<()> {
735    if window_ms < 0 || created_at.abs_diff(now_ms) > window_ms as u64 {
736        Err(SdkError::TimestampOutOfWindow)
737    } else {
738        Ok(())
739    }
740}
741
742#[derive(Clone, Copy, Debug, Eq, PartialEq)]
743pub struct NonceRecord {
744    pub max_nonce: u64,
745    pub expires_at: i64,
746}
747
748pub trait NonceStore {
749    fn check_and_update(
750        &mut self,
751        actor: &AgentId,
752        nonce: u64,
753        now_ms: i64,
754        ttl_ms: i64,
755    ) -> Result<u64>;
756
757    fn max_nonce(&self, actor: &AgentId, now_ms: i64) -> Option<u64>;
758}
759
760#[derive(Clone, Debug, Default)]
761pub struct MemoryNonceStore {
762    records: HashMap<AgentId, NonceRecord>,
763}
764
765impl MemoryNonceStore {
766    pub fn new() -> Self {
767        Self::default()
768    }
769}
770
771impl NonceStore for MemoryNonceStore {
772    fn check_and_update(
773        &mut self,
774        actor: &AgentId,
775        nonce: u64,
776        now_ms: i64,
777        ttl_ms: i64,
778    ) -> Result<u64> {
779        validate_nonce(nonce)?;
780        if ttl_ms < 0 {
781            return Err(SdkError::InvalidNonce(
782                "nonce cache ttl must be non-negative".to_owned(),
783            ));
784        }
785
786        if let Some(record) = self.records.get(actor) {
787            if record.expires_at > now_ms && nonce <= record.max_nonce {
788                return Err(SdkError::NonceNotGreater {
789                    max_nonce: record.max_nonce,
790                });
791            }
792        }
793
794        let record = NonceRecord {
795            max_nonce: nonce,
796            expires_at: now_ms.saturating_add(ttl_ms),
797        };
798        self.records.insert(actor.clone(), record);
799        Ok(record.max_nonce)
800    }
801
802    fn max_nonce(&self, actor: &AgentId, now_ms: i64) -> Option<u64> {
803        self.records
804            .get(actor)
805            .filter(|record| record.expires_at > now_ms)
806            .map(|record| record.max_nonce)
807    }
808}
809
810#[derive(Clone, Debug, Eq, PartialEq)]
811pub struct ClientNonceManager {
812    next_nonce: u64,
813}
814
815impl Default for ClientNonceManager {
816    fn default() -> Self {
817        Self { next_nonce: 1 }
818    }
819}
820
821impl ClientNonceManager {
822    pub fn new() -> Self {
823        Self::default()
824    }
825
826    pub fn with_next(next_nonce: u64) -> Result<Self> {
827        validate_nonce(next_nonce)?;
828        Ok(Self { next_nonce })
829    }
830
831    pub fn peek(&self) -> u64 {
832        self.next_nonce
833    }
834
835    pub fn next_nonce(&mut self) -> Result<u64> {
836        let nonce = self.next_nonce;
837        validate_nonce(nonce)?;
838        self.next_nonce += 1;
839        Ok(nonce)
840    }
841
842    /// Clock-derived nonce, `max(last + 1, created_at)` (Section 6.2): it
843    /// stays monotonic across restarts, restores, and devices sharing a key.
844    pub fn next_nonce_at(&mut self, created_at: i64) -> Result<u64> {
845        let clock = u64::try_from(created_at).unwrap_or(0);
846        if clock > self.next_nonce {
847            self.next_nonce = clock;
848        }
849        self.next_nonce()
850    }
851
852    /// Applies a `Max-Seen-Nonce` value. A value more than [`MAX_NONCE_JUMP`]
853    /// beyond `max(next_nonce, now_ms)` is rejected rather than applied.
854    pub fn observe_max_nonce(&mut self, max_nonce: u64, now_ms: i64) -> Result<()> {
855        validate_nonce(max_nonce)?;
856        let floor = self.next_nonce.max(u64::try_from(now_ms).unwrap_or(0));
857        if max_nonce > floor.saturating_add(MAX_NONCE_JUMP) {
858            return Err(SdkError::InvalidNonce(format!(
859                "Max-Seen-Nonce {max_nonce} jumps too far beyond the local sequence"
860            )));
861        }
862        if max_nonce >= self.next_nonce {
863            self.next_nonce = max_nonce.saturating_add(1);
864        }
865        Ok(())
866    }
867
868    pub fn observe_max_nonce_header(&mut self, value: &str, now_ms: i64) -> Result<()> {
869        let max_nonce = value
870            .parse::<u64>()
871            .map_err(|_| SdkError::InvalidNonce("invalid max nonce header".to_owned()))?;
872        self.observe_max_nonce(max_nonce, now_ms)
873    }
874}
875
876pub fn validate_nonce(nonce: u64) -> Result<()> {
877    if nonce == 0 || nonce > MAX_SAFE_NONCE {
878        // Number.MAX_SAFE_INTEGER in JavaScript, to prevent interoperability issues with JS clients using Number for nonces
879        Err(SdkError::InvalidNonce(
880            "nonce must be a positive integer less than or equal to 9007199254740991".to_owned(),
881        ))
882    } else {
883        Ok(())
884    }
885}
886
887#[derive(Clone, Debug)]
888pub struct LiveWriteOptions {
889    pub now_ms: i64,
890    pub window_ms: i64,
891    pub nonce_ttl_ms: i64,
892}
893
894impl Default for LiveWriteOptions {
895    fn default() -> Self {
896        Self {
897            now_ms: unix_ms(),
898            window_ms: DEFAULT_LIVE_WRITE_WINDOW_MS,
899            nonce_ttl_ms: DEFAULT_NONCE_TTL_MS,
900        }
901    }
902}
903
904/// Outcome of [`verify_submission`]: an exact resubmission of an accepted
905/// envelope, or a new live write with the nonce now recorded.
906#[derive(Clone, Copy, Debug, Eq, PartialEq)]
907pub enum SubmissionResult {
908    Resubmission,
909    Accepted { max_nonce: u64 },
910}
911
912/// Agent Identity Section 6.1 for one submission: verifies the envelope,
913/// answers an exact resubmission of an accepted envelope before the time
914/// window and nonce checks (Section 6.3), and otherwise enforces both.
915pub fn verify_submission<P, S, F>(
916    envelope: &Envelope<P>,
917    options: &LiveWriteOptions,
918    nonce_store: &mut S,
919    is_accepted: F,
920) -> Result<SubmissionResult>
921where
922    P: Serialize,
923    S: NonceStore,
924    F: Fn(&str) -> bool,
925{
926    verify_envelope(envelope)?;
927    if is_accepted(&envelope.hash) {
928        return Ok(SubmissionResult::Resubmission);
929    }
930    verify_timestamp(envelope.event.created_at, options.now_ms, options.window_ms)?;
931    let max_nonce = nonce_store.check_and_update(
932        &envelope.event.actor,
933        envelope.event.nonce,
934        options.now_ms,
935        options.nonce_ttl_ms,
936    )?;
937    Ok(SubmissionResult::Accepted { max_nonce })
938}
939
940pub fn verify_live_envelope<P, S>(
941    envelope: &Envelope<P>,
942    options: &LiveWriteOptions,
943    nonce_store: &mut S,
944) -> Result<u64>
945where
946    P: Serialize,
947    S: NonceStore,
948{
949    verify_envelope(envelope)?;
950    verify_timestamp(envelope.event.created_at, options.now_ms, options.window_ms)?;
951    nonce_store.check_and_update(
952        &envelope.event.actor,
953        envelope.event.nonce,
954        options.now_ms,
955        options.nonce_ttl_ms,
956    )
957}
958
959#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
960pub struct RequestJwtHeader {
961    pub alg: String,
962    pub typ: String,
963    pub kid: AgentId,
964}
965
966#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
967pub struct RequestJwtClaims {
968    pub iss: AgentId,
969    pub sub: AgentId,
970    /// Origin of the receiving service: scheme, host, and non-default port
971    /// with no path. All Agent Protocols endpoints on one origin share this
972    /// value; derive it with [`service_origin`].
973    pub aud: String,
974    pub iat: i64,
975    pub exp: i64,
976}
977
978#[derive(Clone, Debug, PartialEq, Eq)]
979pub struct RequestBinding {
980    pub audience: String,
981}
982
983impl RequestBinding {
984    pub fn new(audience: impl Into<String>) -> Self {
985        Self {
986            audience: audience.into(),
987        }
988    }
989}
990
991impl RequestJwtClaims {
992    pub fn new(agent_id: AgentId, binding: RequestBinding, issued_at: i64, ttl_secs: i64) -> Self {
993        Self {
994            iss: agent_id.clone(),
995            sub: agent_id,
996            aud: binding.audience,
997            iat: issued_at,
998            exp: issued_at + ttl_secs,
999        }
1000    }
1001}
1002
1003#[derive(Clone, Debug)]
1004pub struct RequestAuthContext {
1005    pub audience: String,
1006    pub now_secs: i64,
1007    pub max_ttl_secs: i64,
1008}
1009
1010impl RequestAuthContext {
1011    pub fn new(audience: impl Into<String>) -> Self {
1012        let binding = RequestBinding::new(audience);
1013        Self {
1014            audience: binding.audience,
1015            now_secs: unix_secs(),
1016            max_ttl_secs: DEFAULT_REQUEST_JWT_TTL_SECS,
1017        }
1018    }
1019}
1020
1021pub fn verify_request_jwt(token: &str, context: &RequestAuthContext) -> Result<RequestJwtClaims> {
1022    let parts: Vec<&str> = token.split('.').collect();
1023    if parts.len() != 3 {
1024        return Err(SdkError::InvalidJwt(
1025            "expected three compact JWS parts".into(),
1026        ));
1027    }
1028
1029    let header: RequestJwtHeader = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0])?)?;
1030    let claims: RequestJwtClaims = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1])?)?;
1031    let signature_bytes = URL_SAFE_NO_PAD.decode(parts[2])?;
1032    let signature_bytes: [u8; 64] = signature_bytes
1033        .try_into()
1034        .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
1035    let signature = Signature::from_bytes(&signature_bytes);
1036    let signing_input = format!("{}.{}", parts[0], parts[1]);
1037
1038    if header.alg != "EdDSA" {
1039        return Err(SdkError::InvalidJwtClaim("alg"));
1040    }
1041    if header.typ != "JWT" {
1042        return Err(SdkError::InvalidJwtClaim("typ"));
1043    }
1044    if header.kid != claims.iss || claims.iss != claims.sub {
1045        return Err(SdkError::InvalidJwtClaim("kid/iss/sub"));
1046    }
1047
1048    verify_ed25519_strict(
1049        &header.kid.verifying_key()?,
1050        signing_input.as_bytes(),
1051        &signature.to_bytes(),
1052    )?;
1053
1054    if claims.aud != context.audience {
1055        return Err(SdkError::InvalidJwtClaim("aud"));
1056    }
1057    if claims.exp <= claims.iat {
1058        return Err(SdkError::InvalidJwtClaim("exp/iat"));
1059    }
1060    if claims.iat > context.now_secs || claims.exp < context.now_secs {
1061        return Err(SdkError::InvalidJwtClaim("iat/exp"));
1062    }
1063    if claims.exp - claims.iat > context.max_ttl_secs {
1064        return Err(SdkError::InvalidJwtClaim("ttl"));
1065    }
1066
1067    Ok(claims)
1068}
1069
1070/// Checks that `value` is a serialized HTTPS origin (Agent Identity Section
1071/// 4.4): WHATWG URL parsing and origin serialization must reproduce it exactly.
1072pub fn validate_origin(value: &str) -> Result<()> {
1073    let parsed = url::Url::parse(value)
1074        .map_err(|_| SdkError::InvalidPayload(format!("not a valid origin: {value}")))?;
1075    if parsed.scheme() == "https"
1076        && parsed.host_str().is_some()
1077        && parsed.origin().ascii_serialization() == value
1078    {
1079        Ok(())
1080    } else {
1081        Err(SdkError::InvalidPayload(format!(
1082            "origin must be a serialized HTTPS origin: {value}"
1083        )))
1084    }
1085}
1086
1087pub const AGENT_URL_MAX_ROUTES: usize = 8;
1088
1089/// A parsed Agent URL (Agent Identity Section 3.5):
1090/// `did:agent:<key>[/<protocol>/<resource>][?route=<origin>...]`.
1091/// `protocol` and `resource` are both present or both absent.
1092#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1093pub struct AgentUrl {
1094    pub agent_id: AgentId,
1095    #[serde(default, skip_serializing_if = "Option::is_none")]
1096    pub protocol: Option<String>,
1097    #[serde(default, skip_serializing_if = "Option::is_none")]
1098    pub resource: Option<String>,
1099    #[serde(default)]
1100    pub routes: Vec<String>,
1101}
1102
1103fn invalid_agent_url(message: &str) -> SdkError {
1104    SdkError::protocol("invalid_url", format!("invalid agent URL: {message}"))
1105}
1106
1107fn valid_agent_url_path(protocol: &str, resource: &str) -> bool {
1108    !protocol.is_empty()
1109        && protocol.bytes().all(|b| b.is_ascii_lowercase())
1110        && !resource.is_empty()
1111        && resource
1112            .bytes()
1113            .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
1114}
1115
1116pub fn parse_agent_url(value: &str) -> Result<AgentUrl> {
1117    if !value.bytes().all(|b| (0x21..=0x7e).contains(&b)) || value.contains(['#', '%']) {
1118        return Err(invalid_agent_url(
1119            "must be printable ASCII without fragment or percent-encoding",
1120        ));
1121    }
1122    let (head, query) = match value.split_once('?') {
1123        Some((head, query)) => (head, Some(query)),
1124        None => (value, None),
1125    };
1126    let segments: Vec<&str> = head.split('/').collect();
1127    let mut url = AgentUrl {
1128        agent_id: segments[0].parse()?,
1129        protocol: None,
1130        resource: None,
1131        routes: Vec::new(),
1132    };
1133    if segments.len() != 1 {
1134        if segments.len() != 3 || !valid_agent_url_path(segments[1], segments[2]) {
1135            return Err(invalid_agent_url("path must be /<protocol>/<resource>"));
1136        }
1137        url.protocol = Some(segments[1].to_owned());
1138        url.resource = Some(segments[2].to_owned());
1139    }
1140    if let Some(query) = query {
1141        if query.split('&').count() > AGENT_URL_MAX_ROUTES {
1142            return Err(invalid_agent_url("at most eight routes"));
1143        }
1144        for item in query.split('&') {
1145            let route = item
1146                .strip_prefix("route=")
1147                .ok_or_else(|| invalid_agent_url("query allows only route parameters"))?;
1148            validate_origin(route)?;
1149            if url.routes.iter().any(|known| known == route) {
1150                return Err(invalid_agent_url("duplicate route"));
1151            }
1152            url.routes.push(route.to_owned());
1153        }
1154    }
1155    Ok(url)
1156}
1157
1158/// Formats an Agent URL. `agent_id` is valid by construction.
1159pub fn format_agent_url(url: &AgentUrl) -> Result<String> {
1160    let path = match (&url.protocol, &url.resource) {
1161        (None, None) => String::new(),
1162        (Some(protocol), Some(resource)) if valid_agent_url_path(protocol, resource) => {
1163            format!("/{protocol}/{resource}")
1164        }
1165        _ => return Err(invalid_agent_url("path must be /<protocol>/<resource>")),
1166    };
1167    let unique: std::collections::BTreeSet<&String> = url.routes.iter().collect();
1168    if url.routes.len() > AGENT_URL_MAX_ROUTES || unique.len() != url.routes.len() {
1169        return Err(invalid_agent_url(
1170            "routes must be at most eight unique origins",
1171        ));
1172    }
1173    for route in &url.routes {
1174        validate_origin(route)?;
1175    }
1176    let query = if url.routes.is_empty() {
1177        String::new()
1178    } else {
1179        format!(
1180            "?{}",
1181            url.routes
1182                .iter()
1183                .map(|route| format!("route={route}"))
1184                .collect::<Vec<_>>()
1185                .join("&")
1186        )
1187    };
1188    Ok(format!("{}{path}{query}", url.agent_id.as_str()))
1189}
1190
1191/// Derives the request JWT `aud` from a request URL: the service origin —
1192/// scheme, host, and non-default port, with no path (Agent Identity Section 7).
1193pub fn service_origin(url: &str) -> Result<String> {
1194    let parsed = url::Url::parse(url)
1195        .map_err(|_| SdkError::InvalidPayload(format!("not a valid URL: {url}")))?;
1196    if parsed.scheme() != "https" && parsed.scheme() != "http" {
1197        return Err(SdkError::InvalidPayload(format!(
1198            "not an HTTP(S) URL: {url}"
1199        )));
1200    }
1201    Ok(parsed.origin().ascii_serialization())
1202}
1203
1204pub fn unix_ms() -> i64 {
1205    let duration = SystemTime::now()
1206        .duration_since(UNIX_EPOCH)
1207        .unwrap_or_default();
1208    duration.as_millis().min(i64::MAX as u128) as i64
1209}
1210
1211pub fn unix_secs() -> i64 {
1212    unix_ms() / 1000
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217    use serde::ser;
1218    use serde_json::json;
1219
1220    use super::*;
1221
1222    struct FailingPayload;
1223
1224    impl Serialize for FailingPayload {
1225        fn serialize<S>(&self, _serializer: S) -> std::result::Result<S::Ok, S::Error>
1226        where
1227            S: Serializer,
1228        {
1229            Err(ser::Error::custom("payload cannot be serialized"))
1230        }
1231    }
1232
1233    #[test]
1234    fn signs_and_verifies_event_envelope() {
1235        let signer = AgentSigner::from_seed([7; 32]);
1236        let event = Event::new(
1237            "agent-profile/1.0",
1238            "profile.update",
1239            signer.agent_id(),
1240            1_779_753_600_000,
1241            1,
1242            json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1243        );
1244
1245        let envelope = signer.sign_event(event).unwrap();
1246
1247        assert!(!envelope.hash.starts_with("evt_"));
1248        assert_eq!(envelope.hash.len(), 43);
1249        verify_envelope(&envelope).unwrap();
1250    }
1251
1252    #[test]
1253    fn signs_and_verifies_raw_event_hash_bytes() {
1254        let seed = [18; 32];
1255        let signer = AgentSigner::from_seed(seed);
1256        let signing_key = SigningKey::from_bytes(&seed);
1257        let event = Event::new(
1258            "agent-profile/1.0",
1259            "profile.update",
1260            signer.agent_id(),
1261            1_779_753_600_000,
1262            1,
1263            json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1264        );
1265
1266        let digest = event_hash_bytes(&event).unwrap();
1267        let signature = sign_event_hash(&signing_key, &digest).unwrap();
1268        let envelope = signer.sign_event(event).unwrap();
1269
1270        assert_eq!(envelope.signature, signature);
1271        verify_event_hash_signature(&signing_key.verifying_key(), &digest, &signature).unwrap();
1272    }
1273
1274    #[test]
1275    fn rejects_tampered_event_payload() {
1276        let signer = AgentSigner::from_seed([8; 32]);
1277        let event = Event::new(
1278            "agent-profile/1.0",
1279            "profile.update",
1280            signer.agent_id(),
1281            1_779_753_600_000,
1282            1,
1283            json!({"name": "before"}),
1284        );
1285        let mut envelope = signer.sign_event(event).unwrap();
1286        envelope.event.payload = json!({"name": "after"});
1287
1288        assert!(verify_envelope(&envelope).is_err());
1289    }
1290
1291    #[test]
1292    fn rejects_non_canonical_event_payloads() {
1293        let signer = AgentSigner::from_seed([52; 32]);
1294        let event = Event::new(
1295            "agent-profile/1.0",
1296            "profile.update",
1297            signer.agent_id(),
1298            1000,
1299            1,
1300            FailingPayload,
1301        );
1302        assert!(matches!(
1303            canonical_event_bytes(&event),
1304            Err(SdkError::CanonicalJson(_))
1305        ));
1306        assert!(matches!(
1307            event_hash(&event),
1308            Err(SdkError::CanonicalJson(_))
1309        ));
1310        let envelope = Envelope {
1311            hash: "h".to_owned(),
1312            event,
1313            signature: "s".to_owned(),
1314        };
1315        assert!(matches!(
1316            verify_event_hash(&envelope),
1317            Err(SdkError::CanonicalJson(_))
1318        ));
1319        assert!(matches!(
1320            verify_signature(&envelope),
1321            Err(SdkError::CanonicalJson(_))
1322        ));
1323        assert!(matches!(
1324            verify_envelope(&envelope),
1325            Err(SdkError::CanonicalJson(_))
1326        ));
1327    }
1328
1329    #[test]
1330    fn rejects_nonce_reuse() {
1331        let signer = AgentSigner::from_seed([9; 32]);
1332        let options = LiveWriteOptions {
1333            now_ms: 1000,
1334            window_ms: 1000,
1335            nonce_ttl_ms: 1000,
1336        };
1337        let event = Event::new(
1338            "agent-profile/1.0",
1339            "profile.update",
1340            signer.agent_id(),
1341            1000,
1342            1,
1343            json!({"name": "ResearchAgent"}),
1344        );
1345        let envelope = signer.sign_event(event).unwrap();
1346        let mut store = MemoryNonceStore::new();
1347
1348        let max_nonce = verify_live_envelope(&envelope, &options, &mut store).unwrap();
1349        assert_eq!(max_nonce, 1);
1350        assert!(matches!(
1351            verify_live_envelope(&envelope, &options, &mut store),
1352            Err(SdkError::NonceNotGreater { max_nonce: 1 })
1353        ));
1354    }
1355
1356    #[test]
1357    fn client_nonce_manager_observes_server_max() {
1358        let mut manager = ClientNonceManager::new();
1359
1360        assert_eq!(manager.next_nonce().unwrap(), 1);
1361        manager.observe_max_nonce(5, 0).unwrap();
1362
1363        assert_eq!(manager.peek(), 6);
1364        manager.observe_max_nonce(4, 0).unwrap();
1365        assert_eq!(manager.peek(), 6);
1366        assert_eq!(manager.next_nonce().unwrap(), 6);
1367
1368        let mut invalid = ClientNonceManager {
1369            next_nonce: MAX_SAFE_NONCE + 1,
1370        };
1371        assert!(matches!(
1372            invalid.next_nonce(),
1373            Err(SdkError::InvalidNonce(_))
1374        ));
1375    }
1376
1377    #[test]
1378    fn rejects_nonce_values_outside_safe_json_integer_range() {
1379        let signer = AgentSigner::from_seed([16; 32]);
1380        let event = Event::new(
1381            "agent-profile/1.0",
1382            "profile.update",
1383            signer.agent_id(),
1384            1000,
1385            MAX_SAFE_NONCE + 1,
1386            json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1387        );
1388
1389        assert!(matches!(
1390            signer.sign_event(event),
1391            Err(SdkError::InvalidNonce(_))
1392        ));
1393    }
1394
1395    #[test]
1396    fn signs_and_verifies_request_jwt() {
1397        let signer = AgentSigner::from_seed([10; 32]);
1398        let claims = RequestJwtClaims::new(
1399            signer.agent_id(),
1400            RequestBinding::new("https://api.example.com"),
1401            100,
1402            300,
1403        );
1404        let token = signer.sign_request_jwt(&claims).unwrap();
1405        let context = RequestAuthContext {
1406            audience: "https://api.example.com".to_owned(),
1407            now_secs: 120,
1408            max_ttl_secs: 300,
1409        };
1410        let verified = verify_request_jwt(&token, &context).unwrap();
1411
1412        assert_eq!(verified.iss, signer.agent_id());
1413    }
1414
1415    #[test]
1416    fn rejects_request_jwts_with_non_positive_ttl() {
1417        let signer = AgentSigner::from_seed([17; 32]);
1418        let claims = RequestJwtClaims::new(
1419            signer.agent_id(),
1420            RequestBinding::new("https://api.example.com"),
1421            100,
1422            0,
1423        );
1424        let token = signer.sign_request_jwt(&claims).unwrap();
1425        let context = RequestAuthContext {
1426            audience: "https://api.example.com".to_owned(),
1427            now_secs: 100,
1428            max_ttl_secs: 300,
1429        };
1430
1431        assert!(matches!(
1432            verify_request_jwt(&token, &context),
1433            Err(SdkError::InvalidJwtClaim("exp/iat"))
1434        ));
1435    }
1436
1437    #[test]
1438    fn agent_id_string_conversions() {
1439        let signer = AgentSigner::from_seed([20; 32]);
1440        let agent_id = signer.agent_id();
1441        let text = agent_id.to_string();
1442
1443        assert_eq!(agent_id.as_str(), text);
1444        assert!(format!("{agent_id:?}").starts_with("AgentId("));
1445        assert_eq!(AgentId::try_from(text.clone()).unwrap(), agent_id);
1446        assert_eq!(text.parse::<AgentId>().unwrap(), agent_id);
1447        assert!(matches!(
1448            "not-a-did".parse::<AgentId>(),
1449            Err(SdkError::InvalidAgentIdPrefix)
1450        ));
1451        assert!(matches!(
1452            AgentId("not-a-did".to_owned()).public_key_bytes(),
1453            Err(SdkError::InvalidAgentIdPrefix)
1454        ));
1455        assert!(AgentId(format!("{AGENT_ID_PREFIX}%"))
1456            .public_key_bytes()
1457            .is_err());
1458        let short = AgentId(format!(
1459            "{}{}",
1460            AGENT_ID_PREFIX,
1461            URL_SAFE_NO_PAD.encode([1_u8; 31])
1462        ));
1463        assert!(matches!(
1464            short.public_key_bytes(),
1465            Err(SdkError::InvalidPublicKeyLength(31))
1466        ));
1467        assert!(short.verifying_key().is_err());
1468        assert!(format!("{}%", AGENT_ID_PREFIX).parse::<AgentId>().is_err());
1469    }
1470
1471    #[test]
1472    fn agent_signer_exposes_keys_and_generates() {
1473        let signer = AgentSigner::from_seed([21; 32]);
1474        assert_eq!(
1475            signer.verifying_key().to_bytes(),
1476            signer.agent_id().public_key_bytes().unwrap()
1477        );
1478
1479        let generated = AgentSigner::generate();
1480        let event = Event::new(
1481            "agent-profile/1.0",
1482            "profile.update",
1483            generated.agent_id(),
1484            1000,
1485            1,
1486            json!({"name": "Generated"}),
1487        );
1488        verify_envelope(&generated.sign_event(event).unwrap()).unwrap();
1489    }
1490
1491    #[test]
1492    fn sign_request_jwt_rejects_foreign_claims() {
1493        let signer = AgentSigner::from_seed([22; 32]);
1494        let other = AgentSigner::from_seed([23; 32]);
1495        let claims = RequestJwtClaims {
1496            iss: other.agent_id(),
1497            sub: other.agent_id(),
1498            aud: "https://api.example.com".to_owned(),
1499            iat: 100,
1500            exp: 400,
1501        };
1502        assert!(matches!(
1503            signer.sign_request_jwt(&claims),
1504            Err(SdkError::InvalidJwtClaim("iss/sub"))
1505        ));
1506    }
1507
1508    #[test]
1509    fn free_sign_event_matches_signer() {
1510        let seed = [24; 32];
1511        let signer = AgentSigner::from_seed(seed);
1512        let signing_key = SigningKey::from_bytes(&seed);
1513        let event = Event::new(
1514            "agent-profile/1.0",
1515            "profile.update",
1516            signer.agent_id(),
1517            1000,
1518            1,
1519            json!({"name": "Agent"}),
1520        );
1521
1522        let signature = sign_event(&signing_key, &event).unwrap();
1523        assert_eq!(signer.sign_event(event).unwrap().signature, signature);
1524    }
1525
1526    #[test]
1527    fn rejects_event_hash_with_wrong_length() {
1528        let signing_key = SigningKey::from_bytes(&[25; 32]);
1529        assert!(matches!(
1530            sign_event_hash(&signing_key, &[0_u8; 10]),
1531            Err(SdkError::InvalidEventHashLength(10))
1532        ));
1533        assert!(matches!(
1534            verify_event_hash_signature(&signing_key.verifying_key(), &[0_u8; 10], "AAAA"),
1535            Err(SdkError::InvalidSignatureLength(_))
1536        ));
1537        let signature = URL_SAFE_NO_PAD.encode([0_u8; 64]);
1538        assert!(matches!(
1539            verify_event_hash_signature(&signing_key.verifying_key(), &[0_u8; 10], &signature),
1540            Err(SdkError::InvalidEventHashLength(10))
1541        ));
1542    }
1543
1544    #[test]
1545    fn verify_timestamp_rejects_drift_and_negative_window() {
1546        verify_timestamp(1000, 1200, 1000).unwrap();
1547        assert!(matches!(
1548            verify_timestamp(0, 1_000_000, 1000),
1549            Err(SdkError::TimestampOutOfWindow)
1550        ));
1551        assert!(matches!(
1552            verify_timestamp(100, 100, -1),
1553            Err(SdkError::TimestampOutOfWindow)
1554        ));
1555    }
1556
1557    #[test]
1558    fn verify_live_envelope_propagates_envelope_and_timestamp_errors() {
1559        let signer = AgentSigner::from_seed([53; 32]);
1560        let event = Event::new(
1561            "agent-profile/1.0",
1562            "profile.update",
1563            signer.agent_id(),
1564            1000,
1565            1,
1566            json!({"name": "ResearchAgent"}),
1567        );
1568        let mut envelope = signer.sign_event(event).unwrap();
1569        let mut store = MemoryNonceStore::new();
1570        let options = LiveWriteOptions {
1571            now_ms: 1000,
1572            window_ms: 1000,
1573            nonce_ttl_ms: 1000,
1574        };
1575
1576        envelope.signature = "invalid".to_owned();
1577        assert!(verify_live_envelope(&envelope, &options, &mut store).is_err());
1578
1579        let event = Event::new(
1580            "agent-profile/1.0",
1581            "profile.update",
1582            signer.agent_id(),
1583            0,
1584            2,
1585            json!({"name": "ResearchAgent"}),
1586        );
1587        let envelope = signer.sign_event(event).unwrap();
1588        let expired_options = LiveWriteOptions {
1589            now_ms: 3000,
1590            ..options
1591        };
1592        assert!(matches!(
1593            verify_live_envelope(&envelope, &expired_options, &mut store),
1594            Err(SdkError::TimestampOutOfWindow)
1595        ));
1596    }
1597
1598    #[test]
1599    fn memory_nonce_store_rejects_negative_ttl_and_expires() {
1600        let actor = AgentSigner::from_seed([26; 32]).agent_id();
1601        let mut store = MemoryNonceStore::new();
1602        assert!(matches!(
1603            store.check_and_update(&actor, 1, 1000, -1),
1604            Err(SdkError::InvalidNonce(_))
1605        ));
1606
1607        store.check_and_update(&actor, 4, 1000, 1000).unwrap();
1608        // a strictly greater nonce is accepted while the record is still live
1609        assert_eq!(store.check_and_update(&actor, 5, 1100, 1000).unwrap(), 5);
1610        assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1611        // record has expired by now_ms past expires_at
1612        assert_eq!(store.max_nonce(&actor, 5000), None);
1613        assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1614        // a fresh actor has no record at all
1615        let stranger = AgentSigner::from_seed([27; 32]).agent_id();
1616        assert_eq!(store.max_nonce(&stranger, 1500), None);
1617    }
1618
1619    #[test]
1620    fn client_nonce_manager_with_next_and_header() {
1621        assert!(ClientNonceManager::with_next(0).is_err());
1622        let mut manager = ClientNonceManager::with_next(5).unwrap();
1623        assert_eq!(manager.peek(), 5);
1624
1625        manager.observe_max_nonce_header("10", 0).unwrap();
1626        assert_eq!(manager.peek(), 11);
1627        assert!(matches!(
1628            manager.observe_max_nonce_header("not-a-number", 0),
1629            Err(SdkError::InvalidNonce(_))
1630        ));
1631        assert!(matches!(
1632            manager.observe_max_nonce_header("9007199254740992", 0),
1633            Err(SdkError::InvalidNonce(_))
1634        ));
1635    }
1636
1637    #[test]
1638    fn default_options_and_context_use_real_clock() {
1639        let options = LiveWriteOptions::default();
1640        assert_eq!(options.window_ms, DEFAULT_LIVE_WRITE_WINDOW_MS);
1641        assert_eq!(options.nonce_ttl_ms, DEFAULT_NONCE_TTL_MS);
1642        assert!(options.now_ms > 0);
1643
1644        let context = RequestAuthContext::new("https://api.example.com");
1645        assert_eq!(context.audience, "https://api.example.com");
1646        assert_eq!(context.max_ttl_secs, DEFAULT_REQUEST_JWT_TTL_SECS);
1647        assert_eq!(context.now_secs, unix_secs());
1648        assert!(unix_ms() >= context.now_secs * 1000);
1649    }
1650
1651    fn encode_token(header: Value, claims: Value) -> String {
1652        let header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
1653        let claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).unwrap());
1654        let signature = URL_SAFE_NO_PAD.encode([0_u8; 64]);
1655        format!("{header}.{claims}.{signature}")
1656    }
1657
1658    #[test]
1659    fn verify_request_jwt_rejects_malformed_tokens() {
1660        let signer = AgentSigner::from_seed([28; 32]);
1661        let agent_id = signer.agent_id();
1662        let other = AgentSigner::from_seed([29; 32]).agent_id();
1663        let context = RequestAuthContext {
1664            audience: "https://api.example.com".to_owned(),
1665            now_secs: 200,
1666            max_ttl_secs: 300,
1667        };
1668        let valid_claims = json!({
1669            "iss": agent_id,
1670            "sub": agent_id,
1671            "aud": "https://api.example.com",
1672            "iat": 100,
1673            "exp": 400,
1674        });
1675
1676        assert!(matches!(
1677            verify_request_jwt("only.two", &context),
1678            Err(SdkError::InvalidJwt(_))
1679        ));
1680        assert!(verify_request_jwt("%.e30.AAAA", &context).is_err());
1681        let invalid_header = format!(
1682            "{}.{}.{}",
1683            URL_SAFE_NO_PAD.encode(b"not-json"),
1684            URL_SAFE_NO_PAD.encode(serde_json::to_vec(&valid_claims).unwrap()),
1685            URL_SAFE_NO_PAD.encode([0_u8; 64])
1686        );
1687        assert!(verify_request_jwt(&invalid_header, &context).is_err());
1688        let header = URL_SAFE_NO_PAD.encode(
1689            serde_json::to_vec(&json!({"alg": "EdDSA", "typ": "JWT", "kid": agent_id})).unwrap(),
1690        );
1691        let claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&valid_claims).unwrap());
1692        assert!(verify_request_jwt(&format!("{header}.%.AAAA"), &context).is_err());
1693        assert!(verify_request_jwt(&format!("{header}.{claims}.%"), &context).is_err());
1694        assert!(matches!(
1695            verify_request_jwt(&format!("{header}.{claims}.AAAA"), &context),
1696            Err(SdkError::InvalidSignatureLength(_))
1697        ));
1698        assert!(verify_request_jwt(
1699            &format!("{header}.{claims}.{}", URL_SAFE_NO_PAD.encode([0_u8; 64])),
1700            &context
1701        )
1702        .is_err());
1703        let invalid_key = AgentId::from_public_key(&[255_u8; 32]);
1704        assert!(verify_request_jwt(
1705            &encode_token(
1706                json!({"alg": "EdDSA", "typ": "JWT", "kid": invalid_key}),
1707                json!({
1708                    "iss": invalid_key,
1709                    "sub": invalid_key,
1710                    "aud": "https://api.example.com",
1711                    "iat": 100,
1712                    "exp": 400,
1713                }),
1714            ),
1715            &context,
1716        )
1717        .is_err());
1718        assert!(matches!(
1719            verify_request_jwt(
1720                &encode_token(
1721                    json!({"alg": "HS256", "typ": "JWT", "kid": agent_id}),
1722                    valid_claims.clone(),
1723                ),
1724                &context,
1725            ),
1726            Err(SdkError::InvalidJwtClaim("alg"))
1727        ));
1728        assert!(matches!(
1729            verify_request_jwt(
1730                &encode_token(
1731                    json!({"alg": "EdDSA", "typ": "JWS", "kid": agent_id}),
1732                    valid_claims.clone(),
1733                ),
1734                &context,
1735            ),
1736            Err(SdkError::InvalidJwtClaim("typ"))
1737        ));
1738        assert!(matches!(
1739            verify_request_jwt(
1740                &encode_token(
1741                    json!({"alg": "EdDSA", "typ": "JWT", "kid": other}),
1742                    valid_claims,
1743                ),
1744                &context,
1745            ),
1746            Err(SdkError::InvalidJwtClaim("kid/iss/sub"))
1747        ));
1748    }
1749
1750    #[test]
1751    fn rejects_non_canonical_base64url_encodings() {
1752        let signer = AgentSigner::from_seed([62; 32]);
1753        let agent_id = signer.agent_id();
1754        let suffix = agent_id
1755            .as_str()
1756            .strip_prefix(AGENT_ID_PREFIX)
1757            .unwrap()
1758            .to_owned();
1759
1760        // Padding characters are rejected even when the decoded bytes match.
1761        let padded = AgentId(format!("{AGENT_ID_PREFIX}{suffix}=="));
1762        assert!(padded.public_key_bytes().is_err());
1763
1764        // Non-zero trailing bits give the same key a second string form:
1765        // replace the final character with one sharing its used bits but
1766        // different trailing bits (the last char of a 32-byte value uses 4 of
1767        // its 6 bits).
1768        const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1769        let last = suffix.as_bytes()[suffix.len() - 1];
1770        let index = ALPHABET.iter().position(|&c| c == last).unwrap();
1771        let non_canonical = ALPHABET[(index & !3) | ((index & 3) ^ 1)] as char;
1772        let mut tweaked = suffix[..suffix.len() - 1].to_owned();
1773        tweaked.push(non_canonical);
1774        let tweaked = AgentId(format!("{AGENT_ID_PREFIX}{tweaked}"));
1775        assert!(tweaked.public_key_bytes().is_err());
1776
1777        // Signatures must be canonical base64url too.
1778        let event = Event::new(
1779            "agent-profile/1.0",
1780            "profile.update",
1781            signer.agent_id(),
1782            1000,
1783            1,
1784            json!({"name": "Agent"}),
1785        );
1786        let mut envelope = signer.sign_event(event).unwrap();
1787        envelope.signature.push('=');
1788        assert!(verify_signature(&envelope).is_err());
1789    }
1790
1791    #[test]
1792    fn service_origin_derives_request_jwt_audience() {
1793        assert_eq!(
1794            service_origin("https://api.example.com/v1/rooms/room1").unwrap(),
1795            "https://api.example.com"
1796        );
1797        assert_eq!(
1798            service_origin("https://api.example.com:8443/path?q=1").unwrap(),
1799            "https://api.example.com:8443"
1800        );
1801        assert_eq!(
1802            service_origin("https://API.Example.com:443/").unwrap(),
1803            "https://api.example.com"
1804        );
1805        assert!(service_origin("not a url").is_err());
1806        assert!(service_origin("ftp://example.com").is_err());
1807    }
1808
1809    #[test]
1810    fn verify_request_jwt_enforces_audience_and_window() {
1811        let signer = AgentSigner::from_seed([30; 32]);
1812
1813        let wrong_audience = signer
1814            .sign_request_jwt(&RequestJwtClaims::new(
1815                signer.agent_id(),
1816                RequestBinding::new("https://api.example.com"),
1817                100,
1818                300,
1819            ))
1820            .unwrap();
1821        let context = RequestAuthContext {
1822            audience: "https://other.example.com".to_owned(),
1823            now_secs: 200,
1824            max_ttl_secs: 300,
1825        };
1826        assert!(matches!(
1827            verify_request_jwt(&wrong_audience, &context),
1828            Err(SdkError::InvalidJwtClaim("aud"))
1829        ));
1830
1831        let expired = signer
1832            .sign_request_jwt(&RequestJwtClaims::new(
1833                signer.agent_id(),
1834                RequestBinding::new("https://api.example.com"),
1835                1000,
1836                300,
1837            ))
1838            .unwrap();
1839        let later = RequestAuthContext {
1840            audience: "https://api.example.com".to_owned(),
1841            now_secs: 5000,
1842            max_ttl_secs: 300,
1843        };
1844        assert!(matches!(
1845            verify_request_jwt(&expired, &later),
1846            Err(SdkError::InvalidJwtClaim("iat/exp"))
1847        ));
1848
1849        let long_ttl = signer
1850            .sign_request_jwt(&RequestJwtClaims::new(
1851                signer.agent_id(),
1852                RequestBinding::new("https://api.example.com"),
1853                100,
1854                400,
1855            ))
1856            .unwrap();
1857        let strict = RequestAuthContext {
1858            audience: "https://api.example.com".to_owned(),
1859            now_secs: 200,
1860            max_ttl_secs: 300,
1861        };
1862        assert!(matches!(
1863            verify_request_jwt(&long_ttl, &strict),
1864            Err(SdkError::InvalidJwtClaim("ttl"))
1865        ));
1866    }
1867}