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