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;
17pub const DEFAULT_NONCE_TTL_MS: i64 = 300_000;
18pub const DEFAULT_REQUEST_JWT_TTL_SECS: i64 = 300;
19pub const MAX_NONCE_HEADER: &str = "Max-Seen-Nonce";
20pub const MAX_SAFE_NONCE: u64 = 0x1FFFFFFFFFFFFF;
21
22#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
23pub struct AgentId(String);
24
25impl AgentId {
26    pub fn from_public_key(public_key: &[u8; 32]) -> Self {
27        Self(format!(
28            "{}{}",
29            AGENT_ID_PREFIX,
30            URL_SAFE_NO_PAD.encode(public_key)
31        ))
32    }
33
34    pub fn as_str(&self) -> &str {
35        &self.0
36    }
37
38    pub fn public_key_bytes(&self) -> Result<[u8; 32]> {
39        let encoded = self
40            .0
41            .strip_prefix(AGENT_ID_PREFIX)
42            .ok_or(SdkError::InvalidAgentIdPrefix)?;
43        // URL_SAFE_NO_PAD rejects padding characters and non-zero trailing
44        // bits, so non-canonical encodings of the same key are rejected.
45        let bytes = URL_SAFE_NO_PAD.decode(encoded)?;
46        bytes
47            .try_into()
48            .map_err(|bytes: Vec<u8>| SdkError::InvalidPublicKeyLength(bytes.len()))
49    }
50
51    pub fn verifying_key(&self) -> Result<VerifyingKey> {
52        Ok(VerifyingKey::from_bytes(&self.public_key_bytes()?)?)
53    }
54}
55
56impl fmt::Debug for AgentId {
57    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58        formatter.debug_tuple("AgentId").field(&self.0).finish()
59    }
60}
61
62impl fmt::Display for AgentId {
63    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
64        formatter.write_str(&self.0)
65    }
66}
67
68impl FromStr for AgentId {
69    type Err = SdkError;
70
71    fn from_str(value: &str) -> Result<Self> {
72        if !value.starts_with(AGENT_ID_PREFIX) {
73            return Err(SdkError::InvalidAgentIdPrefix);
74        }
75        let agent_id = Self(value.to_owned());
76        agent_id.public_key_bytes()?;
77        Ok(agent_id)
78    }
79}
80
81impl TryFrom<String> for AgentId {
82    type Error = SdkError;
83
84    fn try_from(value: String) -> Result<Self> {
85        Self::from_str(&value)
86    }
87}
88
89impl Serialize for AgentId {
90    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
91    where
92        S: Serializer,
93    {
94        serializer.serialize_str(&self.0)
95    }
96}
97
98impl<'de> Deserialize<'de> for AgentId {
99    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
100    where
101        D: Deserializer<'de>,
102    {
103        let value = String::deserialize(deserializer)?;
104        Self::from_str(&value).map_err(D::Error::custom)
105    }
106}
107
108pub struct AgentSigner {
109    signing_key: SigningKey,
110}
111
112impl AgentSigner {
113    pub fn generate() -> Self {
114        let mut seed = [0_u8; 32];
115        rand::fill(&mut seed);
116        Self::from_seed(seed)
117    }
118
119    pub fn from_seed(seed: [u8; 32]) -> Self {
120        Self {
121            signing_key: SigningKey::from_bytes(&seed),
122        }
123    }
124
125    pub fn agent_id(&self) -> AgentId {
126        AgentId::from_public_key(&self.signing_key.verifying_key().to_bytes())
127    }
128
129    pub fn verifying_key(&self) -> VerifyingKey {
130        self.signing_key.verifying_key()
131    }
132
133    pub fn sign_event<P>(&self, event: Event<P>) -> Result<Envelope<P>>
134    where
135        P: Serialize,
136    {
137        let event_hash = event_hash_bytes(&event)?;
138        let hash = URL_SAFE_NO_PAD.encode(&event_hash);
139        let signature = sign_event_hash(&self.signing_key, &event_hash)
140            .expect("event hashes are always 32 bytes");
141        Ok(Envelope {
142            hash,
143            event,
144            signature,
145        })
146    }
147
148    pub fn sign_request_jwt(&self, claims: &RequestJwtClaims) -> Result<String> {
149        let agent_id = self.agent_id();
150        if claims.iss != agent_id || claims.sub != agent_id {
151            return Err(SdkError::InvalidJwtClaim("iss/sub"));
152        }
153
154        let header = RequestJwtHeader {
155            alg: "EdDSA".to_owned(),
156            typ: "JWT".to_owned(),
157            kid: agent_id,
158        };
159        let header =
160            URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).expect("JWT header is JSON-safe"));
161        let payload =
162            URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims).expect("JWT claims are JSON-safe"));
163        let signing_input = format!("{header}.{payload}");
164        let signature = self.signing_key.sign(signing_input.as_bytes());
165        Ok(format!(
166            "{signing_input}.{}",
167            URL_SAFE_NO_PAD.encode(signature.to_bytes())
168        ))
169    }
170}
171
172#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
173pub struct Event<P = Value> {
174    pub protocol: String,
175    #[serde(rename = "type")]
176    pub kind: String,
177    pub actor: AgentId,
178    pub created_at: i64,
179    pub nonce: u64,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub room_id: Option<String>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub base_seq: Option<u64>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub base_hash: Option<String>,
186    #[serde(default, skip_serializing_if = "Vec::is_empty")]
187    pub mentions: Vec<AgentId>,
188    pub payload: P,
189    #[serde(default, flatten)]
190    pub extra: BTreeMap<String, Value>,
191}
192
193impl<P> Event<P> {
194    pub fn new(
195        protocol: impl Into<String>,
196        kind: impl Into<String>,
197        actor: AgentId,
198        created_at: i64,
199        nonce: u64,
200        payload: P,
201    ) -> Self {
202        Self {
203            protocol: protocol.into(),
204            kind: kind.into(),
205            actor,
206            created_at,
207            nonce,
208            room_id: None,
209            base_seq: None,
210            base_hash: None,
211            mentions: Vec::new(),
212            payload,
213            extra: BTreeMap::new(),
214        }
215    }
216
217    pub fn with_room_id(mut self, room_id: impl Into<String>) -> Self {
218        self.room_id = Some(room_id.into());
219        self
220    }
221
222    pub fn with_room_head(mut self, base_seq: u64, base_hash: impl Into<String>) -> Self {
223        self.base_seq = Some(base_seq);
224        self.base_hash = Some(base_hash.into());
225        self
226    }
227
228    pub fn with_mentions(mut self, mentions: Vec<AgentId>) -> Self {
229        self.mentions = mentions;
230        self
231    }
232
233    pub fn with_mention(mut self, agent_id: AgentId) -> Self {
234        self.mentions.push(agent_id);
235        self
236    }
237}
238
239#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
240pub struct Envelope<P = Value> {
241    pub hash: String,
242    pub event: Event<P>,
243    pub signature: String,
244}
245
246pub fn canonical_event_bytes<P>(event: &Event<P>) -> Result<Vec<u8>>
247where
248    P: Serialize,
249{
250    serde_jcs::to_vec(event).map_err(|err| SdkError::CanonicalJson(err.to_string()))
251}
252
253pub fn event_hash<P>(event: &Event<P>) -> Result<String>
254where
255    P: Serialize,
256{
257    Ok(URL_SAFE_NO_PAD.encode(event_hash_bytes(event)?))
258}
259
260pub fn event_hash_bytes<P>(event: &Event<P>) -> Result<[u8; 32]>
261where
262    P: Serialize,
263{
264    validate_nonce(event.nonce)?;
265    let digest = Sha3_256::digest(canonical_event_bytes(event)?);
266    let mut bytes = [0_u8; 32];
267    bytes.copy_from_slice(&digest);
268    Ok(bytes)
269}
270
271pub fn sign_event<P>(signing_key: &SigningKey, event: &Event<P>) -> Result<String>
272where
273    P: Serialize,
274{
275    let event_hash = event_hash_bytes(event)?;
276    Ok(sign_event_hash(signing_key, &event_hash).expect("event hashes are always 32 bytes"))
277}
278
279/// Signs a precomputed 32-byte event hash. Signing a digest supplied by
280/// another component without seeing the event it commits to (blind signing) is
281/// NOT RECOMMENDED: `actor` and all event content are inside the digest, so a
282/// blind signer can be tricked into signing arbitrary events attributed to its
283/// key. Prefer [`sign_event`], which canonicalizes and hashes the event.
284pub fn sign_event_hash(signing_key: &SigningKey, event_hash: &[u8]) -> Result<String> {
285    let signature = signing_key.sign(valid_event_hash_bytes(event_hash)?);
286    Ok(URL_SAFE_NO_PAD.encode(signature.to_bytes()))
287}
288
289pub fn verify_event_hash<P>(envelope: &Envelope<P>) -> Result<()>
290where
291    P: Serialize,
292{
293    let expected = event_hash(&envelope.event)?;
294    if expected == envelope.hash {
295        Ok(())
296    } else {
297        Err(SdkError::InvalidEventHash {
298            expected,
299            actual: envelope.hash.clone(),
300        })
301    }
302}
303
304pub fn verify_signature<P>(envelope: &Envelope<P>) -> Result<()>
305where
306    P: Serialize,
307{
308    let event_hash = event_hash_bytes(&envelope.event)?;
309    let verifying_key = envelope.event.actor.verifying_key()?;
310    verify_event_hash_signature(&verifying_key, &event_hash, &envelope.signature)
311}
312
313pub fn verify_event_hash_signature(
314    verifying_key: &VerifyingKey,
315    event_hash: &[u8],
316    encoded_signature: &str,
317) -> Result<()> {
318    let signature_bytes = URL_SAFE_NO_PAD.decode(encoded_signature)?;
319    let signature_bytes: [u8; 64] = signature_bytes
320        .try_into()
321        .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
322    let signature = Signature::from_bytes(&signature_bytes);
323    verifying_key.verify_strict(valid_event_hash_bytes(event_hash)?, &signature)?;
324    Ok(())
325}
326
327fn valid_event_hash_bytes(event_hash: &[u8]) -> Result<&[u8; 32]> {
328    if event_hash.len() != 32 {
329        return Err(SdkError::InvalidEventHashLength(event_hash.len()));
330    }
331    Ok(event_hash
332        .try_into()
333        .expect("event hash length was checked above"))
334}
335
336pub fn verify_envelope<P>(envelope: &Envelope<P>) -> Result<()>
337where
338    P: Serialize,
339{
340    verify_event_hash(envelope)?;
341    verify_signature(envelope)
342}
343
344pub fn verify_timestamp(created_at: i64, now_ms: i64, window_ms: i64) -> Result<()> {
345    if window_ms < 0 || created_at.abs_diff(now_ms) > window_ms as u64 {
346        Err(SdkError::TimestampOutOfWindow)
347    } else {
348        Ok(())
349    }
350}
351
352#[derive(Clone, Copy, Debug, Eq, PartialEq)]
353pub struct NonceRecord {
354    pub max_nonce: u64,
355    pub expires_at: i64,
356}
357
358pub trait NonceStore {
359    fn check_and_update(
360        &mut self,
361        actor: &AgentId,
362        nonce: u64,
363        now_ms: i64,
364        ttl_ms: i64,
365    ) -> Result<u64>;
366
367    fn max_nonce(&self, actor: &AgentId, now_ms: i64) -> Option<u64>;
368}
369
370#[derive(Clone, Debug, Default)]
371pub struct MemoryNonceStore {
372    records: HashMap<AgentId, NonceRecord>,
373}
374
375impl MemoryNonceStore {
376    pub fn new() -> Self {
377        Self::default()
378    }
379}
380
381impl NonceStore for MemoryNonceStore {
382    fn check_and_update(
383        &mut self,
384        actor: &AgentId,
385        nonce: u64,
386        now_ms: i64,
387        ttl_ms: i64,
388    ) -> Result<u64> {
389        validate_nonce(nonce)?;
390        if ttl_ms < 0 {
391            return Err(SdkError::InvalidNonce(
392                "nonce cache ttl must be non-negative".to_owned(),
393            ));
394        }
395
396        if let Some(record) = self.records.get(actor) {
397            if record.expires_at > now_ms && nonce <= record.max_nonce {
398                return Err(SdkError::NonceNotGreater {
399                    max_nonce: record.max_nonce,
400                });
401            }
402        }
403
404        let record = NonceRecord {
405            max_nonce: nonce,
406            expires_at: now_ms.saturating_add(ttl_ms),
407        };
408        self.records.insert(actor.clone(), record);
409        Ok(record.max_nonce)
410    }
411
412    fn max_nonce(&self, actor: &AgentId, now_ms: i64) -> Option<u64> {
413        self.records
414            .get(actor)
415            .filter(|record| record.expires_at > now_ms)
416            .map(|record| record.max_nonce)
417    }
418}
419
420#[derive(Clone, Debug, Eq, PartialEq)]
421pub struct ClientNonceManager {
422    next_nonce: u64,
423}
424
425impl Default for ClientNonceManager {
426    fn default() -> Self {
427        Self { next_nonce: 1 }
428    }
429}
430
431impl ClientNonceManager {
432    pub fn new() -> Self {
433        Self::default()
434    }
435
436    pub fn with_next(next_nonce: u64) -> Result<Self> {
437        validate_nonce(next_nonce)?;
438        Ok(Self { next_nonce })
439    }
440
441    pub fn peek(&self) -> u64 {
442        self.next_nonce
443    }
444
445    pub fn next_nonce(&mut self) -> Result<u64> {
446        let nonce = self.next_nonce;
447        validate_nonce(nonce)?;
448        self.next_nonce += 1;
449        Ok(nonce)
450    }
451
452    pub fn observe_max_nonce(&mut self, max_nonce: u64) {
453        if max_nonce >= self.next_nonce {
454            self.next_nonce = max_nonce.saturating_add(1);
455        }
456    }
457
458    pub fn observe_max_nonce_header(&mut self, value: &str) -> Result<()> {
459        let max_nonce = value
460            .parse::<u64>()
461            .map_err(|_| SdkError::InvalidNonce("invalid max nonce header".to_owned()))?;
462        validate_nonce(max_nonce)?;
463        self.observe_max_nonce(max_nonce);
464        Ok(())
465    }
466}
467
468pub fn validate_nonce(nonce: u64) -> Result<()> {
469    if nonce == 0 || nonce > MAX_SAFE_NONCE {
470        // Number.MAX_SAFE_INTEGER in JavaScript, to prevent interoperability issues with JS clients using Number for nonces
471        Err(SdkError::InvalidNonce(
472            "nonce must be a positive integer less than or equal to 9007199254740991".to_owned(),
473        ))
474    } else {
475        Ok(())
476    }
477}
478
479#[derive(Clone, Debug)]
480pub struct LiveWriteOptions {
481    pub now_ms: i64,
482    pub window_ms: i64,
483    pub nonce_ttl_ms: i64,
484}
485
486impl Default for LiveWriteOptions {
487    fn default() -> Self {
488        Self {
489            now_ms: unix_ms(),
490            window_ms: DEFAULT_LIVE_WRITE_WINDOW_MS,
491            nonce_ttl_ms: DEFAULT_NONCE_TTL_MS,
492        }
493    }
494}
495
496pub fn verify_live_envelope<P, S>(
497    envelope: &Envelope<P>,
498    options: &LiveWriteOptions,
499    nonce_store: &mut S,
500) -> Result<u64>
501where
502    P: Serialize,
503    S: NonceStore,
504{
505    verify_envelope(envelope)?;
506    verify_timestamp(envelope.event.created_at, options.now_ms, options.window_ms)?;
507    nonce_store.check_and_update(
508        &envelope.event.actor,
509        envelope.event.nonce,
510        options.now_ms,
511        options.nonce_ttl_ms,
512    )
513}
514
515#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
516pub struct RequestJwtHeader {
517    pub alg: String,
518    pub typ: String,
519    pub kid: AgentId,
520}
521
522#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
523pub struct RequestJwtClaims {
524    pub iss: AgentId,
525    pub sub: AgentId,
526    /// Origin of the receiving service: scheme, host, and non-default port
527    /// with no path. All Agent Protocols endpoints on one origin share this
528    /// value; derive it with [`service_origin`].
529    pub aud: String,
530    pub iat: i64,
531    pub exp: i64,
532}
533
534#[derive(Clone, Debug, PartialEq, Eq)]
535pub struct RequestBinding {
536    pub audience: String,
537}
538
539impl RequestBinding {
540    pub fn new(audience: impl Into<String>) -> Self {
541        Self {
542            audience: audience.into(),
543        }
544    }
545}
546
547impl RequestJwtClaims {
548    pub fn new(agent_id: AgentId, binding: RequestBinding, issued_at: i64, ttl_secs: i64) -> Self {
549        Self {
550            iss: agent_id.clone(),
551            sub: agent_id,
552            aud: binding.audience,
553            iat: issued_at,
554            exp: issued_at + ttl_secs,
555        }
556    }
557}
558
559#[derive(Clone, Debug)]
560pub struct RequestAuthContext {
561    pub audience: String,
562    pub now_secs: i64,
563    pub max_ttl_secs: i64,
564}
565
566impl RequestAuthContext {
567    pub fn new(audience: impl Into<String>) -> Self {
568        let binding = RequestBinding::new(audience);
569        Self {
570            audience: binding.audience,
571            now_secs: unix_secs(),
572            max_ttl_secs: DEFAULT_REQUEST_JWT_TTL_SECS,
573        }
574    }
575}
576
577pub fn verify_request_jwt(token: &str, context: &RequestAuthContext) -> Result<RequestJwtClaims> {
578    let parts: Vec<&str> = token.split('.').collect();
579    if parts.len() != 3 {
580        return Err(SdkError::InvalidJwt(
581            "expected three compact JWS parts".into(),
582        ));
583    }
584
585    let header: RequestJwtHeader = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0])?)?;
586    let claims: RequestJwtClaims = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1])?)?;
587    let signature_bytes = URL_SAFE_NO_PAD.decode(parts[2])?;
588    let signature_bytes: [u8; 64] = signature_bytes
589        .try_into()
590        .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
591    let signature = Signature::from_bytes(&signature_bytes);
592    let signing_input = format!("{}.{}", parts[0], parts[1]);
593
594    if header.alg != "EdDSA" {
595        return Err(SdkError::InvalidJwtClaim("alg"));
596    }
597    if header.typ != "JWT" {
598        return Err(SdkError::InvalidJwtClaim("typ"));
599    }
600    if header.kid != claims.iss || claims.iss != claims.sub {
601        return Err(SdkError::InvalidJwtClaim("kid/iss/sub"));
602    }
603
604    header
605        .kid
606        .verifying_key()?
607        .verify_strict(signing_input.as_bytes(), &signature)?;
608
609    if claims.aud != context.audience {
610        return Err(SdkError::InvalidJwtClaim("aud"));
611    }
612    if claims.exp <= claims.iat {
613        return Err(SdkError::InvalidJwtClaim("exp/iat"));
614    }
615    if claims.iat > context.now_secs || claims.exp < context.now_secs {
616        return Err(SdkError::InvalidJwtClaim("iat/exp"));
617    }
618    if claims.exp - claims.iat > context.max_ttl_secs {
619        return Err(SdkError::InvalidJwtClaim("ttl"));
620    }
621
622    Ok(claims)
623}
624
625/// Derives the request JWT `aud` from a request URL: the service origin —
626/// scheme, host, and non-default port, with no path (Agent Identity Section 7).
627pub fn service_origin(url: &str) -> Result<String> {
628    let parsed = url::Url::parse(url)
629        .map_err(|_| SdkError::InvalidPayload(format!("not a valid URL: {url}")))?;
630    if parsed.scheme() != "https" && parsed.scheme() != "http" {
631        return Err(SdkError::InvalidPayload(format!(
632            "not an HTTP(S) URL: {url}"
633        )));
634    }
635    Ok(parsed.origin().ascii_serialization())
636}
637
638pub fn unix_ms() -> i64 {
639    let duration = SystemTime::now()
640        .duration_since(UNIX_EPOCH)
641        .unwrap_or_default();
642    duration.as_millis().min(i64::MAX as u128) as i64
643}
644
645pub fn unix_secs() -> i64 {
646    unix_ms() / 1000
647}
648
649#[cfg(test)]
650mod tests {
651    use serde::ser;
652    use serde_json::json;
653
654    use super::*;
655
656    struct FailingPayload;
657
658    impl Serialize for FailingPayload {
659        fn serialize<S>(&self, _serializer: S) -> std::result::Result<S::Ok, S::Error>
660        where
661            S: Serializer,
662        {
663            Err(ser::Error::custom("payload cannot be serialized"))
664        }
665    }
666
667    #[test]
668    fn signs_and_verifies_event_envelope() {
669        let signer = AgentSigner::from_seed([7; 32]);
670        let event = Event::new(
671            "agent-profile/1.0",
672            "profile.update",
673            signer.agent_id(),
674            1_779_753_600_000,
675            1,
676            json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
677        );
678
679        let envelope = signer.sign_event(event).unwrap();
680
681        assert!(!envelope.hash.starts_with("evt_"));
682        assert_eq!(envelope.hash.len(), 43);
683        verify_envelope(&envelope).unwrap();
684    }
685
686    #[test]
687    fn signs_and_verifies_raw_event_hash_bytes() {
688        let seed = [18; 32];
689        let signer = AgentSigner::from_seed(seed);
690        let signing_key = SigningKey::from_bytes(&seed);
691        let event = Event::new(
692            "agent-profile/1.0",
693            "profile.update",
694            signer.agent_id(),
695            1_779_753_600_000,
696            1,
697            json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
698        );
699
700        let digest = event_hash_bytes(&event).unwrap();
701        let signature = sign_event_hash(&signing_key, &digest).unwrap();
702        let envelope = signer.sign_event(event).unwrap();
703
704        assert_eq!(envelope.signature, signature);
705        verify_event_hash_signature(&signing_key.verifying_key(), &digest, &signature).unwrap();
706    }
707
708    #[test]
709    fn rejects_tampered_event_payload() {
710        let signer = AgentSigner::from_seed([8; 32]);
711        let event = Event::new(
712            "agent-profile/1.0",
713            "profile.update",
714            signer.agent_id(),
715            1_779_753_600_000,
716            1,
717            json!({"name": "before"}),
718        );
719        let mut envelope = signer.sign_event(event).unwrap();
720        envelope.event.payload = json!({"name": "after"});
721
722        assert!(verify_envelope(&envelope).is_err());
723    }
724
725    #[test]
726    fn rejects_non_canonical_event_payloads() {
727        let signer = AgentSigner::from_seed([52; 32]);
728        let event = Event::new(
729            "agent-profile/1.0",
730            "profile.update",
731            signer.agent_id(),
732            1000,
733            1,
734            FailingPayload,
735        );
736        assert!(matches!(
737            canonical_event_bytes(&event),
738            Err(SdkError::CanonicalJson(_))
739        ));
740        assert!(matches!(
741            event_hash(&event),
742            Err(SdkError::CanonicalJson(_))
743        ));
744        let envelope = Envelope {
745            hash: "h".to_owned(),
746            event,
747            signature: "s".to_owned(),
748        };
749        assert!(matches!(
750            verify_event_hash(&envelope),
751            Err(SdkError::CanonicalJson(_))
752        ));
753        assert!(matches!(
754            verify_signature(&envelope),
755            Err(SdkError::CanonicalJson(_))
756        ));
757        assert!(matches!(
758            verify_envelope(&envelope),
759            Err(SdkError::CanonicalJson(_))
760        ));
761    }
762
763    #[test]
764    fn rejects_nonce_reuse() {
765        let signer = AgentSigner::from_seed([9; 32]);
766        let options = LiveWriteOptions {
767            now_ms: 1000,
768            window_ms: 1000,
769            nonce_ttl_ms: 1000,
770        };
771        let event = Event::new(
772            "agent-profile/1.0",
773            "profile.update",
774            signer.agent_id(),
775            1000,
776            1,
777            json!({"name": "ResearchAgent"}),
778        );
779        let envelope = signer.sign_event(event).unwrap();
780        let mut store = MemoryNonceStore::new();
781
782        let max_nonce = verify_live_envelope(&envelope, &options, &mut store).unwrap();
783        assert_eq!(max_nonce, 1);
784        assert!(matches!(
785            verify_live_envelope(&envelope, &options, &mut store),
786            Err(SdkError::NonceNotGreater { max_nonce: 1 })
787        ));
788    }
789
790    #[test]
791    fn client_nonce_manager_observes_server_max() {
792        let mut manager = ClientNonceManager::new();
793
794        assert_eq!(manager.next_nonce().unwrap(), 1);
795        manager.observe_max_nonce(5);
796
797        assert_eq!(manager.peek(), 6);
798        manager.observe_max_nonce(4);
799        assert_eq!(manager.peek(), 6);
800        assert_eq!(manager.next_nonce().unwrap(), 6);
801
802        let mut invalid = ClientNonceManager {
803            next_nonce: MAX_SAFE_NONCE + 1,
804        };
805        assert!(matches!(
806            invalid.next_nonce(),
807            Err(SdkError::InvalidNonce(_))
808        ));
809    }
810
811    #[test]
812    fn rejects_nonce_values_outside_safe_json_integer_range() {
813        let signer = AgentSigner::from_seed([16; 32]);
814        let event = Event::new(
815            "agent-profile/1.0",
816            "profile.update",
817            signer.agent_id(),
818            1000,
819            MAX_SAFE_NONCE + 1,
820            json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
821        );
822
823        assert!(matches!(
824            signer.sign_event(event),
825            Err(SdkError::InvalidNonce(_))
826        ));
827    }
828
829    #[test]
830    fn signs_and_verifies_request_jwt() {
831        let signer = AgentSigner::from_seed([10; 32]);
832        let claims = RequestJwtClaims::new(
833            signer.agent_id(),
834            RequestBinding::new("https://api.example.com"),
835            100,
836            300,
837        );
838        let token = signer.sign_request_jwt(&claims).unwrap();
839        let context = RequestAuthContext {
840            audience: "https://api.example.com".to_owned(),
841            now_secs: 120,
842            max_ttl_secs: 300,
843        };
844        let verified = verify_request_jwt(&token, &context).unwrap();
845
846        assert_eq!(verified.iss, signer.agent_id());
847    }
848
849    #[test]
850    fn rejects_request_jwts_with_non_positive_ttl() {
851        let signer = AgentSigner::from_seed([17; 32]);
852        let claims = RequestJwtClaims::new(
853            signer.agent_id(),
854            RequestBinding::new("https://api.example.com"),
855            100,
856            0,
857        );
858        let token = signer.sign_request_jwt(&claims).unwrap();
859        let context = RequestAuthContext {
860            audience: "https://api.example.com".to_owned(),
861            now_secs: 100,
862            max_ttl_secs: 300,
863        };
864
865        assert!(matches!(
866            verify_request_jwt(&token, &context),
867            Err(SdkError::InvalidJwtClaim("exp/iat"))
868        ));
869    }
870
871    #[test]
872    fn agent_id_string_conversions() {
873        let signer = AgentSigner::from_seed([20; 32]);
874        let agent_id = signer.agent_id();
875        let text = agent_id.to_string();
876
877        assert_eq!(agent_id.as_str(), text);
878        assert!(format!("{agent_id:?}").starts_with("AgentId("));
879        assert_eq!(AgentId::try_from(text.clone()).unwrap(), agent_id);
880        assert_eq!(text.parse::<AgentId>().unwrap(), agent_id);
881        assert!(matches!(
882            "not-a-did".parse::<AgentId>(),
883            Err(SdkError::InvalidAgentIdPrefix)
884        ));
885        assert!(matches!(
886            AgentId("not-a-did".to_owned()).public_key_bytes(),
887            Err(SdkError::InvalidAgentIdPrefix)
888        ));
889        assert!(AgentId(format!("{AGENT_ID_PREFIX}%"))
890            .public_key_bytes()
891            .is_err());
892        let short = AgentId(format!(
893            "{}{}",
894            AGENT_ID_PREFIX,
895            URL_SAFE_NO_PAD.encode([1_u8; 31])
896        ));
897        assert!(matches!(
898            short.public_key_bytes(),
899            Err(SdkError::InvalidPublicKeyLength(31))
900        ));
901        assert!(short.verifying_key().is_err());
902        assert!(format!("{}%", AGENT_ID_PREFIX).parse::<AgentId>().is_err());
903    }
904
905    #[test]
906    fn agent_signer_exposes_keys_and_generates() {
907        let signer = AgentSigner::from_seed([21; 32]);
908        assert_eq!(
909            signer.verifying_key().to_bytes(),
910            signer.agent_id().public_key_bytes().unwrap()
911        );
912
913        let generated = AgentSigner::generate();
914        let event = Event::new(
915            "agent-profile/1.0",
916            "profile.update",
917            generated.agent_id(),
918            1000,
919            1,
920            json!({"name": "Generated"}),
921        );
922        verify_envelope(&generated.sign_event(event).unwrap()).unwrap();
923    }
924
925    #[test]
926    fn sign_request_jwt_rejects_foreign_claims() {
927        let signer = AgentSigner::from_seed([22; 32]);
928        let other = AgentSigner::from_seed([23; 32]);
929        let claims = RequestJwtClaims {
930            iss: other.agent_id(),
931            sub: other.agent_id(),
932            aud: "https://api.example.com".to_owned(),
933            iat: 100,
934            exp: 400,
935        };
936        assert!(matches!(
937            signer.sign_request_jwt(&claims),
938            Err(SdkError::InvalidJwtClaim("iss/sub"))
939        ));
940    }
941
942    #[test]
943    fn free_sign_event_matches_signer() {
944        let seed = [24; 32];
945        let signer = AgentSigner::from_seed(seed);
946        let signing_key = SigningKey::from_bytes(&seed);
947        let event = Event::new(
948            "agent-profile/1.0",
949            "profile.update",
950            signer.agent_id(),
951            1000,
952            1,
953            json!({"name": "Agent"}),
954        );
955
956        let signature = sign_event(&signing_key, &event).unwrap();
957        assert_eq!(signer.sign_event(event).unwrap().signature, signature);
958    }
959
960    #[test]
961    fn rejects_event_hash_with_wrong_length() {
962        let signing_key = SigningKey::from_bytes(&[25; 32]);
963        assert!(matches!(
964            sign_event_hash(&signing_key, &[0_u8; 10]),
965            Err(SdkError::InvalidEventHashLength(10))
966        ));
967        assert!(matches!(
968            verify_event_hash_signature(&signing_key.verifying_key(), &[0_u8; 10], "AAAA"),
969            Err(SdkError::InvalidSignatureLength(_))
970        ));
971        let signature = URL_SAFE_NO_PAD.encode([0_u8; 64]);
972        assert!(matches!(
973            verify_event_hash_signature(&signing_key.verifying_key(), &[0_u8; 10], &signature),
974            Err(SdkError::InvalidEventHashLength(10))
975        ));
976    }
977
978    #[test]
979    fn verify_timestamp_rejects_drift_and_negative_window() {
980        verify_timestamp(1000, 1200, 1000).unwrap();
981        assert!(matches!(
982            verify_timestamp(0, 1_000_000, 1000),
983            Err(SdkError::TimestampOutOfWindow)
984        ));
985        assert!(matches!(
986            verify_timestamp(100, 100, -1),
987            Err(SdkError::TimestampOutOfWindow)
988        ));
989    }
990
991    #[test]
992    fn verify_live_envelope_propagates_envelope_and_timestamp_errors() {
993        let signer = AgentSigner::from_seed([53; 32]);
994        let event = Event::new(
995            "agent-profile/1.0",
996            "profile.update",
997            signer.agent_id(),
998            1000,
999            1,
1000            json!({"name": "ResearchAgent"}),
1001        );
1002        let mut envelope = signer.sign_event(event).unwrap();
1003        let mut store = MemoryNonceStore::new();
1004        let options = LiveWriteOptions {
1005            now_ms: 1000,
1006            window_ms: 1000,
1007            nonce_ttl_ms: 1000,
1008        };
1009
1010        envelope.signature = "invalid".to_owned();
1011        assert!(verify_live_envelope(&envelope, &options, &mut store).is_err());
1012
1013        let event = Event::new(
1014            "agent-profile/1.0",
1015            "profile.update",
1016            signer.agent_id(),
1017            0,
1018            2,
1019            json!({"name": "ResearchAgent"}),
1020        );
1021        let envelope = signer.sign_event(event).unwrap();
1022        let expired_options = LiveWriteOptions {
1023            now_ms: 3000,
1024            ..options
1025        };
1026        assert!(matches!(
1027            verify_live_envelope(&envelope, &expired_options, &mut store),
1028            Err(SdkError::TimestampOutOfWindow)
1029        ));
1030    }
1031
1032    #[test]
1033    fn memory_nonce_store_rejects_negative_ttl_and_expires() {
1034        let actor = AgentSigner::from_seed([26; 32]).agent_id();
1035        let mut store = MemoryNonceStore::new();
1036        assert!(matches!(
1037            store.check_and_update(&actor, 1, 1000, -1),
1038            Err(SdkError::InvalidNonce(_))
1039        ));
1040
1041        store.check_and_update(&actor, 4, 1000, 1000).unwrap();
1042        // a strictly greater nonce is accepted while the record is still live
1043        assert_eq!(store.check_and_update(&actor, 5, 1100, 1000).unwrap(), 5);
1044        assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1045        // record has expired by now_ms past expires_at
1046        assert_eq!(store.max_nonce(&actor, 5000), None);
1047        assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1048        // a fresh actor has no record at all
1049        let stranger = AgentSigner::from_seed([27; 32]).agent_id();
1050        assert_eq!(store.max_nonce(&stranger, 1500), None);
1051    }
1052
1053    #[test]
1054    fn client_nonce_manager_with_next_and_header() {
1055        assert!(ClientNonceManager::with_next(0).is_err());
1056        let mut manager = ClientNonceManager::with_next(5).unwrap();
1057        assert_eq!(manager.peek(), 5);
1058
1059        manager.observe_max_nonce_header("10").unwrap();
1060        assert_eq!(manager.peek(), 11);
1061        assert!(matches!(
1062            manager.observe_max_nonce_header("not-a-number"),
1063            Err(SdkError::InvalidNonce(_))
1064        ));
1065        assert!(matches!(
1066            manager.observe_max_nonce_header("9007199254740992"),
1067            Err(SdkError::InvalidNonce(_))
1068        ));
1069    }
1070
1071    #[test]
1072    fn default_options_and_context_use_real_clock() {
1073        let options = LiveWriteOptions::default();
1074        assert_eq!(options.window_ms, DEFAULT_LIVE_WRITE_WINDOW_MS);
1075        assert_eq!(options.nonce_ttl_ms, DEFAULT_NONCE_TTL_MS);
1076        assert!(options.now_ms > 0);
1077
1078        let context = RequestAuthContext::new("https://api.example.com");
1079        assert_eq!(context.audience, "https://api.example.com");
1080        assert_eq!(context.max_ttl_secs, DEFAULT_REQUEST_JWT_TTL_SECS);
1081        assert_eq!(context.now_secs, unix_secs());
1082        assert!(unix_ms() >= context.now_secs * 1000);
1083    }
1084
1085    fn encode_token(header: Value, claims: Value) -> String {
1086        let header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
1087        let claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).unwrap());
1088        let signature = URL_SAFE_NO_PAD.encode([0_u8; 64]);
1089        format!("{header}.{claims}.{signature}")
1090    }
1091
1092    #[test]
1093    fn verify_request_jwt_rejects_malformed_tokens() {
1094        let signer = AgentSigner::from_seed([28; 32]);
1095        let agent_id = signer.agent_id();
1096        let other = AgentSigner::from_seed([29; 32]).agent_id();
1097        let context = RequestAuthContext {
1098            audience: "https://api.example.com".to_owned(),
1099            now_secs: 200,
1100            max_ttl_secs: 300,
1101        };
1102        let valid_claims = json!({
1103            "iss": agent_id,
1104            "sub": agent_id,
1105            "aud": "https://api.example.com",
1106            "iat": 100,
1107            "exp": 400,
1108        });
1109
1110        assert!(matches!(
1111            verify_request_jwt("only.two", &context),
1112            Err(SdkError::InvalidJwt(_))
1113        ));
1114        assert!(verify_request_jwt("%.e30.AAAA", &context).is_err());
1115        let invalid_header = format!(
1116            "{}.{}.{}",
1117            URL_SAFE_NO_PAD.encode(b"not-json"),
1118            URL_SAFE_NO_PAD.encode(serde_json::to_vec(&valid_claims).unwrap()),
1119            URL_SAFE_NO_PAD.encode([0_u8; 64])
1120        );
1121        assert!(verify_request_jwt(&invalid_header, &context).is_err());
1122        let header = URL_SAFE_NO_PAD.encode(
1123            serde_json::to_vec(&json!({"alg": "EdDSA", "typ": "JWT", "kid": agent_id})).unwrap(),
1124        );
1125        let claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&valid_claims).unwrap());
1126        assert!(verify_request_jwt(&format!("{header}.%.AAAA"), &context).is_err());
1127        assert!(verify_request_jwt(&format!("{header}.{claims}.%"), &context).is_err());
1128        assert!(matches!(
1129            verify_request_jwt(&format!("{header}.{claims}.AAAA"), &context),
1130            Err(SdkError::InvalidSignatureLength(_))
1131        ));
1132        assert!(verify_request_jwt(
1133            &format!("{header}.{claims}.{}", URL_SAFE_NO_PAD.encode([0_u8; 64])),
1134            &context
1135        )
1136        .is_err());
1137        let invalid_key = AgentId::from_public_key(&[255_u8; 32]);
1138        assert!(verify_request_jwt(
1139            &encode_token(
1140                json!({"alg": "EdDSA", "typ": "JWT", "kid": invalid_key}),
1141                json!({
1142                    "iss": invalid_key,
1143                    "sub": invalid_key,
1144                    "aud": "https://api.example.com",
1145                    "iat": 100,
1146                    "exp": 400,
1147                }),
1148            ),
1149            &context,
1150        )
1151        .is_err());
1152        assert!(matches!(
1153            verify_request_jwt(
1154                &encode_token(
1155                    json!({"alg": "HS256", "typ": "JWT", "kid": agent_id}),
1156                    valid_claims.clone(),
1157                ),
1158                &context,
1159            ),
1160            Err(SdkError::InvalidJwtClaim("alg"))
1161        ));
1162        assert!(matches!(
1163            verify_request_jwt(
1164                &encode_token(
1165                    json!({"alg": "EdDSA", "typ": "JWS", "kid": agent_id}),
1166                    valid_claims.clone(),
1167                ),
1168                &context,
1169            ),
1170            Err(SdkError::InvalidJwtClaim("typ"))
1171        ));
1172        assert!(matches!(
1173            verify_request_jwt(
1174                &encode_token(
1175                    json!({"alg": "EdDSA", "typ": "JWT", "kid": other}),
1176                    valid_claims,
1177                ),
1178                &context,
1179            ),
1180            Err(SdkError::InvalidJwtClaim("kid/iss/sub"))
1181        ));
1182    }
1183
1184    #[test]
1185    fn rejects_non_canonical_base64url_encodings() {
1186        let signer = AgentSigner::from_seed([62; 32]);
1187        let agent_id = signer.agent_id();
1188        let suffix = agent_id
1189            .as_str()
1190            .strip_prefix(AGENT_ID_PREFIX)
1191            .unwrap()
1192            .to_owned();
1193
1194        // Padding characters are rejected even when the decoded bytes match.
1195        let padded = AgentId(format!("{AGENT_ID_PREFIX}{suffix}=="));
1196        assert!(padded.public_key_bytes().is_err());
1197
1198        // Non-zero trailing bits give the same key a second string form:
1199        // replace the final character with one sharing its used bits but
1200        // different trailing bits (the last char of a 32-byte value uses 4 of
1201        // its 6 bits).
1202        const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1203        let last = suffix.as_bytes()[suffix.len() - 1];
1204        let index = ALPHABET.iter().position(|&c| c == last).unwrap();
1205        let non_canonical = ALPHABET[(index & !3) | ((index & 3) ^ 1)] as char;
1206        let mut tweaked = suffix[..suffix.len() - 1].to_owned();
1207        tweaked.push(non_canonical);
1208        let tweaked = AgentId(format!("{AGENT_ID_PREFIX}{tweaked}"));
1209        assert!(tweaked.public_key_bytes().is_err());
1210
1211        // Signatures must be canonical base64url too.
1212        let event = Event::new(
1213            "agent-profile/1.0",
1214            "profile.update",
1215            signer.agent_id(),
1216            1000,
1217            1,
1218            json!({"name": "Agent"}),
1219        );
1220        let mut envelope = signer.sign_event(event).unwrap();
1221        envelope.signature.push('=');
1222        assert!(verify_signature(&envelope).is_err());
1223    }
1224
1225    #[test]
1226    fn service_origin_derives_request_jwt_audience() {
1227        assert_eq!(
1228            service_origin("https://api.example.com/v1/rooms/room1").unwrap(),
1229            "https://api.example.com"
1230        );
1231        assert_eq!(
1232            service_origin("https://api.example.com:8443/path?q=1").unwrap(),
1233            "https://api.example.com:8443"
1234        );
1235        assert_eq!(
1236            service_origin("https://API.Example.com:443/").unwrap(),
1237            "https://api.example.com"
1238        );
1239        assert!(service_origin("not a url").is_err());
1240        assert!(service_origin("ftp://example.com").is_err());
1241    }
1242
1243    #[test]
1244    fn verify_request_jwt_enforces_audience_and_window() {
1245        let signer = AgentSigner::from_seed([30; 32]);
1246
1247        let wrong_audience = signer
1248            .sign_request_jwt(&RequestJwtClaims::new(
1249                signer.agent_id(),
1250                RequestBinding::new("https://api.example.com"),
1251                100,
1252                300,
1253            ))
1254            .unwrap();
1255        let context = RequestAuthContext {
1256            audience: "https://other.example.com".to_owned(),
1257            now_secs: 200,
1258            max_ttl_secs: 300,
1259        };
1260        assert!(matches!(
1261            verify_request_jwt(&wrong_audience, &context),
1262            Err(SdkError::InvalidJwtClaim("aud"))
1263        ));
1264
1265        let expired = signer
1266            .sign_request_jwt(&RequestJwtClaims::new(
1267                signer.agent_id(),
1268                RequestBinding::new("https://api.example.com"),
1269                1000,
1270                300,
1271            ))
1272            .unwrap();
1273        let later = RequestAuthContext {
1274            audience: "https://api.example.com".to_owned(),
1275            now_secs: 5000,
1276            max_ttl_secs: 300,
1277        };
1278        assert!(matches!(
1279            verify_request_jwt(&expired, &later),
1280            Err(SdkError::InvalidJwtClaim("iat/exp"))
1281        ));
1282
1283        let long_ttl = signer
1284            .sign_request_jwt(&RequestJwtClaims::new(
1285                signer.agent_id(),
1286                RequestBinding::new("https://api.example.com"),
1287                100,
1288                400,
1289            ))
1290            .unwrap();
1291        let strict = RequestAuthContext {
1292            audience: "https://api.example.com".to_owned(),
1293            now_secs: 200,
1294            max_ttl_secs: 300,
1295        };
1296        assert!(matches!(
1297            verify_request_jwt(&long_ttl, &strict),
1298            Err(SdkError::InvalidJwtClaim("ttl"))
1299        ));
1300    }
1301}