1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
2use base64::Engine as _;
3use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
4use serde::de::Error as DeError;
5use serde::{Deserialize, Deserializer, Serialize, Serializer};
6use serde_json::Value;
7use sha3::{Digest, Sha3_256};
8use std::collections::{BTreeMap, HashMap};
9use std::fmt;
10use std::str::FromStr;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13use crate::error::{Result, SdkError};
14
15pub const AGENT_ID_PREFIX: &str = "did:agent:";
16pub const DEFAULT_LIVE_WRITE_WINDOW_MS: i64 = 300_000;
17pub const DEFAULT_NONCE_TTL_MS: i64 = 300_000;
18pub const DEFAULT_REQUEST_JWT_TTL_SECS: i64 = 300;
19pub const MAX_NONCE_HEADER: &str = "Max-Seen-Nonce";
20
21#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
22pub struct AgentId(String);
23
24impl AgentId {
25 pub fn from_public_key(public_key: &[u8; 32]) -> Self {
26 Self(format!(
27 "{}{}",
28 AGENT_ID_PREFIX,
29 URL_SAFE_NO_PAD.encode(public_key)
30 ))
31 }
32
33 pub fn as_str(&self) -> &str {
34 &self.0
35 }
36
37 pub fn public_key_bytes(&self) -> Result<[u8; 32]> {
38 let encoded = self
39 .0
40 .strip_prefix(AGENT_ID_PREFIX)
41 .ok_or(SdkError::InvalidAgentIdPrefix)?;
42 let bytes = URL_SAFE_NO_PAD.decode(encoded)?;
43 bytes
44 .try_into()
45 .map_err(|bytes: Vec<u8>| SdkError::InvalidPublicKeyLength(bytes.len()))
46 }
47
48 pub fn verifying_key(&self) -> Result<VerifyingKey> {
49 Ok(VerifyingKey::from_bytes(&self.public_key_bytes()?)?)
50 }
51}
52
53impl fmt::Debug for AgentId {
54 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
55 formatter.debug_tuple("AgentId").field(&self.0).finish()
56 }
57}
58
59impl fmt::Display for AgentId {
60 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
61 formatter.write_str(&self.0)
62 }
63}
64
65impl FromStr for AgentId {
66 type Err = SdkError;
67
68 fn from_str(value: &str) -> Result<Self> {
69 if !value.starts_with(AGENT_ID_PREFIX) {
70 return Err(SdkError::InvalidAgentIdPrefix);
71 }
72 let agent_id = Self(value.to_owned());
73 agent_id.public_key_bytes()?;
74 Ok(agent_id)
75 }
76}
77
78impl TryFrom<String> for AgentId {
79 type Error = SdkError;
80
81 fn try_from(value: String) -> Result<Self> {
82 Self::from_str(&value)
83 }
84}
85
86impl Serialize for AgentId {
87 fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
88 where
89 S: Serializer,
90 {
91 serializer.serialize_str(&self.0)
92 }
93}
94
95impl<'de> Deserialize<'de> for AgentId {
96 fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
97 where
98 D: Deserializer<'de>,
99 {
100 let value = String::deserialize(deserializer)?;
101 Self::from_str(&value).map_err(D::Error::custom)
102 }
103}
104
105pub struct AgentSigner {
106 signing_key: SigningKey,
107}
108
109impl AgentSigner {
110 pub fn generate() -> Self {
111 let mut seed = [0_u8; 32];
112 rand::fill(&mut seed);
113 Self::from_seed(seed)
114 }
115
116 pub fn from_seed(seed: [u8; 32]) -> Self {
117 Self {
118 signing_key: SigningKey::from_bytes(&seed),
119 }
120 }
121
122 pub fn agent_id(&self) -> AgentId {
123 AgentId::from_public_key(&self.signing_key.verifying_key().to_bytes())
124 }
125
126 pub fn verifying_key(&self) -> VerifyingKey {
127 self.signing_key.verifying_key()
128 }
129
130 pub fn sign_event<P>(&self, event: Event<P>) -> Result<Envelope<P>>
131 where
132 P: Serialize,
133 {
134 let hash = event_hash(&event)?;
135 let signature = sign_event(&self.signing_key, &event)?;
136 Ok(Envelope {
137 hash,
138 event,
139 signature,
140 })
141 }
142
143 pub fn sign_request_jwt(&self, claims: &RequestJwtClaims) -> Result<String> {
144 let agent_id = self.agent_id();
145 if claims.iss != agent_id || claims.sub != agent_id {
146 return Err(SdkError::InvalidJwtClaim("iss/sub"));
147 }
148
149 let header = RequestJwtHeader {
150 alg: "EdDSA".to_owned(),
151 typ: "JWT".to_owned(),
152 kid: agent_id,
153 };
154 let header = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&header)?);
155 let payload = URL_SAFE_NO_PAD.encode(serde_json::to_vec(claims)?);
156 let signing_input = format!("{header}.{payload}");
157 let signature = self.signing_key.sign(signing_input.as_bytes());
158 Ok(format!(
159 "{signing_input}.{}",
160 URL_SAFE_NO_PAD.encode(signature.to_bytes())
161 ))
162 }
163}
164
165#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
166pub struct Event<P = Value> {
167 pub protocol: String,
168 #[serde(rename = "type")]
169 pub kind: String,
170 pub actor: AgentId,
171 pub created_at: i64,
172 pub nonce: u64,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 pub room_id: Option<String>,
175 pub payload: P,
176 #[serde(default, flatten)]
177 pub extra: BTreeMap<String, Value>,
178}
179
180impl<P> Event<P> {
181 pub fn new(
182 protocol: impl Into<String>,
183 kind: impl Into<String>,
184 actor: AgentId,
185 created_at: i64,
186 nonce: u64,
187 payload: P,
188 ) -> Self {
189 Self {
190 protocol: protocol.into(),
191 kind: kind.into(),
192 actor,
193 created_at,
194 nonce,
195 room_id: None,
196 payload,
197 extra: BTreeMap::new(),
198 }
199 }
200
201 pub fn with_room_id(mut self, room_id: impl Into<String>) -> Self {
202 self.room_id = Some(room_id.into());
203 self
204 }
205}
206
207#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
208pub struct Envelope<P = Value> {
209 pub hash: String,
210 pub event: Event<P>,
211 pub signature: String,
212}
213
214pub fn canonical_event_bytes<P>(event: &Event<P>) -> Result<Vec<u8>>
215where
216 P: Serialize,
217{
218 serde_jcs::to_vec(event).map_err(|err| SdkError::CanonicalJson(err.to_string()))
219}
220
221pub fn event_hash<P>(event: &Event<P>) -> Result<String>
222where
223 P: Serialize,
224{
225 let digest = Sha3_256::digest(canonical_event_bytes(event)?);
226 Ok(URL_SAFE_NO_PAD.encode(digest))
227}
228
229pub fn sign_event<P>(signing_key: &SigningKey, event: &Event<P>) -> Result<String>
230where
231 P: Serialize,
232{
233 let bytes = canonical_event_bytes(event)?;
234 let signature = signing_key.sign(&bytes);
235 Ok(URL_SAFE_NO_PAD.encode(signature.to_bytes()))
236}
237
238pub fn verify_event_hash<P>(envelope: &Envelope<P>) -> Result<()>
239where
240 P: Serialize,
241{
242 let expected = event_hash(&envelope.event)?;
243 if expected == envelope.hash {
244 Ok(())
245 } else {
246 Err(SdkError::InvalidEventHash {
247 expected,
248 actual: envelope.hash.clone(),
249 })
250 }
251}
252
253pub fn verify_signature<P>(envelope: &Envelope<P>) -> Result<()>
254where
255 P: Serialize,
256{
257 let bytes = canonical_event_bytes(&envelope.event)?;
258 let signature_bytes = URL_SAFE_NO_PAD.decode(&envelope.signature)?;
259 let signature_bytes: [u8; 64] = signature_bytes
260 .try_into()
261 .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
262 let signature = Signature::from_bytes(&signature_bytes);
263 envelope
264 .event
265 .actor
266 .verifying_key()?
267 .verify_strict(&bytes, &signature)?;
268 Ok(())
269}
270
271pub fn verify_envelope<P>(envelope: &Envelope<P>) -> Result<()>
272where
273 P: Serialize,
274{
275 verify_event_hash(envelope)?;
276 verify_signature(envelope)
277}
278
279pub fn verify_timestamp(created_at: i64, now_ms: i64, window_ms: i64) -> Result<()> {
280 if window_ms < 0 || created_at.abs_diff(now_ms) > window_ms as u64 {
281 Err(SdkError::TimestampOutOfWindow)
282 } else {
283 Ok(())
284 }
285}
286
287#[derive(Clone, Copy, Debug, Eq, PartialEq)]
288pub struct NonceRecord {
289 pub max_nonce: u64,
290 pub expires_at: i64,
291}
292
293pub trait NonceStore {
294 fn check_and_update(
295 &mut self,
296 actor: &AgentId,
297 nonce: u64,
298 now_ms: i64,
299 ttl_ms: i64,
300 ) -> Result<u64>;
301
302 fn max_nonce(&self, actor: &AgentId, now_ms: i64) -> Option<u64>;
303}
304
305#[derive(Clone, Debug, Default)]
306pub struct MemoryNonceStore {
307 records: HashMap<AgentId, NonceRecord>,
308}
309
310impl MemoryNonceStore {
311 pub fn new() -> Self {
312 Self::default()
313 }
314}
315
316impl NonceStore for MemoryNonceStore {
317 fn check_and_update(
318 &mut self,
319 actor: &AgentId,
320 nonce: u64,
321 now_ms: i64,
322 ttl_ms: i64,
323 ) -> Result<u64> {
324 validate_nonce(nonce)?;
325 if ttl_ms < 0 {
326 return Err(SdkError::InvalidNonce(
327 "nonce cache ttl must be non-negative".to_owned(),
328 ));
329 }
330
331 if let Some(record) = self.records.get(actor) {
332 if record.expires_at > now_ms && nonce <= record.max_nonce {
333 return Err(SdkError::NonceNotGreater {
334 max_nonce: record.max_nonce,
335 });
336 }
337 }
338
339 let record = NonceRecord {
340 max_nonce: nonce,
341 expires_at: now_ms.saturating_add(ttl_ms),
342 };
343 self.records.insert(actor.clone(), record);
344 Ok(record.max_nonce)
345 }
346
347 fn max_nonce(&self, actor: &AgentId, now_ms: i64) -> Option<u64> {
348 self.records
349 .get(actor)
350 .filter(|record| record.expires_at > now_ms)
351 .map(|record| record.max_nonce)
352 }
353}
354
355#[derive(Clone, Debug, Eq, PartialEq)]
356pub struct ClientNonceManager {
357 next_nonce: u64,
358}
359
360impl Default for ClientNonceManager {
361 fn default() -> Self {
362 Self { next_nonce: 1 }
363 }
364}
365
366impl ClientNonceManager {
367 pub fn new() -> Self {
368 Self::default()
369 }
370
371 pub fn with_next(next_nonce: u64) -> Result<Self> {
372 validate_nonce(next_nonce)?;
373 Ok(Self { next_nonce })
374 }
375
376 pub fn peek(&self) -> u64 {
377 self.next_nonce
378 }
379
380 pub fn next_nonce(&mut self) -> Result<u64> {
381 let nonce = self.next_nonce;
382 self.next_nonce = self
383 .next_nonce
384 .checked_add(1)
385 .ok_or_else(|| SdkError::InvalidNonce("nonce counter overflow".to_owned()))?;
386 Ok(nonce)
387 }
388
389 pub fn observe_max_nonce(&mut self, max_nonce: u64) {
390 if max_nonce >= self.next_nonce {
391 self.next_nonce = max_nonce.saturating_add(1);
392 }
393 }
394
395 pub fn observe_max_nonce_header(&mut self, value: &str) -> Result<()> {
396 let max_nonce = value
397 .parse::<u64>()
398 .map_err(|_| SdkError::InvalidNonce("invalid max nonce header".to_owned()))?;
399 self.observe_max_nonce(max_nonce);
400 Ok(())
401 }
402}
403
404pub fn validate_nonce(nonce: u64) -> Result<()> {
405 if nonce == 0 || nonce > 0x1FFFFFFFFFFFFF {
406 Err(SdkError::InvalidNonce(
408 "nonce must be a positive integer less than or equal to 9007199254740991".to_owned(),
409 ))
410 } else {
411 Ok(())
412 }
413}
414
415#[derive(Clone, Debug)]
416pub struct LiveWriteOptions {
417 pub now_ms: i64,
418 pub window_ms: i64,
419 pub nonce_ttl_ms: i64,
420}
421
422impl Default for LiveWriteOptions {
423 fn default() -> Self {
424 Self {
425 now_ms: unix_ms(),
426 window_ms: DEFAULT_LIVE_WRITE_WINDOW_MS,
427 nonce_ttl_ms: DEFAULT_NONCE_TTL_MS,
428 }
429 }
430}
431
432pub fn verify_live_envelope<P, S>(
433 envelope: &Envelope<P>,
434 options: &LiveWriteOptions,
435 nonce_store: &mut S,
436) -> Result<u64>
437where
438 P: Serialize,
439 S: NonceStore,
440{
441 verify_envelope(envelope)?;
442 verify_timestamp(envelope.event.created_at, options.now_ms, options.window_ms)?;
443 nonce_store.check_and_update(
444 &envelope.event.actor,
445 envelope.event.nonce,
446 options.now_ms,
447 options.nonce_ttl_ms,
448 )
449}
450
451#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
452pub struct RequestJwtHeader {
453 pub alg: String,
454 pub typ: String,
455 pub kid: AgentId,
456}
457
458#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
459pub struct RequestJwtClaims {
460 pub iss: AgentId,
461 pub sub: AgentId,
462 pub aud: String,
463 pub iat: i64,
464 pub exp: i64,
465}
466
467#[derive(Clone, Debug, PartialEq, Eq)]
468pub struct RequestBinding {
469 pub audience: String,
470}
471
472impl RequestBinding {
473 pub fn new(audience: impl Into<String>) -> Self {
474 Self {
475 audience: audience.into(),
476 }
477 }
478}
479
480impl RequestJwtClaims {
481 pub fn new(agent_id: AgentId, binding: RequestBinding, issued_at: i64, ttl_secs: i64) -> Self {
482 Self {
483 iss: agent_id.clone(),
484 sub: agent_id,
485 aud: binding.audience,
486 iat: issued_at,
487 exp: issued_at + ttl_secs,
488 }
489 }
490}
491
492#[derive(Clone, Debug)]
493pub struct RequestAuthContext {
494 pub audience: String,
495 pub now_secs: i64,
496 pub max_ttl_secs: i64,
497}
498
499impl RequestAuthContext {
500 pub fn new(audience: impl Into<String>) -> Self {
501 let binding = RequestBinding::new(audience);
502 Self {
503 audience: binding.audience,
504 now_secs: unix_secs(),
505 max_ttl_secs: DEFAULT_REQUEST_JWT_TTL_SECS,
506 }
507 }
508}
509
510pub fn verify_request_jwt(token: &str, context: &RequestAuthContext) -> Result<RequestJwtClaims> {
511 let parts: Vec<&str> = token.split('.').collect();
512 if parts.len() != 3 {
513 return Err(SdkError::InvalidJwt(
514 "expected three compact JWS parts".into(),
515 ));
516 }
517
518 let header: RequestJwtHeader = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[0])?)?;
519 let claims: RequestJwtClaims = serde_json::from_slice(&URL_SAFE_NO_PAD.decode(parts[1])?)?;
520 let signature_bytes = URL_SAFE_NO_PAD.decode(parts[2])?;
521 let signature_bytes: [u8; 64] = signature_bytes
522 .try_into()
523 .map_err(|bytes: Vec<u8>| SdkError::InvalidSignatureLength(bytes.len()))?;
524 let signature = Signature::from_bytes(&signature_bytes);
525 let signing_input = format!("{}.{}", parts[0], parts[1]);
526
527 if header.alg != "EdDSA" {
528 return Err(SdkError::InvalidJwtClaim("alg"));
529 }
530 if header.typ != "JWT" {
531 return Err(SdkError::InvalidJwtClaim("typ"));
532 }
533 if header.kid != claims.iss || claims.iss != claims.sub {
534 return Err(SdkError::InvalidJwtClaim("kid/iss/sub"));
535 }
536
537 header
538 .kid
539 .verifying_key()?
540 .verify_strict(signing_input.as_bytes(), &signature)?;
541
542 if claims.aud != context.audience {
543 return Err(SdkError::InvalidJwtClaim("aud"));
544 }
545 if claims.iat > context.now_secs || claims.exp < context.now_secs {
546 return Err(SdkError::InvalidJwtClaim("iat/exp"));
547 }
548 if claims.exp - claims.iat > context.max_ttl_secs {
549 return Err(SdkError::InvalidJwtClaim("ttl"));
550 }
551
552 Ok(claims)
553}
554
555pub fn unix_ms() -> i64 {
556 SystemTime::now()
557 .duration_since(UNIX_EPOCH)
558 .map(|duration| duration.as_millis().min(i64::MAX as u128) as i64)
559 .unwrap_or(0)
560}
561
562pub fn unix_secs() -> i64 {
563 unix_ms() / 1000
564}
565
566#[cfg(test)]
567mod tests {
568 use serde_json::json;
569
570 use super::*;
571
572 #[test]
573 fn signs_and_verifies_event_envelope() {
574 let signer = AgentSigner::from_seed([7; 32]);
575 let event = Event::new(
576 "agent-profile/1.0",
577 "profile.update",
578 signer.agent_id(),
579 1_779_753_600_000,
580 1,
581 json!({"agent_id": signer.agent_id(), "name": "ResearchAgent"}),
582 );
583
584 let envelope = signer.sign_event(event).unwrap();
585
586 assert!(!envelope.hash.starts_with("evt_"));
587 assert_eq!(envelope.hash.len(), 43);
588 verify_envelope(&envelope).unwrap();
589 }
590
591 #[test]
592 fn rejects_tampered_event_payload() {
593 let signer = AgentSigner::from_seed([8; 32]);
594 let event = Event::new(
595 "agent-profile/1.0",
596 "profile.update",
597 signer.agent_id(),
598 1_779_753_600_000,
599 1,
600 json!({"name": "before"}),
601 );
602 let mut envelope = signer.sign_event(event).unwrap();
603 envelope.event.payload = json!({"name": "after"});
604
605 assert!(verify_envelope(&envelope).is_err());
606 }
607
608 #[test]
609 fn rejects_nonce_reuse() {
610 let signer = AgentSigner::from_seed([9; 32]);
611 let options = LiveWriteOptions {
612 now_ms: 1000,
613 window_ms: 1000,
614 nonce_ttl_ms: 1000,
615 };
616 let event = Event::new(
617 "agent-profile/1.0",
618 "profile.update",
619 signer.agent_id(),
620 1000,
621 1,
622 json!({"name": "ResearchAgent"}),
623 );
624 let envelope = signer.sign_event(event).unwrap();
625 let mut store = MemoryNonceStore::new();
626
627 let max_nonce = verify_live_envelope(&envelope, &options, &mut store).unwrap();
628 assert_eq!(max_nonce, 1);
629 assert!(matches!(
630 verify_live_envelope(&envelope, &options, &mut store),
631 Err(SdkError::NonceNotGreater { max_nonce: 1 })
632 ));
633 }
634
635 #[test]
636 fn client_nonce_manager_observes_server_max() {
637 let mut manager = ClientNonceManager::new();
638
639 assert_eq!(manager.next_nonce().unwrap(), 1);
640 manager.observe_max_nonce(5);
641
642 assert_eq!(manager.peek(), 6);
643 assert_eq!(manager.next_nonce().unwrap(), 6);
644 }
645
646 #[test]
647 fn signs_and_verifies_request_jwt() {
648 let signer = AgentSigner::from_seed([10; 32]);
649 let claims = RequestJwtClaims::new(
650 signer.agent_id(),
651 RequestBinding::new("https://api.example.com"),
652 100,
653 300,
654 );
655 let token = signer.sign_request_jwt(&claims).unwrap();
656 let context = RequestAuthContext {
657 audience: "https://api.example.com".to_owned(),
658 now_secs: 120,
659 max_ttl_secs: 300,
660 };
661 let verified = verify_request_jwt(&token, &context).unwrap();
662
663 assert_eq!(verified.iss, signer.agent_id());
664 }
665}