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