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 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 assert_eq!(store.check_and_update(&actor, 5, 1100, 1000).unwrap(), 5);
1021 assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1022 assert_eq!(store.max_nonce(&actor, 5000), None);
1024 assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1025 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}