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 = 2 * DEFAULT_LIVE_WRITE_WINDOW_MS;
21pub const DEFAULT_REQUEST_JWT_TTL_SECS: i64 = 300;
22pub const MAX_NONCE_HEADER: &str = "Max-Seen-Nonce";
23pub const MAX_SAFE_NONCE: u64 = 0x1FFFFFFFFFFFFF;
24pub const MAX_NONCE_JUMP: u64 = 1 << 32;
29
30pub const IDENTITY_EVENT_FIELDS: [&str; 6] = [
32 "protocol",
33 "type",
34 "actor",
35 "created_at",
36 "nonce",
37 "payload",
38];
39
40pub const SHARED_ERROR_CODES: [&str; 12] = [
42 "invalid_request",
43 "invalid_event",
44 "invalid_event_hash",
45 "invalid_signature",
46 "invalid_actor",
47 "timestamp_out_of_window",
48 "nonce_not_greater",
49 "invalid_token",
50 "permission_denied",
51 "not_found",
52 "rate_limited",
53 "payload_too_large",
54];
55
56#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
57pub struct AgentId(String);
58
59impl AgentId {
60 pub fn from_public_key(public_key: &[u8; 32]) -> Self {
61 Self(format!(
62 "{}{}",
63 AGENT_ID_PREFIX,
64 URL_SAFE_NO_PAD.encode(public_key)
65 ))
66 }
67
68 pub fn as_str(&self) -> &str {
69 &self.0
70 }
71
72 pub fn public_key_bytes(&self) -> Result<[u8; 32]> {
73 let encoded = self
74 .0
75 .strip_prefix(AGENT_ID_PREFIX)
76 .ok_or(SdkError::InvalidAgentIdPrefix)?;
77 let bytes = URL_SAFE_NO_PAD.decode(encoded)?;
80 bytes
81 .try_into()
82 .map_err(|bytes: Vec<u8>| SdkError::InvalidPublicKeyLength(bytes.len()))
83 }
84
85 pub fn verifying_key(&self) -> Result<VerifyingKey> {
86 Ok(VerifyingKey::from_bytes(&self.public_key_bytes()?)?)
87 }
88}
89
90impl fmt::Debug for AgentId {
91 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
92 formatter.debug_tuple("AgentId").field(&self.0).finish()
93 }
94}
95
96impl fmt::Display for AgentId {
97 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
98 formatter.write_str(&self.0)
99 }
100}
101
102impl FromStr for AgentId {
103 type Err = SdkError;
104
105 fn from_str(value: &str) -> Result<Self> {
106 if !value.starts_with(AGENT_ID_PREFIX) {
107 return Err(SdkError::InvalidAgentIdPrefix);
108 }
109 let agent_id = Self(value.to_owned());
110 agent_id.public_key_bytes()?;
111 Ok(agent_id)
112 }
113}
114
115impl TryFrom<String> for AgentId {
116 type Error = SdkError;
117
118 fn try_from(value: String) -> Result<Self> {
119 Self::from_str(&value)
120 }
121}
122
123impl Serialize for AgentId {
124 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
125 where
126 S: Serializer,
127 {
128 serializer.serialize_str(&self.0)
129 }
130}
131
132impl<'de> Deserialize<'de> for AgentId {
133 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
134 where
135 D: Deserializer<'de>,
136 {
137 let value = String::deserialize(deserializer)?;
138 Self::from_str(&value).map_err(D::Error::custom)
139 }
140}
141
142pub struct AgentSigner {
143 signing_key: SigningKey,
144}
145
146impl AgentSigner {
147 pub fn generate() -> Self {
148 let mut seed = [0_u8; 32];
149 rand::fill(&mut seed);
150 Self::from_seed(seed)
151 }
152
153 pub fn from_seed(seed: [u8; 32]) -> Self {
154 Self {
155 signing_key: SigningKey::from_bytes(&seed),
156 }
157 }
158
159 pub fn agent_id(&self) -> AgentId {
160 AgentId::from_public_key(&self.signing_key.verifying_key().to_bytes())
161 }
162
163 pub fn verifying_key(&self) -> VerifyingKey {
164 self.signing_key.verifying_key()
165 }
166
167 pub fn sign_event<P>(&self, event: Event<P>) -> Result<Envelope<P>>
168 where
169 P: Serialize,
170 {
171 let event_hash = event_hash_bytes(&event)?;
172 let hash = URL_SAFE_NO_PAD.encode(event_hash);
173 let signature = sign_event_hash(&self.signing_key, &event_hash)
174 .expect("event hashes are always 32 bytes");
175 Ok(Envelope {
176 hash,
177 event,
178 signature,
179 })
180 }
181
182 pub fn sign_request_jwt(&self, claims: &RequestJwtClaims) -> Result<String> {
183 let agent_id = self.agent_id();
184 if claims.iss != agent_id || claims.sub != agent_id {
185 return Err(SdkError::InvalidJwtClaim("iss/sub"));
186 }
187
188 let header = RequestJwtHeader {
189 alg: "EdDSA".to_owned(),
190 typ: "JWT".to_owned(),
191 kid: agent_id,
192 };
193 let header =
194 URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).expect("JWT header is JSON-safe"));
195 let payload =
196 URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims).expect("JWT claims are JSON-safe"));
197 let signing_input = format!("{header}.{payload}");
198 let signature = self.signing_key.sign(signing_input.as_bytes());
199 Ok(format!(
200 "{signing_input}.{}",
201 URL_SAFE_NO_PAD.encode(signature.to_bytes())
202 ))
203 }
204}
205
206#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
207pub struct Event<P = Value> {
208 pub protocol: String,
209 #[serde(rename = "type")]
210 pub kind: String,
211 pub actor: AgentId,
212 pub created_at: i64,
213 pub nonce: u64,
214 #[serde(skip_serializing_if = "Option::is_none")]
215 pub room_id: Option<String>,
216 #[serde(skip_serializing_if = "Option::is_none")]
217 pub base_seq: Option<u64>,
218 #[serde(skip_serializing_if = "Option::is_none")]
219 pub base_hash: Option<String>,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub mentions: Option<Vec<AgentId>>,
224 pub payload: P,
225 #[serde(default, flatten)]
228 pub extra: BTreeMap<String, Value>,
229}
230
231impl<P> Event<P> {
232 pub fn new(
233 protocol: impl Into<String>,
234 kind: impl Into<String>,
235 actor: AgentId,
236 created_at: i64,
237 nonce: u64,
238 payload: P,
239 ) -> Self {
240 Self {
241 protocol: protocol.into(),
242 kind: kind.into(),
243 actor,
244 created_at,
245 nonce,
246 room_id: None,
247 base_seq: None,
248 base_hash: None,
249 mentions: None,
250 payload,
251 extra: BTreeMap::new(),
252 }
253 }
254
255 pub fn with_room_id(mut self, room_id: impl Into<String>) -> Self {
256 self.room_id = Some(room_id.into());
257 self
258 }
259
260 pub fn with_room_head(mut self, base_seq: u64, base_hash: impl Into<String>) -> Self {
261 self.base_seq = Some(base_seq);
262 self.base_hash = Some(base_hash.into());
263 self
264 }
265
266 pub fn with_mentions(mut self, mentions: Vec<AgentId>) -> Self {
267 self.mentions = Some(mentions);
268 self
269 }
270
271 pub fn with_mention(mut self, agent_id: AgentId) -> Self {
272 self.mentions.get_or_insert_with(Vec::new).push(agent_id);
273 self
274 }
275
276 pub fn mentions(&self) -> &[AgentId] {
278 self.mentions.as_deref().unwrap_or(&[])
279 }
280}
281
282pub fn validate_event_fields<P>(event: &Event<P>, extra_fields: &[&str]) -> Result<()> {
285 let present = [
286 ("room_id", event.room_id.is_some()),
287 ("base_seq", event.base_seq.is_some()),
288 ("base_hash", event.base_hash.is_some()),
289 ("mentions", event.mentions.is_some()),
290 ];
291 for (field, is_present) in present {
292 if is_present && !extra_fields.contains(&field) {
293 return Err(SdkError::protocol(
294 "invalid_event",
295 format!("unknown event field: {field}"),
296 ));
297 }
298 }
299 if let Some(field) = event
300 .extra
301 .keys()
302 .find(|key| !extra_fields.contains(&key.as_str()))
303 {
304 return Err(SdkError::protocol(
305 "invalid_event",
306 format!("unknown event field: {field}"),
307 ));
308 }
309 Ok(())
310}
311
312#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
314pub struct ErrorResponse {
315 pub error: ErrorBody,
316}
317
318#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
319pub struct ErrorBody {
320 pub code: String,
321 pub message: String,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
323 pub data: Option<Value>,
324}
325
326#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
329pub struct ListResponse<T> {
330 pub result: Vec<T>,
331 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub next_cursor: Option<String>,
333}
334
335#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
337pub struct AcceptedRecord<P = Value> {
338 pub envelope: Envelope<P>,
339 pub accepted_at: i64,
340}
341
342#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
345pub struct DiscoveryDocument {
346 pub protocol: String,
347 pub service: String,
348 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
349 pub endpoints: BTreeMap<String, String>,
350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
351 pub features: Vec<String>,
352 #[serde(default, flatten)]
353 pub extra: BTreeMap<String, Value>,
354}
355
356#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
357pub struct Envelope<P = Value> {
358 pub hash: String,
359 pub event: Event<P>,
360 pub signature: String,
361}
362
363pub fn canonical_event_bytes<P>(event: &Event<P>) -> Result<Vec<u8>>
364where
365 P: Serialize,
366{
367 serde_jcs::to_vec(event).map_err(|err| SdkError::CanonicalJson(err.to_string()))
368}
369
370pub fn event_hash<P>(event: &Event<P>) -> Result<String>
371where
372 P: Serialize,
373{
374 Ok(URL_SAFE_NO_PAD.encode(event_hash_bytes(event)?))
375}
376
377pub fn event_hash_bytes<P>(event: &Event<P>) -> Result<[u8; 32]>
378where
379 P: Serialize,
380{
381 validate_nonce(event.nonce)?;
382 let digest = Sha3_256::digest(canonical_event_bytes(event)?);
383 let mut bytes = [0_u8; 32];
384 bytes.copy_from_slice(&digest);
385 Ok(bytes)
386}
387
388pub fn sign_event<P>(signing_key: &SigningKey, event: &Event<P>) -> Result<String>
389where
390 P: Serialize,
391{
392 let event_hash = event_hash_bytes(event)?;
393 Ok(sign_event_hash(signing_key, &event_hash).expect("event hashes are always 32 bytes"))
394}
395
396pub fn sign_event_hash(signing_key: &SigningKey, event_hash: &[u8]) -> Result<String> {
402 let signature = signing_key.sign(valid_event_hash_bytes(event_hash)?);
403 Ok(URL_SAFE_NO_PAD.encode(signature.to_bytes()))
404}
405
406pub fn verify_event_hash<P>(envelope: &Envelope<P>) -> Result<()>
407where
408 P: Serialize,
409{
410 let expected = event_hash(&envelope.event)?;
411 if expected == envelope.hash {
412 Ok(())
413 } else {
414 Err(SdkError::InvalidEventHash {
415 expected,
416 actual: envelope.hash.clone(),
417 })
418 }
419}
420
421pub fn verify_signature<P>(envelope: &Envelope<P>) -> Result<()>
422where
423 P: Serialize,
424{
425 let event_hash = event_hash_bytes(&envelope.event)?;
426 let verifying_key = envelope.event.actor.verifying_key()?;
427 verify_event_hash_signature(&verifying_key, &event_hash, &envelope.signature)
428}
429
430pub fn verify_event_hash_signature(
431 verifying_key: &VerifyingKey,
432 event_hash: &[u8],
433 encoded_signature: &str,
434) -> Result<()> {
435 let signature_bytes = URL_SAFE_NO_PAD.decode(encoded_signature)?;
436 let signature_bytes: [u8; 64] = signature_bytes
437 .try_into()
438 .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
439 verify_ed25519_strict(
440 verifying_key,
441 valid_event_hash_bytes(event_hash)?,
442 &signature_bytes,
443 )
444}
445
446const FIELD_P_LE: [u8; 32] = [
448 0xed, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
449 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f,
450];
451const GROUP_L_LE: [u8; 32] = [
453 0xed, 0xd3, 0xf5, 0x5c, 0x1a, 0x63, 0x12, 0x58, 0xd6, 0x9c, 0xf7, 0xa2, 0xde, 0xf9, 0xde, 0x14,
454 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10,
455];
456const SMALL_ORDER_Y: [[u8; 32]; 5] = [
458 [0; 32],
459 [
460 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
461 0, 0, 0,
462 ],
463 [
464 0xec, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
465 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
466 0xff, 0x7f,
467 ],
468 [
469 0x26, 0xe8, 0x95, 0x8f, 0xc2, 0xb2, 0x27, 0xb0, 0x45, 0xc3, 0xf4, 0x89, 0xf2, 0xef, 0x98,
470 0xf0, 0xd5, 0xdf, 0xac, 0x05, 0xd3, 0xc6, 0x33, 0x39, 0xb1, 0x38, 0x02, 0x88, 0x6d, 0x53,
471 0xfc, 0x05,
472 ],
473 [
474 0xc7, 0x17, 0x6a, 0x70, 0x3d, 0x4d, 0xd8, 0x4f, 0xba, 0x3c, 0x0b, 0x76, 0x0d, 0x10, 0x67,
475 0x0f, 0x2a, 0x20, 0x53, 0xfa, 0x2c, 0x39, 0xcc, 0xc6, 0x4e, 0xc7, 0xfd, 0x77, 0x92, 0xac,
476 0x03, 0x7a,
477 ],
478];
479
480fn less_than_le(a: &[u8; 32], b: &[u8; 32]) -> bool {
482 for i in (0..32).rev() {
483 if a[i] != b[i] {
484 return a[i] < b[i];
485 }
486 }
487 false
488}
489
490fn is_strict_point(encoding: &[u8; 32]) -> bool {
492 let mut y = *encoding;
493 y[31] &= 0x7f;
494 less_than_le(&y, &FIELD_P_LE) && !SMALL_ORDER_Y.contains(&y)
495}
496
497pub fn verify_ed25519_strict(
501 verifying_key: &VerifyingKey,
502 message: &[u8],
503 signature: &[u8; 64],
504) -> Result<()> {
505 let failed = || SdkError::protocol("invalid_signature", "signature verification failed");
506 let r: [u8; 32] = signature[..32].try_into().expect("64-byte signature");
507 let s: [u8; 32] = signature[32..].try_into().expect("64-byte signature");
508 if !is_strict_point(verifying_key.as_bytes()) || !is_strict_point(&r) {
509 return Err(failed());
510 }
511 if !less_than_le(&s, &GROUP_L_LE) {
512 return Err(failed());
513 }
514 verifying_key
515 .verify_strict(message, &Signature::from_bytes(signature))
516 .map_err(|_| failed())
517}
518
519pub fn verify_ed25519_strict_bytes(
522 public_key: &[u8; 32],
523 message: &[u8],
524 signature: &[u8; 64],
525) -> Result<()> {
526 let failed = || SdkError::protocol("invalid_signature", "signature verification failed");
527 if !is_strict_point(public_key) {
528 return Err(failed());
529 }
530 let key = VerifyingKey::from_bytes(public_key).map_err(|_| failed())?;
531 verify_ed25519_strict(&key, message, signature)
532}
533
534pub fn parse_strict_json(text: &str) -> Result<Value> {
539 let mut deserializer = serde_json::Deserializer::from_str(text);
540 let value = StrictJson::deserialize(&mut deserializer)
541 .map_err(|err| SdkError::protocol("invalid_event", format!("invalid JSON: {err}")))?;
542 deserializer
543 .end()
544 .map_err(|err| SdkError::protocol("invalid_event", format!("invalid JSON: {err}")))?;
545 Ok(value.0)
546}
547
548pub fn parse_envelope_json<P>(text: &str) -> Result<Envelope<P>>
550where
551 P: serde::de::DeserializeOwned,
552{
553 let value = parse_strict_json(text)?;
554 let object = value.as_object().ok_or_else(|| {
555 SdkError::protocol(
556 "invalid_event",
557 "envelope must contain hash, event, and signature",
558 )
559 })?;
560 if let Some(key) = object
561 .keys()
562 .find(|key| !matches!(key.as_str(), "hash" | "event" | "signature"))
563 {
564 return Err(SdkError::protocol(
565 "invalid_event",
566 format!("unknown envelope field: {key}"),
567 ));
568 }
569 serde_json::from_value(value)
570 .map_err(|err| SdkError::protocol("invalid_event", format!("invalid envelope: {err}")))
571}
572
573struct StrictJson(Value);
574
575impl<'de> Deserialize<'de> for StrictJson {
576 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
577 where
578 D: Deserializer<'de>,
579 {
580 deserializer
581 .deserialize_any(StrictJsonVisitor)
582 .map(StrictJson)
583 }
584}
585
586struct StrictJsonVisitor;
587
588impl<'de> serde::de::Visitor<'de> for StrictJsonVisitor {
589 type Value = Value;
590
591 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
592 formatter.write_str("I-JSON")
593 }
594
595 fn visit_bool<E>(self, value: bool) -> std::result::Result<Value, E> {
596 Ok(Value::Bool(value))
597 }
598
599 fn visit_i64<E: DeError>(self, value: i64) -> std::result::Result<Value, E> {
600 if value.unsigned_abs() > MAX_SAFE_NONCE {
601 return Err(E::custom("integer outside the safe range"));
602 }
603 Ok(Value::from(value))
604 }
605
606 fn visit_u64<E: DeError>(self, value: u64) -> std::result::Result<Value, E> {
607 if value > MAX_SAFE_NONCE {
608 return Err(E::custom("integer outside the safe range"));
609 }
610 Ok(Value::from(value))
611 }
612
613 fn visit_f64<E: DeError>(self, value: f64) -> std::result::Result<Value, E> {
614 if value.fract() == 0.0 && value.abs() > MAX_SAFE_NONCE as f64 {
615 return Err(E::custom("integer outside the safe range"));
616 }
617 serde_json::Number::from_f64(value)
618 .map(Value::Number)
619 .ok_or_else(|| E::custom("number out of range"))
620 }
621
622 fn visit_str<E>(self, value: &str) -> std::result::Result<Value, E> {
623 Ok(Value::String(value.to_owned()))
624 }
625
626 fn visit_string<E>(self, value: String) -> std::result::Result<Value, E> {
627 Ok(Value::String(value))
628 }
629
630 fn visit_unit<E>(self) -> std::result::Result<Value, E> {
631 Ok(Value::Null)
632 }
633
634 fn visit_seq<A>(self, mut seq: A) -> std::result::Result<Value, A::Error>
635 where
636 A: serde::de::SeqAccess<'de>,
637 {
638 let mut values = Vec::new();
639 while let Some(StrictJson(value)) = seq.next_element()? {
640 values.push(value);
641 }
642 Ok(Value::Array(values))
643 }
644
645 fn visit_map<A>(self, mut map: A) -> std::result::Result<Value, A::Error>
646 where
647 A: serde::de::MapAccess<'de>,
648 {
649 let mut object = serde_json::Map::new();
650 while let Some(key) = map.next_key::<String>()? {
651 if object.contains_key(&key) {
652 return Err(A::Error::custom(format!("duplicate member name {key:?}")));
653 }
654 let StrictJson(value) = map.next_value()?;
655 object.insert(key, value);
656 }
657 Ok(Value::Object(object))
658 }
659}
660
661fn valid_event_hash_bytes(event_hash: &[u8]) -> Result<&[u8; 32]> {
662 if event_hash.len() != 32 {
663 return Err(SdkError::InvalidEventHashLength(event_hash.len()));
664 }
665 Ok(event_hash
666 .try_into()
667 .expect("event hash length was checked above"))
668}
669
670pub fn verify_envelope<P>(envelope: &Envelope<P>) -> Result<()>
671where
672 P: Serialize,
673{
674 verify_event_hash(envelope)?;
675 verify_signature(envelope)
676}
677
678pub fn verify_timestamp(created_at: i64, now_ms: i64, window_ms: i64) -> Result<()> {
679 if window_ms < 0 || created_at.abs_diff(now_ms) > window_ms as u64 {
680 Err(SdkError::TimestampOutOfWindow)
681 } else {
682 Ok(())
683 }
684}
685
686#[derive(Clone, Copy, Debug, Eq, PartialEq)]
687pub struct NonceRecord {
688 pub max_nonce: u64,
689 pub expires_at: i64,
690}
691
692pub trait NonceStore {
693 fn check_and_update(
694 &mut self,
695 actor: &AgentId,
696 nonce: u64,
697 now_ms: i64,
698 ttl_ms: i64,
699 ) -> Result<u64>;
700
701 fn max_nonce(&self, actor: &AgentId, now_ms: i64) -> Option<u64>;
702}
703
704#[derive(Clone, Debug, Default)]
705pub struct MemoryNonceStore {
706 records: HashMap<AgentId, NonceRecord>,
707}
708
709impl MemoryNonceStore {
710 pub fn new() -> Self {
711 Self::default()
712 }
713}
714
715impl NonceStore for MemoryNonceStore {
716 fn check_and_update(
717 &mut self,
718 actor: &AgentId,
719 nonce: u64,
720 now_ms: i64,
721 ttl_ms: i64,
722 ) -> Result<u64> {
723 validate_nonce(nonce)?;
724 if ttl_ms < 0 {
725 return Err(SdkError::InvalidNonce(
726 "nonce cache ttl must be non-negative".to_owned(),
727 ));
728 }
729
730 if let Some(record) = self.records.get(actor) {
731 if record.expires_at > now_ms && nonce <= record.max_nonce {
732 return Err(SdkError::NonceNotGreater {
733 max_nonce: record.max_nonce,
734 });
735 }
736 }
737
738 let record = NonceRecord {
739 max_nonce: nonce,
740 expires_at: now_ms.saturating_add(ttl_ms),
741 };
742 self.records.insert(actor.clone(), record);
743 Ok(record.max_nonce)
744 }
745
746 fn max_nonce(&self, actor: &AgentId, now_ms: i64) -> Option<u64> {
747 self.records
748 .get(actor)
749 .filter(|record| record.expires_at > now_ms)
750 .map(|record| record.max_nonce)
751 }
752}
753
754#[derive(Clone, Debug, Eq, PartialEq)]
755pub struct ClientNonceManager {
756 next_nonce: u64,
757}
758
759impl Default for ClientNonceManager {
760 fn default() -> Self {
761 Self { next_nonce: 1 }
762 }
763}
764
765impl ClientNonceManager {
766 pub fn new() -> Self {
767 Self::default()
768 }
769
770 pub fn with_next(next_nonce: u64) -> Result<Self> {
771 validate_nonce(next_nonce)?;
772 Ok(Self { next_nonce })
773 }
774
775 pub fn peek(&self) -> u64 {
776 self.next_nonce
777 }
778
779 pub fn next_nonce(&mut self) -> Result<u64> {
780 let nonce = self.next_nonce;
781 validate_nonce(nonce)?;
782 self.next_nonce += 1;
783 Ok(nonce)
784 }
785
786 pub fn next_nonce_at(&mut self, created_at: i64) -> Result<u64> {
789 let clock = u64::try_from(created_at).unwrap_or(0);
790 if clock > self.next_nonce {
791 self.next_nonce = clock;
792 }
793 self.next_nonce()
794 }
795
796 pub fn observe_max_nonce(&mut self, max_nonce: u64, now_ms: i64) -> Result<()> {
799 validate_nonce(max_nonce)?;
800 let floor = self.next_nonce.max(u64::try_from(now_ms).unwrap_or(0));
801 if max_nonce > floor.saturating_add(MAX_NONCE_JUMP) {
802 return Err(SdkError::InvalidNonce(format!(
803 "Max-Seen-Nonce {max_nonce} jumps too far beyond the local sequence"
804 )));
805 }
806 if max_nonce >= self.next_nonce {
807 self.next_nonce = max_nonce.saturating_add(1);
808 }
809 Ok(())
810 }
811
812 pub fn observe_max_nonce_header(&mut self, value: &str, now_ms: i64) -> Result<()> {
813 let max_nonce = value
814 .parse::<u64>()
815 .map_err(|_| SdkError::InvalidNonce("invalid max nonce header".to_owned()))?;
816 self.observe_max_nonce(max_nonce, now_ms)
817 }
818}
819
820pub fn validate_nonce(nonce: u64) -> Result<()> {
821 if nonce == 0 || nonce > MAX_SAFE_NONCE {
822 Err(SdkError::InvalidNonce(
824 "nonce must be a positive integer less than or equal to 9007199254740991".to_owned(),
825 ))
826 } else {
827 Ok(())
828 }
829}
830
831#[derive(Clone, Debug)]
832pub struct LiveWriteOptions {
833 pub now_ms: i64,
834 pub window_ms: i64,
835 pub nonce_ttl_ms: i64,
836}
837
838impl Default for LiveWriteOptions {
839 fn default() -> Self {
840 Self {
841 now_ms: unix_ms(),
842 window_ms: DEFAULT_LIVE_WRITE_WINDOW_MS,
843 nonce_ttl_ms: DEFAULT_NONCE_TTL_MS,
844 }
845 }
846}
847
848#[derive(Clone, Copy, Debug, Eq, PartialEq)]
851pub enum SubmissionResult {
852 Resubmission,
853 Accepted { max_nonce: u64 },
854}
855
856pub fn verify_submission<P, S, F>(
860 envelope: &Envelope<P>,
861 options: &LiveWriteOptions,
862 nonce_store: &mut S,
863 is_accepted: F,
864) -> Result<SubmissionResult>
865where
866 P: Serialize,
867 S: NonceStore,
868 F: Fn(&str) -> bool,
869{
870 verify_envelope(envelope)?;
871 if is_accepted(&envelope.hash) {
872 return Ok(SubmissionResult::Resubmission);
873 }
874 verify_timestamp(envelope.event.created_at, options.now_ms, options.window_ms)?;
875 let max_nonce = nonce_store.check_and_update(
876 &envelope.event.actor,
877 envelope.event.nonce,
878 options.now_ms,
879 options.nonce_ttl_ms,
880 )?;
881 Ok(SubmissionResult::Accepted { max_nonce })
882}
883
884pub fn verify_live_envelope<P, S>(
885 envelope: &Envelope<P>,
886 options: &LiveWriteOptions,
887 nonce_store: &mut S,
888) -> Result<u64>
889where
890 P: Serialize,
891 S: NonceStore,
892{
893 verify_envelope(envelope)?;
894 verify_timestamp(envelope.event.created_at, options.now_ms, options.window_ms)?;
895 nonce_store.check_and_update(
896 &envelope.event.actor,
897 envelope.event.nonce,
898 options.now_ms,
899 options.nonce_ttl_ms,
900 )
901}
902
903#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
904pub struct RequestJwtHeader {
905 pub alg: String,
906 pub typ: String,
907 pub kid: AgentId,
908}
909
910#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
911pub struct RequestJwtClaims {
912 pub iss: AgentId,
913 pub sub: AgentId,
914 pub aud: String,
918 pub iat: i64,
919 pub exp: i64,
920}
921
922#[derive(Clone, Debug, PartialEq, Eq)]
923pub struct RequestBinding {
924 pub audience: String,
925}
926
927impl RequestBinding {
928 pub fn new(audience: impl Into<String>) -> Self {
929 Self {
930 audience: audience.into(),
931 }
932 }
933}
934
935impl RequestJwtClaims {
936 pub fn new(agent_id: AgentId, binding: RequestBinding, issued_at: i64, ttl_secs: i64) -> Self {
937 Self {
938 iss: agent_id.clone(),
939 sub: agent_id,
940 aud: binding.audience,
941 iat: issued_at,
942 exp: issued_at + ttl_secs,
943 }
944 }
945}
946
947#[derive(Clone, Debug)]
948pub struct RequestAuthContext {
949 pub audience: String,
950 pub now_secs: i64,
951 pub max_ttl_secs: i64,
952}
953
954impl RequestAuthContext {
955 pub fn new(audience: impl Into<String>) -> Self {
956 let binding = RequestBinding::new(audience);
957 Self {
958 audience: binding.audience,
959 now_secs: unix_secs(),
960 max_ttl_secs: DEFAULT_REQUEST_JWT_TTL_SECS,
961 }
962 }
963}
964
965pub fn verify_request_jwt(token: &str, context: &RequestAuthContext) -> Result<RequestJwtClaims> {
966 let parts: Vec<&str> = token.split('.').collect();
967 if parts.len() != 3 {
968 return Err(SdkError::InvalidJwt(
969 "expected three compact JWS parts".into(),
970 ));
971 }
972
973 let header: RequestJwtHeader = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0])?)?;
974 let claims: RequestJwtClaims = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1])?)?;
975 let signature_bytes = URL_SAFE_NO_PAD.decode(parts[2])?;
976 let signature_bytes: [u8; 64] = signature_bytes
977 .try_into()
978 .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
979 let signature = Signature::from_bytes(&signature_bytes);
980 let signing_input = format!("{}.{}", parts[0], parts[1]);
981
982 if header.alg != "EdDSA" {
983 return Err(SdkError::InvalidJwtClaim("alg"));
984 }
985 if header.typ != "JWT" {
986 return Err(SdkError::InvalidJwtClaim("typ"));
987 }
988 if header.kid != claims.iss || claims.iss != claims.sub {
989 return Err(SdkError::InvalidJwtClaim("kid/iss/sub"));
990 }
991
992 verify_ed25519_strict(
993 &header.kid.verifying_key()?,
994 signing_input.as_bytes(),
995 &signature.to_bytes(),
996 )?;
997
998 if claims.aud != context.audience {
999 return Err(SdkError::InvalidJwtClaim("aud"));
1000 }
1001 if claims.exp <= claims.iat {
1002 return Err(SdkError::InvalidJwtClaim("exp/iat"));
1003 }
1004 if claims.iat > context.now_secs || claims.exp < context.now_secs {
1005 return Err(SdkError::InvalidJwtClaim("iat/exp"));
1006 }
1007 if claims.exp - claims.iat > context.max_ttl_secs {
1008 return Err(SdkError::InvalidJwtClaim("ttl"));
1009 }
1010
1011 Ok(claims)
1012}
1013
1014pub fn validate_origin(value: &str) -> Result<()> {
1017 let parsed = url::Url::parse(value)
1018 .map_err(|_| SdkError::InvalidPayload(format!("not a valid origin: {value}")))?;
1019 if parsed.scheme() == "https"
1020 && parsed.host_str().is_some()
1021 && parsed.origin().ascii_serialization() == value
1022 {
1023 Ok(())
1024 } else {
1025 Err(SdkError::InvalidPayload(format!(
1026 "origin must be a serialized HTTPS origin: {value}"
1027 )))
1028 }
1029}
1030
1031pub fn service_origin(url: &str) -> Result<String> {
1034 let parsed = url::Url::parse(url)
1035 .map_err(|_| SdkError::InvalidPayload(format!("not a valid URL: {url}")))?;
1036 if parsed.scheme() != "https" && parsed.scheme() != "http" {
1037 return Err(SdkError::InvalidPayload(format!(
1038 "not an HTTP(S) URL: {url}"
1039 )));
1040 }
1041 Ok(parsed.origin().ascii_serialization())
1042}
1043
1044pub fn unix_ms() -> i64 {
1045 let duration = SystemTime::now()
1046 .duration_since(UNIX_EPOCH)
1047 .unwrap_or_default();
1048 duration.as_millis().min(i64::MAX as u128) as i64
1049}
1050
1051pub fn unix_secs() -> i64 {
1052 unix_ms() / 1000
1053}
1054
1055#[cfg(test)]
1056mod tests {
1057 use serde::ser;
1058 use serde_json::json;
1059
1060 use super::*;
1061
1062 struct FailingPayload;
1063
1064 impl Serialize for FailingPayload {
1065 fn serialize<S>(&self, _serializer: S) -> std::result::Result<S::Ok, S::Error>
1066 where
1067 S: Serializer,
1068 {
1069 Err(ser::Error::custom("payload cannot be serialized"))
1070 }
1071 }
1072
1073 #[test]
1074 fn signs_and_verifies_event_envelope() {
1075 let signer = AgentSigner::from_seed([7; 32]);
1076 let event = Event::new(
1077 "agent-profile/1.0",
1078 "profile.update",
1079 signer.agent_id(),
1080 1_779_753_600_000,
1081 1,
1082 json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1083 );
1084
1085 let envelope = signer.sign_event(event).unwrap();
1086
1087 assert!(!envelope.hash.starts_with("evt_"));
1088 assert_eq!(envelope.hash.len(), 43);
1089 verify_envelope(&envelope).unwrap();
1090 }
1091
1092 #[test]
1093 fn signs_and_verifies_raw_event_hash_bytes() {
1094 let seed = [18; 32];
1095 let signer = AgentSigner::from_seed(seed);
1096 let signing_key = SigningKey::from_bytes(&seed);
1097 let event = Event::new(
1098 "agent-profile/1.0",
1099 "profile.update",
1100 signer.agent_id(),
1101 1_779_753_600_000,
1102 1,
1103 json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1104 );
1105
1106 let digest = event_hash_bytes(&event).unwrap();
1107 let signature = sign_event_hash(&signing_key, &digest).unwrap();
1108 let envelope = signer.sign_event(event).unwrap();
1109
1110 assert_eq!(envelope.signature, signature);
1111 verify_event_hash_signature(&signing_key.verifying_key(), &digest, &signature).unwrap();
1112 }
1113
1114 #[test]
1115 fn rejects_tampered_event_payload() {
1116 let signer = AgentSigner::from_seed([8; 32]);
1117 let event = Event::new(
1118 "agent-profile/1.0",
1119 "profile.update",
1120 signer.agent_id(),
1121 1_779_753_600_000,
1122 1,
1123 json!({"name": "before"}),
1124 );
1125 let mut envelope = signer.sign_event(event).unwrap();
1126 envelope.event.payload = json!({"name": "after"});
1127
1128 assert!(verify_envelope(&envelope).is_err());
1129 }
1130
1131 #[test]
1132 fn rejects_non_canonical_event_payloads() {
1133 let signer = AgentSigner::from_seed([52; 32]);
1134 let event = Event::new(
1135 "agent-profile/1.0",
1136 "profile.update",
1137 signer.agent_id(),
1138 1000,
1139 1,
1140 FailingPayload,
1141 );
1142 assert!(matches!(
1143 canonical_event_bytes(&event),
1144 Err(SdkError::CanonicalJson(_))
1145 ));
1146 assert!(matches!(
1147 event_hash(&event),
1148 Err(SdkError::CanonicalJson(_))
1149 ));
1150 let envelope = Envelope {
1151 hash: "h".to_owned(),
1152 event,
1153 signature: "s".to_owned(),
1154 };
1155 assert!(matches!(
1156 verify_event_hash(&envelope),
1157 Err(SdkError::CanonicalJson(_))
1158 ));
1159 assert!(matches!(
1160 verify_signature(&envelope),
1161 Err(SdkError::CanonicalJson(_))
1162 ));
1163 assert!(matches!(
1164 verify_envelope(&envelope),
1165 Err(SdkError::CanonicalJson(_))
1166 ));
1167 }
1168
1169 #[test]
1170 fn rejects_nonce_reuse() {
1171 let signer = AgentSigner::from_seed([9; 32]);
1172 let options = LiveWriteOptions {
1173 now_ms: 1000,
1174 window_ms: 1000,
1175 nonce_ttl_ms: 1000,
1176 };
1177 let event = Event::new(
1178 "agent-profile/1.0",
1179 "profile.update",
1180 signer.agent_id(),
1181 1000,
1182 1,
1183 json!({"name": "ResearchAgent"}),
1184 );
1185 let envelope = signer.sign_event(event).unwrap();
1186 let mut store = MemoryNonceStore::new();
1187
1188 let max_nonce = verify_live_envelope(&envelope, &options, &mut store).unwrap();
1189 assert_eq!(max_nonce, 1);
1190 assert!(matches!(
1191 verify_live_envelope(&envelope, &options, &mut store),
1192 Err(SdkError::NonceNotGreater { max_nonce: 1 })
1193 ));
1194 }
1195
1196 #[test]
1197 fn client_nonce_manager_observes_server_max() {
1198 let mut manager = ClientNonceManager::new();
1199
1200 assert_eq!(manager.next_nonce().unwrap(), 1);
1201 manager.observe_max_nonce(5, 0).unwrap();
1202
1203 assert_eq!(manager.peek(), 6);
1204 manager.observe_max_nonce(4, 0).unwrap();
1205 assert_eq!(manager.peek(), 6);
1206 assert_eq!(manager.next_nonce().unwrap(), 6);
1207
1208 let mut invalid = ClientNonceManager {
1209 next_nonce: MAX_SAFE_NONCE + 1,
1210 };
1211 assert!(matches!(
1212 invalid.next_nonce(),
1213 Err(SdkError::InvalidNonce(_))
1214 ));
1215 }
1216
1217 #[test]
1218 fn rejects_nonce_values_outside_safe_json_integer_range() {
1219 let signer = AgentSigner::from_seed([16; 32]);
1220 let event = Event::new(
1221 "agent-profile/1.0",
1222 "profile.update",
1223 signer.agent_id(),
1224 1000,
1225 MAX_SAFE_NONCE + 1,
1226 json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1227 );
1228
1229 assert!(matches!(
1230 signer.sign_event(event),
1231 Err(SdkError::InvalidNonce(_))
1232 ));
1233 }
1234
1235 #[test]
1236 fn signs_and_verifies_request_jwt() {
1237 let signer = AgentSigner::from_seed([10; 32]);
1238 let claims = RequestJwtClaims::new(
1239 signer.agent_id(),
1240 RequestBinding::new("https://api.example.com"),
1241 100,
1242 300,
1243 );
1244 let token = signer.sign_request_jwt(&claims).unwrap();
1245 let context = RequestAuthContext {
1246 audience: "https://api.example.com".to_owned(),
1247 now_secs: 120,
1248 max_ttl_secs: 300,
1249 };
1250 let verified = verify_request_jwt(&token, &context).unwrap();
1251
1252 assert_eq!(verified.iss, signer.agent_id());
1253 }
1254
1255 #[test]
1256 fn rejects_request_jwts_with_non_positive_ttl() {
1257 let signer = AgentSigner::from_seed([17; 32]);
1258 let claims = RequestJwtClaims::new(
1259 signer.agent_id(),
1260 RequestBinding::new("https://api.example.com"),
1261 100,
1262 0,
1263 );
1264 let token = signer.sign_request_jwt(&claims).unwrap();
1265 let context = RequestAuthContext {
1266 audience: "https://api.example.com".to_owned(),
1267 now_secs: 100,
1268 max_ttl_secs: 300,
1269 };
1270
1271 assert!(matches!(
1272 verify_request_jwt(&token, &context),
1273 Err(SdkError::InvalidJwtClaim("exp/iat"))
1274 ));
1275 }
1276
1277 #[test]
1278 fn agent_id_string_conversions() {
1279 let signer = AgentSigner::from_seed([20; 32]);
1280 let agent_id = signer.agent_id();
1281 let text = agent_id.to_string();
1282
1283 assert_eq!(agent_id.as_str(), text);
1284 assert!(format!("{agent_id:?}").starts_with("AgentId("));
1285 assert_eq!(AgentId::try_from(text.clone()).unwrap(), agent_id);
1286 assert_eq!(text.parse::<AgentId>().unwrap(), agent_id);
1287 assert!(matches!(
1288 "not-a-did".parse::<AgentId>(),
1289 Err(SdkError::InvalidAgentIdPrefix)
1290 ));
1291 assert!(matches!(
1292 AgentId("not-a-did".to_owned()).public_key_bytes(),
1293 Err(SdkError::InvalidAgentIdPrefix)
1294 ));
1295 assert!(AgentId(format!("{AGENT_ID_PREFIX}%"))
1296 .public_key_bytes()
1297 .is_err());
1298 let short = AgentId(format!(
1299 "{}{}",
1300 AGENT_ID_PREFIX,
1301 URL_SAFE_NO_PAD.encode([1_u8; 31])
1302 ));
1303 assert!(matches!(
1304 short.public_key_bytes(),
1305 Err(SdkError::InvalidPublicKeyLength(31))
1306 ));
1307 assert!(short.verifying_key().is_err());
1308 assert!(format!("{}%", AGENT_ID_PREFIX).parse::<AgentId>().is_err());
1309 }
1310
1311 #[test]
1312 fn agent_signer_exposes_keys_and_generates() {
1313 let signer = AgentSigner::from_seed([21; 32]);
1314 assert_eq!(
1315 signer.verifying_key().to_bytes(),
1316 signer.agent_id().public_key_bytes().unwrap()
1317 );
1318
1319 let generated = AgentSigner::generate();
1320 let event = Event::new(
1321 "agent-profile/1.0",
1322 "profile.update",
1323 generated.agent_id(),
1324 1000,
1325 1,
1326 json!({"name": "Generated"}),
1327 );
1328 verify_envelope(&generated.sign_event(event).unwrap()).unwrap();
1329 }
1330
1331 #[test]
1332 fn sign_request_jwt_rejects_foreign_claims() {
1333 let signer = AgentSigner::from_seed([22; 32]);
1334 let other = AgentSigner::from_seed([23; 32]);
1335 let claims = RequestJwtClaims {
1336 iss: other.agent_id(),
1337 sub: other.agent_id(),
1338 aud: "https://api.example.com".to_owned(),
1339 iat: 100,
1340 exp: 400,
1341 };
1342 assert!(matches!(
1343 signer.sign_request_jwt(&claims),
1344 Err(SdkError::InvalidJwtClaim("iss/sub"))
1345 ));
1346 }
1347
1348 #[test]
1349 fn free_sign_event_matches_signer() {
1350 let seed = [24; 32];
1351 let signer = AgentSigner::from_seed(seed);
1352 let signing_key = SigningKey::from_bytes(&seed);
1353 let event = Event::new(
1354 "agent-profile/1.0",
1355 "profile.update",
1356 signer.agent_id(),
1357 1000,
1358 1,
1359 json!({"name": "Agent"}),
1360 );
1361
1362 let signature = sign_event(&signing_key, &event).unwrap();
1363 assert_eq!(signer.sign_event(event).unwrap().signature, signature);
1364 }
1365
1366 #[test]
1367 fn rejects_event_hash_with_wrong_length() {
1368 let signing_key = SigningKey::from_bytes(&[25; 32]);
1369 assert!(matches!(
1370 sign_event_hash(&signing_key, &[0_u8; 10]),
1371 Err(SdkError::InvalidEventHashLength(10))
1372 ));
1373 assert!(matches!(
1374 verify_event_hash_signature(&signing_key.verifying_key(), &[0_u8; 10], "AAAA"),
1375 Err(SdkError::InvalidSignatureLength(_))
1376 ));
1377 let signature = URL_SAFE_NO_PAD.encode([0_u8; 64]);
1378 assert!(matches!(
1379 verify_event_hash_signature(&signing_key.verifying_key(), &[0_u8; 10], &signature),
1380 Err(SdkError::InvalidEventHashLength(10))
1381 ));
1382 }
1383
1384 #[test]
1385 fn verify_timestamp_rejects_drift_and_negative_window() {
1386 verify_timestamp(1000, 1200, 1000).unwrap();
1387 assert!(matches!(
1388 verify_timestamp(0, 1_000_000, 1000),
1389 Err(SdkError::TimestampOutOfWindow)
1390 ));
1391 assert!(matches!(
1392 verify_timestamp(100, 100, -1),
1393 Err(SdkError::TimestampOutOfWindow)
1394 ));
1395 }
1396
1397 #[test]
1398 fn verify_live_envelope_propagates_envelope_and_timestamp_errors() {
1399 let signer = AgentSigner::from_seed([53; 32]);
1400 let event = Event::new(
1401 "agent-profile/1.0",
1402 "profile.update",
1403 signer.agent_id(),
1404 1000,
1405 1,
1406 json!({"name": "ResearchAgent"}),
1407 );
1408 let mut envelope = signer.sign_event(event).unwrap();
1409 let mut store = MemoryNonceStore::new();
1410 let options = LiveWriteOptions {
1411 now_ms: 1000,
1412 window_ms: 1000,
1413 nonce_ttl_ms: 1000,
1414 };
1415
1416 envelope.signature = "invalid".to_owned();
1417 assert!(verify_live_envelope(&envelope, &options, &mut store).is_err());
1418
1419 let event = Event::new(
1420 "agent-profile/1.0",
1421 "profile.update",
1422 signer.agent_id(),
1423 0,
1424 2,
1425 json!({"name": "ResearchAgent"}),
1426 );
1427 let envelope = signer.sign_event(event).unwrap();
1428 let expired_options = LiveWriteOptions {
1429 now_ms: 3000,
1430 ..options
1431 };
1432 assert!(matches!(
1433 verify_live_envelope(&envelope, &expired_options, &mut store),
1434 Err(SdkError::TimestampOutOfWindow)
1435 ));
1436 }
1437
1438 #[test]
1439 fn memory_nonce_store_rejects_negative_ttl_and_expires() {
1440 let actor = AgentSigner::from_seed([26; 32]).agent_id();
1441 let mut store = MemoryNonceStore::new();
1442 assert!(matches!(
1443 store.check_and_update(&actor, 1, 1000, -1),
1444 Err(SdkError::InvalidNonce(_))
1445 ));
1446
1447 store.check_and_update(&actor, 4, 1000, 1000).unwrap();
1448 assert_eq!(store.check_and_update(&actor, 5, 1100, 1000).unwrap(), 5);
1450 assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1451 assert_eq!(store.max_nonce(&actor, 5000), None);
1453 assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1454 let stranger = AgentSigner::from_seed([27; 32]).agent_id();
1456 assert_eq!(store.max_nonce(&stranger, 1500), None);
1457 }
1458
1459 #[test]
1460 fn client_nonce_manager_with_next_and_header() {
1461 assert!(ClientNonceManager::with_next(0).is_err());
1462 let mut manager = ClientNonceManager::with_next(5).unwrap();
1463 assert_eq!(manager.peek(), 5);
1464
1465 manager.observe_max_nonce_header("10", 0).unwrap();
1466 assert_eq!(manager.peek(), 11);
1467 assert!(matches!(
1468 manager.observe_max_nonce_header("not-a-number", 0),
1469 Err(SdkError::InvalidNonce(_))
1470 ));
1471 assert!(matches!(
1472 manager.observe_max_nonce_header("9007199254740992", 0),
1473 Err(SdkError::InvalidNonce(_))
1474 ));
1475 }
1476
1477 #[test]
1478 fn default_options_and_context_use_real_clock() {
1479 let options = LiveWriteOptions::default();
1480 assert_eq!(options.window_ms, DEFAULT_LIVE_WRITE_WINDOW_MS);
1481 assert_eq!(options.nonce_ttl_ms, DEFAULT_NONCE_TTL_MS);
1482 assert!(options.now_ms > 0);
1483
1484 let context = RequestAuthContext::new("https://api.example.com");
1485 assert_eq!(context.audience, "https://api.example.com");
1486 assert_eq!(context.max_ttl_secs, DEFAULT_REQUEST_JWT_TTL_SECS);
1487 assert_eq!(context.now_secs, unix_secs());
1488 assert!(unix_ms() >= context.now_secs * 1000);
1489 }
1490
1491 fn encode_token(header: Value, claims: Value) -> String {
1492 let header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
1493 let claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).unwrap());
1494 let signature = URL_SAFE_NO_PAD.encode([0_u8; 64]);
1495 format!("{header}.{claims}.{signature}")
1496 }
1497
1498 #[test]
1499 fn verify_request_jwt_rejects_malformed_tokens() {
1500 let signer = AgentSigner::from_seed([28; 32]);
1501 let agent_id = signer.agent_id();
1502 let other = AgentSigner::from_seed([29; 32]).agent_id();
1503 let context = RequestAuthContext {
1504 audience: "https://api.example.com".to_owned(),
1505 now_secs: 200,
1506 max_ttl_secs: 300,
1507 };
1508 let valid_claims = json!({
1509 "iss": agent_id,
1510 "sub": agent_id,
1511 "aud": "https://api.example.com",
1512 "iat": 100,
1513 "exp": 400,
1514 });
1515
1516 assert!(matches!(
1517 verify_request_jwt("only.two", &context),
1518 Err(SdkError::InvalidJwt(_))
1519 ));
1520 assert!(verify_request_jwt("%.e30.AAAA", &context).is_err());
1521 let invalid_header = format!(
1522 "{}.{}.{}",
1523 URL_SAFE_NO_PAD.encode(b"not-json"),
1524 URL_SAFE_NO_PAD.encode(serde_json::to_vec(&valid_claims).unwrap()),
1525 URL_SAFE_NO_PAD.encode([0_u8; 64])
1526 );
1527 assert!(verify_request_jwt(&invalid_header, &context).is_err());
1528 let header = URL_SAFE_NO_PAD.encode(
1529 serde_json::to_vec(&json!({"alg": "EdDSA", "typ": "JWT", "kid": agent_id})).unwrap(),
1530 );
1531 let claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&valid_claims).unwrap());
1532 assert!(verify_request_jwt(&format!("{header}.%.AAAA"), &context).is_err());
1533 assert!(verify_request_jwt(&format!("{header}.{claims}.%"), &context).is_err());
1534 assert!(matches!(
1535 verify_request_jwt(&format!("{header}.{claims}.AAAA"), &context),
1536 Err(SdkError::InvalidSignatureLength(_))
1537 ));
1538 assert!(verify_request_jwt(
1539 &format!("{header}.{claims}.{}", URL_SAFE_NO_PAD.encode([0_u8; 64])),
1540 &context
1541 )
1542 .is_err());
1543 let invalid_key = AgentId::from_public_key(&[255_u8; 32]);
1544 assert!(verify_request_jwt(
1545 &encode_token(
1546 json!({"alg": "EdDSA", "typ": "JWT", "kid": invalid_key}),
1547 json!({
1548 "iss": invalid_key,
1549 "sub": invalid_key,
1550 "aud": "https://api.example.com",
1551 "iat": 100,
1552 "exp": 400,
1553 }),
1554 ),
1555 &context,
1556 )
1557 .is_err());
1558 assert!(matches!(
1559 verify_request_jwt(
1560 &encode_token(
1561 json!({"alg": "HS256", "typ": "JWT", "kid": agent_id}),
1562 valid_claims.clone(),
1563 ),
1564 &context,
1565 ),
1566 Err(SdkError::InvalidJwtClaim("alg"))
1567 ));
1568 assert!(matches!(
1569 verify_request_jwt(
1570 &encode_token(
1571 json!({"alg": "EdDSA", "typ": "JWS", "kid": agent_id}),
1572 valid_claims.clone(),
1573 ),
1574 &context,
1575 ),
1576 Err(SdkError::InvalidJwtClaim("typ"))
1577 ));
1578 assert!(matches!(
1579 verify_request_jwt(
1580 &encode_token(
1581 json!({"alg": "EdDSA", "typ": "JWT", "kid": other}),
1582 valid_claims,
1583 ),
1584 &context,
1585 ),
1586 Err(SdkError::InvalidJwtClaim("kid/iss/sub"))
1587 ));
1588 }
1589
1590 #[test]
1591 fn rejects_non_canonical_base64url_encodings() {
1592 let signer = AgentSigner::from_seed([62; 32]);
1593 let agent_id = signer.agent_id();
1594 let suffix = agent_id
1595 .as_str()
1596 .strip_prefix(AGENT_ID_PREFIX)
1597 .unwrap()
1598 .to_owned();
1599
1600 let padded = AgentId(format!("{AGENT_ID_PREFIX}{suffix}=="));
1602 assert!(padded.public_key_bytes().is_err());
1603
1604 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1609 let last = suffix.as_bytes()[suffix.len() - 1];
1610 let index = ALPHABET.iter().position(|&c| c == last).unwrap();
1611 let non_canonical = ALPHABET[(index & !3) | ((index & 3) ^ 1)] as char;
1612 let mut tweaked = suffix[..suffix.len() - 1].to_owned();
1613 tweaked.push(non_canonical);
1614 let tweaked = AgentId(format!("{AGENT_ID_PREFIX}{tweaked}"));
1615 assert!(tweaked.public_key_bytes().is_err());
1616
1617 let event = Event::new(
1619 "agent-profile/1.0",
1620 "profile.update",
1621 signer.agent_id(),
1622 1000,
1623 1,
1624 json!({"name": "Agent"}),
1625 );
1626 let mut envelope = signer.sign_event(event).unwrap();
1627 envelope.signature.push('=');
1628 assert!(verify_signature(&envelope).is_err());
1629 }
1630
1631 #[test]
1632 fn service_origin_derives_request_jwt_audience() {
1633 assert_eq!(
1634 service_origin("https://api.example.com/v1/rooms/room1").unwrap(),
1635 "https://api.example.com"
1636 );
1637 assert_eq!(
1638 service_origin("https://api.example.com:8443/path?q=1").unwrap(),
1639 "https://api.example.com:8443"
1640 );
1641 assert_eq!(
1642 service_origin("https://API.Example.com:443/").unwrap(),
1643 "https://api.example.com"
1644 );
1645 assert!(service_origin("not a url").is_err());
1646 assert!(service_origin("ftp://example.com").is_err());
1647 }
1648
1649 #[test]
1650 fn verify_request_jwt_enforces_audience_and_window() {
1651 let signer = AgentSigner::from_seed([30; 32]);
1652
1653 let wrong_audience = signer
1654 .sign_request_jwt(&RequestJwtClaims::new(
1655 signer.agent_id(),
1656 RequestBinding::new("https://api.example.com"),
1657 100,
1658 300,
1659 ))
1660 .unwrap();
1661 let context = RequestAuthContext {
1662 audience: "https://other.example.com".to_owned(),
1663 now_secs: 200,
1664 max_ttl_secs: 300,
1665 };
1666 assert!(matches!(
1667 verify_request_jwt(&wrong_audience, &context),
1668 Err(SdkError::InvalidJwtClaim("aud"))
1669 ));
1670
1671 let expired = signer
1672 .sign_request_jwt(&RequestJwtClaims::new(
1673 signer.agent_id(),
1674 RequestBinding::new("https://api.example.com"),
1675 1000,
1676 300,
1677 ))
1678 .unwrap();
1679 let later = RequestAuthContext {
1680 audience: "https://api.example.com".to_owned(),
1681 now_secs: 5000,
1682 max_ttl_secs: 300,
1683 };
1684 assert!(matches!(
1685 verify_request_jwt(&expired, &later),
1686 Err(SdkError::InvalidJwtClaim("iat/exp"))
1687 ));
1688
1689 let long_ttl = signer
1690 .sign_request_jwt(&RequestJwtClaims::new(
1691 signer.agent_id(),
1692 RequestBinding::new("https://api.example.com"),
1693 100,
1694 400,
1695 ))
1696 .unwrap();
1697 let strict = RequestAuthContext {
1698 audience: "https://api.example.com".to_owned(),
1699 now_secs: 200,
1700 max_ttl_secs: 300,
1701 };
1702 assert!(matches!(
1703 verify_request_jwt(&long_ttl, &strict),
1704 Err(SdkError::InvalidJwtClaim("ttl"))
1705 ));
1706 }
1707}