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