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