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 const AGENT_URL_MAX_ROUTES: usize = 8;
1088
1089#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1093pub struct AgentUrl {
1094 pub agent_id: AgentId,
1095 #[serde(default, skip_serializing_if = "Option::is_none")]
1096 pub protocol: Option<String>,
1097 #[serde(default, skip_serializing_if = "Option::is_none")]
1098 pub resource: Option<String>,
1099 #[serde(default)]
1100 pub routes: Vec<String>,
1101}
1102
1103fn invalid_agent_url(message: &str) -> SdkError {
1104 SdkError::protocol("invalid_url", format!("invalid agent URL: {message}"))
1105}
1106
1107fn valid_agent_url_path(protocol: &str, resource: &str) -> bool {
1108 !protocol.is_empty()
1109 && protocol.bytes().all(|b| b.is_ascii_lowercase())
1110 && !resource.is_empty()
1111 && resource
1112 .bytes()
1113 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
1114}
1115
1116pub fn parse_agent_url(value: &str) -> Result<AgentUrl> {
1117 if !value.bytes().all(|b| (0x21..=0x7e).contains(&b)) || value.contains(['#', '%']) {
1118 return Err(invalid_agent_url(
1119 "must be printable ASCII without fragment or percent-encoding",
1120 ));
1121 }
1122 let (head, query) = match value.split_once('?') {
1123 Some((head, query)) => (head, Some(query)),
1124 None => (value, None),
1125 };
1126 let segments: Vec<&str> = head.split('/').collect();
1127 let mut url = AgentUrl {
1128 agent_id: segments[0].parse()?,
1129 protocol: None,
1130 resource: None,
1131 routes: Vec::new(),
1132 };
1133 if segments.len() != 1 {
1134 if segments.len() != 3 || !valid_agent_url_path(segments[1], segments[2]) {
1135 return Err(invalid_agent_url("path must be /<protocol>/<resource>"));
1136 }
1137 url.protocol = Some(segments[1].to_owned());
1138 url.resource = Some(segments[2].to_owned());
1139 }
1140 if let Some(query) = query {
1141 if query.split('&').count() > AGENT_URL_MAX_ROUTES {
1142 return Err(invalid_agent_url("at most eight routes"));
1143 }
1144 for item in query.split('&') {
1145 let route = item
1146 .strip_prefix("route=")
1147 .ok_or_else(|| invalid_agent_url("query allows only route parameters"))?;
1148 validate_origin(route)?;
1149 if url.routes.iter().any(|known| known == route) {
1150 return Err(invalid_agent_url("duplicate route"));
1151 }
1152 url.routes.push(route.to_owned());
1153 }
1154 }
1155 Ok(url)
1156}
1157
1158pub fn format_agent_url(url: &AgentUrl) -> Result<String> {
1160 let path = match (&url.protocol, &url.resource) {
1161 (None, None) => String::new(),
1162 (Some(protocol), Some(resource)) if valid_agent_url_path(protocol, resource) => {
1163 format!("/{protocol}/{resource}")
1164 }
1165 _ => return Err(invalid_agent_url("path must be /<protocol>/<resource>")),
1166 };
1167 let unique: std::collections::BTreeSet<&String> = url.routes.iter().collect();
1168 if url.routes.len() > AGENT_URL_MAX_ROUTES || unique.len() != url.routes.len() {
1169 return Err(invalid_agent_url(
1170 "routes must be at most eight unique origins",
1171 ));
1172 }
1173 for route in &url.routes {
1174 validate_origin(route)?;
1175 }
1176 let query = if url.routes.is_empty() {
1177 String::new()
1178 } else {
1179 format!(
1180 "?{}",
1181 url.routes
1182 .iter()
1183 .map(|route| format!("route={route}"))
1184 .collect::<Vec<_>>()
1185 .join("&")
1186 )
1187 };
1188 Ok(format!("{}{path}{query}", url.agent_id.as_str()))
1189}
1190
1191pub fn service_origin(url: &str) -> Result<String> {
1194 let parsed = url::Url::parse(url)
1195 .map_err(|_| SdkError::InvalidPayload(format!("not a valid URL: {url}")))?;
1196 if parsed.scheme() != "https" && parsed.scheme() != "http" {
1197 return Err(SdkError::InvalidPayload(format!(
1198 "not an HTTP(S) URL: {url}"
1199 )));
1200 }
1201 Ok(parsed.origin().ascii_serialization())
1202}
1203
1204pub fn unix_ms() -> i64 {
1205 let duration = SystemTime::now()
1206 .duration_since(UNIX_EPOCH)
1207 .unwrap_or_default();
1208 duration.as_millis().min(i64::MAX as u128) as i64
1209}
1210
1211pub fn unix_secs() -> i64 {
1212 unix_ms() / 1000
1213}
1214
1215#[cfg(test)]
1216mod tests {
1217 use serde::ser;
1218 use serde_json::json;
1219
1220 use super::*;
1221
1222 struct FailingPayload;
1223
1224 impl Serialize for FailingPayload {
1225 fn serialize<S>(&self, _serializer: S) -> std::result::Result<S::Ok, S::Error>
1226 where
1227 S: Serializer,
1228 {
1229 Err(ser::Error::custom("payload cannot be serialized"))
1230 }
1231 }
1232
1233 #[test]
1234 fn signs_and_verifies_event_envelope() {
1235 let signer = AgentSigner::from_seed([7; 32]);
1236 let event = Event::new(
1237 "agent-profile/1.0",
1238 "profile.update",
1239 signer.agent_id(),
1240 1_779_753_600_000,
1241 1,
1242 json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1243 );
1244
1245 let envelope = signer.sign_event(event).unwrap();
1246
1247 assert!(!envelope.hash.starts_with("evt_"));
1248 assert_eq!(envelope.hash.len(), 43);
1249 verify_envelope(&envelope).unwrap();
1250 }
1251
1252 #[test]
1253 fn signs_and_verifies_raw_event_hash_bytes() {
1254 let seed = [18; 32];
1255 let signer = AgentSigner::from_seed(seed);
1256 let signing_key = SigningKey::from_bytes(&seed);
1257 let event = Event::new(
1258 "agent-profile/1.0",
1259 "profile.update",
1260 signer.agent_id(),
1261 1_779_753_600_000,
1262 1,
1263 json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1264 );
1265
1266 let digest = event_hash_bytes(&event).unwrap();
1267 let signature = sign_event_hash(&signing_key, &digest).unwrap();
1268 let envelope = signer.sign_event(event).unwrap();
1269
1270 assert_eq!(envelope.signature, signature);
1271 verify_event_hash_signature(&signing_key.verifying_key(), &digest, &signature).unwrap();
1272 }
1273
1274 #[test]
1275 fn rejects_tampered_event_payload() {
1276 let signer = AgentSigner::from_seed([8; 32]);
1277 let event = Event::new(
1278 "agent-profile/1.0",
1279 "profile.update",
1280 signer.agent_id(),
1281 1_779_753_600_000,
1282 1,
1283 json!({"name": "before"}),
1284 );
1285 let mut envelope = signer.sign_event(event).unwrap();
1286 envelope.event.payload = json!({"name": "after"});
1287
1288 assert!(verify_envelope(&envelope).is_err());
1289 }
1290
1291 #[test]
1292 fn rejects_non_canonical_event_payloads() {
1293 let signer = AgentSigner::from_seed([52; 32]);
1294 let event = Event::new(
1295 "agent-profile/1.0",
1296 "profile.update",
1297 signer.agent_id(),
1298 1000,
1299 1,
1300 FailingPayload,
1301 );
1302 assert!(matches!(
1303 canonical_event_bytes(&event),
1304 Err(SdkError::CanonicalJson(_))
1305 ));
1306 assert!(matches!(
1307 event_hash(&event),
1308 Err(SdkError::CanonicalJson(_))
1309 ));
1310 let envelope = Envelope {
1311 hash: "h".to_owned(),
1312 event,
1313 signature: "s".to_owned(),
1314 };
1315 assert!(matches!(
1316 verify_event_hash(&envelope),
1317 Err(SdkError::CanonicalJson(_))
1318 ));
1319 assert!(matches!(
1320 verify_signature(&envelope),
1321 Err(SdkError::CanonicalJson(_))
1322 ));
1323 assert!(matches!(
1324 verify_envelope(&envelope),
1325 Err(SdkError::CanonicalJson(_))
1326 ));
1327 }
1328
1329 #[test]
1330 fn rejects_nonce_reuse() {
1331 let signer = AgentSigner::from_seed([9; 32]);
1332 let options = LiveWriteOptions {
1333 now_ms: 1000,
1334 window_ms: 1000,
1335 nonce_ttl_ms: 1000,
1336 };
1337 let event = Event::new(
1338 "agent-profile/1.0",
1339 "profile.update",
1340 signer.agent_id(),
1341 1000,
1342 1,
1343 json!({"name": "ResearchAgent"}),
1344 );
1345 let envelope = signer.sign_event(event).unwrap();
1346 let mut store = MemoryNonceStore::new();
1347
1348 let max_nonce = verify_live_envelope(&envelope, &options, &mut store).unwrap();
1349 assert_eq!(max_nonce, 1);
1350 assert!(matches!(
1351 verify_live_envelope(&envelope, &options, &mut store),
1352 Err(SdkError::NonceNotGreater { max_nonce: 1 })
1353 ));
1354 }
1355
1356 #[test]
1357 fn client_nonce_manager_observes_server_max() {
1358 let mut manager = ClientNonceManager::new();
1359
1360 assert_eq!(manager.next_nonce().unwrap(), 1);
1361 manager.observe_max_nonce(5, 0).unwrap();
1362
1363 assert_eq!(manager.peek(), 6);
1364 manager.observe_max_nonce(4, 0).unwrap();
1365 assert_eq!(manager.peek(), 6);
1366 assert_eq!(manager.next_nonce().unwrap(), 6);
1367
1368 let mut invalid = ClientNonceManager {
1369 next_nonce: MAX_SAFE_NONCE + 1,
1370 };
1371 assert!(matches!(
1372 invalid.next_nonce(),
1373 Err(SdkError::InvalidNonce(_))
1374 ));
1375 }
1376
1377 #[test]
1378 fn rejects_nonce_values_outside_safe_json_integer_range() {
1379 let signer = AgentSigner::from_seed([16; 32]);
1380 let event = Event::new(
1381 "agent-profile/1.0",
1382 "profile.update",
1383 signer.agent_id(),
1384 1000,
1385 MAX_SAFE_NONCE + 1,
1386 json!({"id": signer.agent_id(), "name": "ResearchAgent"}),
1387 );
1388
1389 assert!(matches!(
1390 signer.sign_event(event),
1391 Err(SdkError::InvalidNonce(_))
1392 ));
1393 }
1394
1395 #[test]
1396 fn signs_and_verifies_request_jwt() {
1397 let signer = AgentSigner::from_seed([10; 32]);
1398 let claims = RequestJwtClaims::new(
1399 signer.agent_id(),
1400 RequestBinding::new("https://api.example.com"),
1401 100,
1402 300,
1403 );
1404 let token = signer.sign_request_jwt(&claims).unwrap();
1405 let context = RequestAuthContext {
1406 audience: "https://api.example.com".to_owned(),
1407 now_secs: 120,
1408 max_ttl_secs: 300,
1409 };
1410 let verified = verify_request_jwt(&token, &context).unwrap();
1411
1412 assert_eq!(verified.iss, signer.agent_id());
1413 }
1414
1415 #[test]
1416 fn rejects_request_jwts_with_non_positive_ttl() {
1417 let signer = AgentSigner::from_seed([17; 32]);
1418 let claims = RequestJwtClaims::new(
1419 signer.agent_id(),
1420 RequestBinding::new("https://api.example.com"),
1421 100,
1422 0,
1423 );
1424 let token = signer.sign_request_jwt(&claims).unwrap();
1425 let context = RequestAuthContext {
1426 audience: "https://api.example.com".to_owned(),
1427 now_secs: 100,
1428 max_ttl_secs: 300,
1429 };
1430
1431 assert!(matches!(
1432 verify_request_jwt(&token, &context),
1433 Err(SdkError::InvalidJwtClaim("exp/iat"))
1434 ));
1435 }
1436
1437 #[test]
1438 fn agent_id_string_conversions() {
1439 let signer = AgentSigner::from_seed([20; 32]);
1440 let agent_id = signer.agent_id();
1441 let text = agent_id.to_string();
1442
1443 assert_eq!(agent_id.as_str(), text);
1444 assert!(format!("{agent_id:?}").starts_with("AgentId("));
1445 assert_eq!(AgentId::try_from(text.clone()).unwrap(), agent_id);
1446 assert_eq!(text.parse::<AgentId>().unwrap(), agent_id);
1447 assert!(matches!(
1448 "not-a-did".parse::<AgentId>(),
1449 Err(SdkError::InvalidAgentIdPrefix)
1450 ));
1451 assert!(matches!(
1452 AgentId("not-a-did".to_owned()).public_key_bytes(),
1453 Err(SdkError::InvalidAgentIdPrefix)
1454 ));
1455 assert!(AgentId(format!("{AGENT_ID_PREFIX}%"))
1456 .public_key_bytes()
1457 .is_err());
1458 let short = AgentId(format!(
1459 "{}{}",
1460 AGENT_ID_PREFIX,
1461 URL_SAFE_NO_PAD.encode([1_u8; 31])
1462 ));
1463 assert!(matches!(
1464 short.public_key_bytes(),
1465 Err(SdkError::InvalidPublicKeyLength(31))
1466 ));
1467 assert!(short.verifying_key().is_err());
1468 assert!(format!("{}%", AGENT_ID_PREFIX).parse::<AgentId>().is_err());
1469 }
1470
1471 #[test]
1472 fn agent_signer_exposes_keys_and_generates() {
1473 let signer = AgentSigner::from_seed([21; 32]);
1474 assert_eq!(
1475 signer.verifying_key().to_bytes(),
1476 signer.agent_id().public_key_bytes().unwrap()
1477 );
1478
1479 let generated = AgentSigner::generate();
1480 let event = Event::new(
1481 "agent-profile/1.0",
1482 "profile.update",
1483 generated.agent_id(),
1484 1000,
1485 1,
1486 json!({"name": "Generated"}),
1487 );
1488 verify_envelope(&generated.sign_event(event).unwrap()).unwrap();
1489 }
1490
1491 #[test]
1492 fn sign_request_jwt_rejects_foreign_claims() {
1493 let signer = AgentSigner::from_seed([22; 32]);
1494 let other = AgentSigner::from_seed([23; 32]);
1495 let claims = RequestJwtClaims {
1496 iss: other.agent_id(),
1497 sub: other.agent_id(),
1498 aud: "https://api.example.com".to_owned(),
1499 iat: 100,
1500 exp: 400,
1501 };
1502 assert!(matches!(
1503 signer.sign_request_jwt(&claims),
1504 Err(SdkError::InvalidJwtClaim("iss/sub"))
1505 ));
1506 }
1507
1508 #[test]
1509 fn free_sign_event_matches_signer() {
1510 let seed = [24; 32];
1511 let signer = AgentSigner::from_seed(seed);
1512 let signing_key = SigningKey::from_bytes(&seed);
1513 let event = Event::new(
1514 "agent-profile/1.0",
1515 "profile.update",
1516 signer.agent_id(),
1517 1000,
1518 1,
1519 json!({"name": "Agent"}),
1520 );
1521
1522 let signature = sign_event(&signing_key, &event).unwrap();
1523 assert_eq!(signer.sign_event(event).unwrap().signature, signature);
1524 }
1525
1526 #[test]
1527 fn rejects_event_hash_with_wrong_length() {
1528 let signing_key = SigningKey::from_bytes(&[25; 32]);
1529 assert!(matches!(
1530 sign_event_hash(&signing_key, &[0_u8; 10]),
1531 Err(SdkError::InvalidEventHashLength(10))
1532 ));
1533 assert!(matches!(
1534 verify_event_hash_signature(&signing_key.verifying_key(), &[0_u8; 10], "AAAA"),
1535 Err(SdkError::InvalidSignatureLength(_))
1536 ));
1537 let signature = URL_SAFE_NO_PAD.encode([0_u8; 64]);
1538 assert!(matches!(
1539 verify_event_hash_signature(&signing_key.verifying_key(), &[0_u8; 10], &signature),
1540 Err(SdkError::InvalidEventHashLength(10))
1541 ));
1542 }
1543
1544 #[test]
1545 fn verify_timestamp_rejects_drift_and_negative_window() {
1546 verify_timestamp(1000, 1200, 1000).unwrap();
1547 assert!(matches!(
1548 verify_timestamp(0, 1_000_000, 1000),
1549 Err(SdkError::TimestampOutOfWindow)
1550 ));
1551 assert!(matches!(
1552 verify_timestamp(100, 100, -1),
1553 Err(SdkError::TimestampOutOfWindow)
1554 ));
1555 }
1556
1557 #[test]
1558 fn verify_live_envelope_propagates_envelope_and_timestamp_errors() {
1559 let signer = AgentSigner::from_seed([53; 32]);
1560 let event = Event::new(
1561 "agent-profile/1.0",
1562 "profile.update",
1563 signer.agent_id(),
1564 1000,
1565 1,
1566 json!({"name": "ResearchAgent"}),
1567 );
1568 let mut envelope = signer.sign_event(event).unwrap();
1569 let mut store = MemoryNonceStore::new();
1570 let options = LiveWriteOptions {
1571 now_ms: 1000,
1572 window_ms: 1000,
1573 nonce_ttl_ms: 1000,
1574 };
1575
1576 envelope.signature = "invalid".to_owned();
1577 assert!(verify_live_envelope(&envelope, &options, &mut store).is_err());
1578
1579 let event = Event::new(
1580 "agent-profile/1.0",
1581 "profile.update",
1582 signer.agent_id(),
1583 0,
1584 2,
1585 json!({"name": "ResearchAgent"}),
1586 );
1587 let envelope = signer.sign_event(event).unwrap();
1588 let expired_options = LiveWriteOptions {
1589 now_ms: 3000,
1590 ..options
1591 };
1592 assert!(matches!(
1593 verify_live_envelope(&envelope, &expired_options, &mut store),
1594 Err(SdkError::TimestampOutOfWindow)
1595 ));
1596 }
1597
1598 #[test]
1599 fn memory_nonce_store_rejects_negative_ttl_and_expires() {
1600 let actor = AgentSigner::from_seed([26; 32]).agent_id();
1601 let mut store = MemoryNonceStore::new();
1602 assert!(matches!(
1603 store.check_and_update(&actor, 1, 1000, -1),
1604 Err(SdkError::InvalidNonce(_))
1605 ));
1606
1607 store.check_and_update(&actor, 4, 1000, 1000).unwrap();
1608 assert_eq!(store.check_and_update(&actor, 5, 1100, 1000).unwrap(), 5);
1610 assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1611 assert_eq!(store.max_nonce(&actor, 5000), None);
1613 assert_eq!(store.max_nonce(&actor, 1500), Some(5));
1614 let stranger = AgentSigner::from_seed([27; 32]).agent_id();
1616 assert_eq!(store.max_nonce(&stranger, 1500), None);
1617 }
1618
1619 #[test]
1620 fn client_nonce_manager_with_next_and_header() {
1621 assert!(ClientNonceManager::with_next(0).is_err());
1622 let mut manager = ClientNonceManager::with_next(5).unwrap();
1623 assert_eq!(manager.peek(), 5);
1624
1625 manager.observe_max_nonce_header("10", 0).unwrap();
1626 assert_eq!(manager.peek(), 11);
1627 assert!(matches!(
1628 manager.observe_max_nonce_header("not-a-number", 0),
1629 Err(SdkError::InvalidNonce(_))
1630 ));
1631 assert!(matches!(
1632 manager.observe_max_nonce_header("9007199254740992", 0),
1633 Err(SdkError::InvalidNonce(_))
1634 ));
1635 }
1636
1637 #[test]
1638 fn default_options_and_context_use_real_clock() {
1639 let options = LiveWriteOptions::default();
1640 assert_eq!(options.window_ms, DEFAULT_LIVE_WRITE_WINDOW_MS);
1641 assert_eq!(options.nonce_ttl_ms, DEFAULT_NONCE_TTL_MS);
1642 assert!(options.now_ms > 0);
1643
1644 let context = RequestAuthContext::new("https://api.example.com");
1645 assert_eq!(context.audience, "https://api.example.com");
1646 assert_eq!(context.max_ttl_secs, DEFAULT_REQUEST_JWT_TTL_SECS);
1647 assert_eq!(context.now_secs, unix_secs());
1648 assert!(unix_ms() >= context.now_secs * 1000);
1649 }
1650
1651 fn encode_token(header: Value, claims: Value) -> String {
1652 let header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header).unwrap());
1653 let claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims).unwrap());
1654 let signature = URL_SAFE_NO_PAD.encode([0_u8; 64]);
1655 format!("{header}.{claims}.{signature}")
1656 }
1657
1658 #[test]
1659 fn verify_request_jwt_rejects_malformed_tokens() {
1660 let signer = AgentSigner::from_seed([28; 32]);
1661 let agent_id = signer.agent_id();
1662 let other = AgentSigner::from_seed([29; 32]).agent_id();
1663 let context = RequestAuthContext {
1664 audience: "https://api.example.com".to_owned(),
1665 now_secs: 200,
1666 max_ttl_secs: 300,
1667 };
1668 let valid_claims = json!({
1669 "iss": agent_id,
1670 "sub": agent_id,
1671 "aud": "https://api.example.com",
1672 "iat": 100,
1673 "exp": 400,
1674 });
1675
1676 assert!(matches!(
1677 verify_request_jwt("only.two", &context),
1678 Err(SdkError::InvalidJwt(_))
1679 ));
1680 assert!(verify_request_jwt("%.e30.AAAA", &context).is_err());
1681 let invalid_header = format!(
1682 "{}.{}.{}",
1683 URL_SAFE_NO_PAD.encode(b"not-json"),
1684 URL_SAFE_NO_PAD.encode(serde_json::to_vec(&valid_claims).unwrap()),
1685 URL_SAFE_NO_PAD.encode([0_u8; 64])
1686 );
1687 assert!(verify_request_jwt(&invalid_header, &context).is_err());
1688 let header = URL_SAFE_NO_PAD.encode(
1689 serde_json::to_vec(&json!({"alg": "EdDSA", "typ": "JWT", "kid": agent_id})).unwrap(),
1690 );
1691 let claims = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&valid_claims).unwrap());
1692 assert!(verify_request_jwt(&format!("{header}.%.AAAA"), &context).is_err());
1693 assert!(verify_request_jwt(&format!("{header}.{claims}.%"), &context).is_err());
1694 assert!(matches!(
1695 verify_request_jwt(&format!("{header}.{claims}.AAAA"), &context),
1696 Err(SdkError::InvalidSignatureLength(_))
1697 ));
1698 assert!(verify_request_jwt(
1699 &format!("{header}.{claims}.{}", URL_SAFE_NO_PAD.encode([0_u8; 64])),
1700 &context
1701 )
1702 .is_err());
1703 let invalid_key = AgentId::from_public_key(&[255_u8; 32]);
1704 assert!(verify_request_jwt(
1705 &encode_token(
1706 json!({"alg": "EdDSA", "typ": "JWT", "kid": invalid_key}),
1707 json!({
1708 "iss": invalid_key,
1709 "sub": invalid_key,
1710 "aud": "https://api.example.com",
1711 "iat": 100,
1712 "exp": 400,
1713 }),
1714 ),
1715 &context,
1716 )
1717 .is_err());
1718 assert!(matches!(
1719 verify_request_jwt(
1720 &encode_token(
1721 json!({"alg": "HS256", "typ": "JWT", "kid": agent_id}),
1722 valid_claims.clone(),
1723 ),
1724 &context,
1725 ),
1726 Err(SdkError::InvalidJwtClaim("alg"))
1727 ));
1728 assert!(matches!(
1729 verify_request_jwt(
1730 &encode_token(
1731 json!({"alg": "EdDSA", "typ": "JWS", "kid": agent_id}),
1732 valid_claims.clone(),
1733 ),
1734 &context,
1735 ),
1736 Err(SdkError::InvalidJwtClaim("typ"))
1737 ));
1738 assert!(matches!(
1739 verify_request_jwt(
1740 &encode_token(
1741 json!({"alg": "EdDSA", "typ": "JWT", "kid": other}),
1742 valid_claims,
1743 ),
1744 &context,
1745 ),
1746 Err(SdkError::InvalidJwtClaim("kid/iss/sub"))
1747 ));
1748 }
1749
1750 #[test]
1751 fn rejects_non_canonical_base64url_encodings() {
1752 let signer = AgentSigner::from_seed([62; 32]);
1753 let agent_id = signer.agent_id();
1754 let suffix = agent_id
1755 .as_str()
1756 .strip_prefix(AGENT_ID_PREFIX)
1757 .unwrap()
1758 .to_owned();
1759
1760 let padded = AgentId(format!("{AGENT_ID_PREFIX}{suffix}=="));
1762 assert!(padded.public_key_bytes().is_err());
1763
1764 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1769 let last = suffix.as_bytes()[suffix.len() - 1];
1770 let index = ALPHABET.iter().position(|&c| c == last).unwrap();
1771 let non_canonical = ALPHABET[(index & !3) | ((index & 3) ^ 1)] as char;
1772 let mut tweaked = suffix[..suffix.len() - 1].to_owned();
1773 tweaked.push(non_canonical);
1774 let tweaked = AgentId(format!("{AGENT_ID_PREFIX}{tweaked}"));
1775 assert!(tweaked.public_key_bytes().is_err());
1776
1777 let event = Event::new(
1779 "agent-profile/1.0",
1780 "profile.update",
1781 signer.agent_id(),
1782 1000,
1783 1,
1784 json!({"name": "Agent"}),
1785 );
1786 let mut envelope = signer.sign_event(event).unwrap();
1787 envelope.signature.push('=');
1788 assert!(verify_signature(&envelope).is_err());
1789 }
1790
1791 #[test]
1792 fn service_origin_derives_request_jwt_audience() {
1793 assert_eq!(
1794 service_origin("https://api.example.com/v1/rooms/room1").unwrap(),
1795 "https://api.example.com"
1796 );
1797 assert_eq!(
1798 service_origin("https://api.example.com:8443/path?q=1").unwrap(),
1799 "https://api.example.com:8443"
1800 );
1801 assert_eq!(
1802 service_origin("https://API.Example.com:443/").unwrap(),
1803 "https://api.example.com"
1804 );
1805 assert!(service_origin("not a url").is_err());
1806 assert!(service_origin("ftp://example.com").is_err());
1807 }
1808
1809 #[test]
1810 fn verify_request_jwt_enforces_audience_and_window() {
1811 let signer = AgentSigner::from_seed([30; 32]);
1812
1813 let wrong_audience = signer
1814 .sign_request_jwt(&RequestJwtClaims::new(
1815 signer.agent_id(),
1816 RequestBinding::new("https://api.example.com"),
1817 100,
1818 300,
1819 ))
1820 .unwrap();
1821 let context = RequestAuthContext {
1822 audience: "https://other.example.com".to_owned(),
1823 now_secs: 200,
1824 max_ttl_secs: 300,
1825 };
1826 assert!(matches!(
1827 verify_request_jwt(&wrong_audience, &context),
1828 Err(SdkError::InvalidJwtClaim("aud"))
1829 ));
1830
1831 let expired = signer
1832 .sign_request_jwt(&RequestJwtClaims::new(
1833 signer.agent_id(),
1834 RequestBinding::new("https://api.example.com"),
1835 1000,
1836 300,
1837 ))
1838 .unwrap();
1839 let later = RequestAuthContext {
1840 audience: "https://api.example.com".to_owned(),
1841 now_secs: 5000,
1842 max_ttl_secs: 300,
1843 };
1844 assert!(matches!(
1845 verify_request_jwt(&expired, &later),
1846 Err(SdkError::InvalidJwtClaim("iat/exp"))
1847 ));
1848
1849 let long_ttl = signer
1850 .sign_request_jwt(&RequestJwtClaims::new(
1851 signer.agent_id(),
1852 RequestBinding::new("https://api.example.com"),
1853 100,
1854 400,
1855 ))
1856 .unwrap();
1857 let strict = RequestAuthContext {
1858 audience: "https://api.example.com".to_owned(),
1859 now_secs: 200,
1860 max_ttl_secs: 300,
1861 };
1862 assert!(matches!(
1863 verify_request_jwt(&long_ttl, &strict),
1864 Err(SdkError::InvalidJwtClaim("ttl"))
1865 ));
1866 }
1867}