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