1use std::{
16 collections::{HashMap, HashSet},
17 fmt,
18 sync::Arc,
19 time::{SystemTime, UNIX_EPOCH},
20};
21
22use serde::{Deserialize, Serialize};
23use serde_json::{Value, json};
24use typesec_core::{
25 Capability, SecureValue,
26 permissions::{AiCanInfer, CanReadSensitive},
27 resource::GenericResource,
28 secure_value::Secret,
29};
30
31use crate::http::{HttpClient, ReqwestHttpClient};
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(try_from = "String", into = "String")]
36pub struct Did(String);
37
38impl Did {
39 pub fn parse(value: impl Into<String>) -> Result<Self, DidError> {
41 let value = value.into();
42 let parts: Vec<_> = value.split(':').collect();
43 if parts.len() < 3 || parts.first() != Some(&"did") || parts[1].is_empty() {
44 return Err(DidError::InvalidDid(value));
45 }
46 Ok(Self(value))
47 }
48
49 pub fn key(public_key: impl AsRef<[u8]>) -> Self {
51 Self(format!("did:key:z{}", hex_encode(public_key.as_ref())))
52 }
53
54 pub fn web(host: impl AsRef<str>) -> Result<Self, DidError> {
56 let host = host.as_ref().trim();
57 if host.is_empty() || host.contains('/') {
58 return Err(DidError::InvalidDid(format!("did:web:{host}")));
59 }
60 Ok(Self(format!("did:web:{host}")))
61 }
62
63 pub fn as_str(&self) -> &str {
65 &self.0
66 }
67}
68
69impl fmt::Display for Did {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str(&self.0)
72 }
73}
74
75impl TryFrom<String> for Did {
76 type Error = DidError;
77
78 fn try_from(value: String) -> Result<Self, Self::Error> {
79 Self::parse(value)
80 }
81}
82
83impl From<Did> for String {
84 fn from(value: Did) -> Self {
85 value.0
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
91pub struct VerificationMethod {
92 pub id: String,
94 #[serde(rename = "type")]
96 pub method_type: String,
97 pub controller: Did,
99 pub public_key_hex: String,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
103 pub key_version: Option<u64>,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
106 pub key_status: Option<String>,
107}
108
109impl VerificationMethod {
110 pub fn local(id: impl Into<String>, controller: Did, public_key: impl AsRef<[u8]>) -> Self {
112 Self {
113 id: id.into(),
114 method_type: "TypesecDemoKey2026".to_owned(),
115 controller,
116 public_key_hex: hex_encode(public_key.as_ref()),
117 key_version: None,
118 key_status: None,
119 }
120 }
121
122 fn public_key(&self) -> Result<Vec<u8>, DidError> {
123 hex_decode(&self.public_key_hex)
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct DidService {
130 pub id: String,
132 #[serde(rename = "type")]
134 pub service_type: String,
135 pub service_endpoint: String,
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
141pub struct DidDocument {
142 pub id: Did,
144 #[serde(default)]
146 pub verification_method: Vec<VerificationMethod>,
147 #[serde(default)]
149 pub authentication: Vec<String>,
150 #[serde(default)]
152 pub key_agreement: Vec<String>,
153 #[serde(default)]
155 pub service: Vec<DidService>,
156}
157
158impl DidDocument {
159 pub fn single_key(did: Did, public_key: impl AsRef<[u8]>) -> Self {
161 let key_id = format!("{did}#key-1");
162 Self {
163 id: did.clone(),
164 verification_method: vec![VerificationMethod::local(&key_id, did, public_key)],
165 authentication: vec![key_id.clone()],
166 key_agreement: vec![key_id],
167 service: Vec::new(),
168 }
169 }
170
171 pub fn with_signing_and_agreement_keys(
174 did: Did,
175 signing_public: impl AsRef<[u8]>,
176 agreement_public: impl AsRef<[u8]>,
177 ) -> Self {
178 let signing_id = format!("{did}#key-1");
179 let agreement_id = format!("{did}#key-2");
180 Self {
181 id: did.clone(),
182 verification_method: vec![
183 VerificationMethod {
184 id: signing_id.clone(),
185 method_type: "Ed25519VerificationKey2020".to_owned(),
186 controller: did.clone(),
187 public_key_hex: hex_encode(signing_public.as_ref()),
188 key_version: Some(1),
189 key_status: Some("active".to_owned()),
190 },
191 VerificationMethod {
192 id: agreement_id.clone(),
193 method_type: "X25519KeyAgreementKey2020".to_owned(),
194 controller: did,
195 public_key_hex: hex_encode(agreement_public.as_ref()),
196 key_version: Some(1),
197 key_status: Some("active".to_owned()),
198 },
199 ],
200 authentication: vec![signing_id],
201 key_agreement: vec![agreement_id],
202 service: Vec::new(),
203 }
204 }
205
206 fn method(&self, id: &str) -> Option<&VerificationMethod> {
207 self.verification_method
208 .iter()
209 .find(|method| method.id == id)
210 }
211
212 fn authentication_key(&self, kid: &str) -> Result<&VerificationMethod, DidError> {
213 if !self.authentication.iter().any(|id| id == kid) {
214 return Err(DidError::MissingVerificationMethod(kid.to_owned()));
215 }
216 self.method(kid)
217 .ok_or_else(|| DidError::MissingVerificationMethod(kid.to_owned()))
218 }
219
220 fn key_agreement_key(&self) -> Result<&VerificationMethod, DidError> {
221 let kid = self
222 .key_agreement
223 .first()
224 .ok_or(DidError::MissingKeyAgreement)?;
225 self.method(kid)
226 .ok_or_else(|| DidError::MissingVerificationMethod(kid.clone()))
227 }
228
229 fn key_agreement_keys(&self) -> Result<Vec<&VerificationMethod>, DidError> {
230 if self.key_agreement.is_empty() {
231 return Err(DidError::MissingKeyAgreement);
232 }
233
234 self.key_agreement
235 .iter()
236 .map(|kid| {
237 self.method(kid)
238 .ok_or_else(|| DidError::MissingVerificationMethod(kid.clone()))
239 })
240 .collect()
241 }
242}
243
244pub trait DidResolver: Send + Sync {
246 fn resolve(&self, did: &Did) -> Result<DidDocument, DidError>;
248}
249
250#[derive(Debug, Default, Clone)]
252pub struct StaticDidResolver {
253 documents: HashMap<Did, DidDocument>,
254}
255
256impl StaticDidResolver {
257 pub fn new() -> Self {
259 Self::default()
260 }
261
262 pub fn with_document(mut self, document: DidDocument) -> Self {
264 self.documents.insert(document.id.clone(), document);
265 self
266 }
267}
268
269impl DidResolver for StaticDidResolver {
270 fn resolve(&self, did: &Did) -> Result<DidDocument, DidError> {
271 self.documents
272 .get(did)
273 .cloned()
274 .ok_or_else(|| DidError::Unresolved(did.to_string()))
275 }
276}
277
278pub trait DidKeyStore: Send + Sync {
280 fn sign(&self, signer: &Did, message: &[u8]) -> Result<String, DidError>;
282
283 fn verify(
285 &self,
286 method: &VerificationMethod,
287 message: &[u8],
288 signature: &str,
289 ) -> Result<(), DidError>;
290
291 fn encrypt_for(
293 &self,
294 sender: &Did,
295 recipient_public_key: &[u8],
296 plaintext: &[u8],
297 nonce: &[u8],
298 ) -> Result<String, DidError>;
299
300 fn decrypt_for(
302 &self,
303 recipient: &Did,
304 sender_public_key: &[u8],
305 nonce: &[u8],
306 ciphertext_hex: &str,
307 ) -> Result<Vec<u8>, DidError>;
308}
309
310#[cfg(any(test, feature = "demo-crypto"))]
315#[derive(Debug, Clone, PartialEq, Eq)]
316pub struct DemoDidKeyPair {
317 pub public_key: Vec<u8>,
319 private_key: Vec<u8>,
320}
321
322#[cfg(any(test, feature = "demo-crypto"))]
323impl DemoDidKeyPair {
324 pub fn from_seed(seed: impl AsRef<[u8]>) -> Self {
326 let private_key = derive_bytes(b"typesec-did-private", seed.as_ref(), 32);
327 let public_key = private_key.clone();
328 Self {
329 public_key,
330 private_key,
331 }
332 }
333}
334
335#[cfg(any(test, feature = "demo-crypto"))]
341#[derive(Debug, Default, Clone)]
342pub struct DemoDidKeyStore {
343 keys: HashMap<Did, DemoDidKeyPair>,
344}
345
346#[cfg(any(test, feature = "demo-crypto"))]
347impl DemoDidKeyStore {
348 pub fn new() -> Self {
350 Self::default()
351 }
352
353 pub fn with_key(mut self, did: Did, key: DemoDidKeyPair) -> Self {
355 self.keys.insert(did, key);
356 self
357 }
358
359 fn key(&self, did: &Did) -> Result<&DemoDidKeyPair, DidError> {
360 self.keys
361 .get(did)
362 .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))
363 }
364}
365
366#[cfg(any(test, feature = "demo-crypto"))]
367impl DidKeyStore for DemoDidKeyStore {
368 fn sign(&self, signer: &Did, message: &[u8]) -> Result<String, DidError> {
369 let key = self.key(signer)?;
370 Ok(hex_encode(&derive_bytes(&key.private_key, message, 32)))
371 }
372
373 fn verify(
374 &self,
375 method: &VerificationMethod,
376 message: &[u8],
377 signature: &str,
378 ) -> Result<(), DidError> {
379 let public = method.public_key()?;
380 let expected = hex_encode(&derive_bytes(&public, message, 32));
381 if constant_time_eq(expected.as_bytes(), signature.as_bytes()) {
382 Ok(())
383 } else {
384 Err(DidError::InvalidSignature)
385 }
386 }
387
388 fn encrypt_for(
389 &self,
390 sender: &Did,
391 recipient_public_key: &[u8],
392 plaintext: &[u8],
393 nonce: &[u8],
394 ) -> Result<String, DidError> {
395 let sender_key = self.key(sender)?;
396 let ciphertext = xor_stream(
397 plaintext,
398 &derive_shared_key(&sender_key.private_key, recipient_public_key, nonce),
399 );
400 Ok(hex_encode(&ciphertext))
401 }
402
403 fn decrypt_for(
404 &self,
405 recipient: &Did,
406 sender_public_key: &[u8],
407 nonce: &[u8],
408 ciphertext_hex: &str,
409 ) -> Result<Vec<u8>, DidError> {
410 let recipient_key = self.key(recipient)?;
411 let ciphertext = hex_decode(ciphertext_hex)?;
412 Ok(xor_stream(
413 &ciphertext,
414 &derive_shared_key(&recipient_key.private_key, sender_public_key, nonce),
415 ))
416 }
417}
418
419#[derive(Clone)]
427pub struct Ed25519DidKey {
428 signing: ed25519_dalek::SigningKey,
429 agreement: x25519_dalek::StaticSecret,
430}
431
432impl Ed25519DidKey {
433 pub fn generate() -> Result<Self, DidError> {
435 let mut signing_seed = [0u8; 32];
436 let mut agreement_seed = [0u8; 32];
437 getrandom::getrandom(&mut signing_seed).map_err(|e| DidError::KeyGen(e.to_string()))?;
438 getrandom::getrandom(&mut agreement_seed).map_err(|e| DidError::KeyGen(e.to_string()))?;
439 Ok(Self::from_seeds(signing_seed, agreement_seed))
440 }
441
442 pub fn from_seed(seed: impl AsRef<[u8]>) -> Self {
447 let signing_seed = sha256_tagged(b"typesec-ed25519-signing", seed.as_ref());
448 let agreement_seed = sha256_tagged(b"typesec-x25519-agreement", seed.as_ref());
449 Self::from_seeds(signing_seed, agreement_seed)
450 }
451
452 fn from_seeds(signing_seed: [u8; 32], agreement_seed: [u8; 32]) -> Self {
453 Self {
454 signing: ed25519_dalek::SigningKey::from_bytes(&signing_seed),
455 agreement: x25519_dalek::StaticSecret::from(agreement_seed),
456 }
457 }
458
459 pub fn signing_public(&self) -> [u8; 32] {
461 self.signing.verifying_key().to_bytes()
462 }
463
464 pub fn agreement_public(&self) -> [u8; 32] {
466 x25519_dalek::PublicKey::from(&self.agreement).to_bytes()
467 }
468
469 pub fn document(&self, did: Did) -> DidDocument {
471 DidDocument::with_signing_and_agreement_keys(
472 did,
473 self.signing_public(),
474 self.agreement_public(),
475 )
476 }
477}
478
479impl std::fmt::Debug for Ed25519DidKey {
480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
481 f.debug_struct("Ed25519DidKey")
482 .field("signing_public", &hex_encode(&self.signing_public()))
483 .field("agreement_public", &hex_encode(&self.agreement_public()))
484 .finish_non_exhaustive()
485 }
486}
487
488#[derive(Debug, Default, Clone)]
491pub struct Ed25519DidKeyStore {
492 keys: HashMap<Did, Vec<Ed25519DidKeyRecord>>,
493 retired_methods: HashSet<String>,
494}
495
496#[derive(Debug, Clone)]
497struct Ed25519DidKeyRecord {
498 version: u64,
499 key: Ed25519DidKey,
500 retired: bool,
501}
502
503impl Ed25519DidKeyStore {
504 pub fn new() -> Self {
506 Self::default()
507 }
508
509 pub fn with_key(mut self, did: Did, key: Ed25519DidKey) -> Self {
511 self.keys.insert(
512 did,
513 vec![Ed25519DidKeyRecord {
514 version: 1,
515 key,
516 retired: false,
517 }],
518 );
519 self
520 }
521
522 pub fn rotate_key(&mut self, did: &Did, key: Ed25519DidKey) -> Result<u64, DidError> {
527 let records = self
528 .keys
529 .get_mut(did)
530 .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))?;
531 let next_version = records
532 .iter()
533 .map(|record| record.version)
534 .max()
535 .unwrap_or(0)
536 + 1;
537 records.push(Ed25519DidKeyRecord {
538 version: next_version,
539 key,
540 retired: false,
541 });
542 Ok(next_version)
543 }
544
545 pub fn retire_key(&mut self, did: &Did, version: u64) -> Result<(), DidError> {
550 if self.active_key_version(did)? == version {
551 return Err(DidError::CannotRetireActiveKey {
552 did: did.to_string(),
553 version,
554 });
555 }
556
557 let records = self
558 .keys
559 .get_mut(did)
560 .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))?;
561 let record = records
562 .iter_mut()
563 .find(|record| record.version == version)
564 .ok_or_else(|| DidError::MissingKeyVersion {
565 did: did.to_string(),
566 version,
567 })?;
568 record.retired = true;
569 self.retired_methods
570 .insert(Self::signing_method_id(did, version));
571 self.retired_methods
572 .insert(Self::agreement_method_id(did, version));
573 Ok(())
574 }
575
576 pub fn active_key_version(&self, did: &Did) -> Result<u64, DidError> {
578 Ok(self.active_record(did)?.version)
579 }
580
581 pub fn document(&self, did: &Did) -> Result<DidDocument, DidError> {
583 let records = self
584 .keys
585 .get(did)
586 .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))?;
587 let active_version = self.active_key_version(did)?;
588 let mut verification_method = Vec::new();
589 let mut authentication = Vec::new();
590 let mut key_agreement = Vec::new();
591
592 for record in records.iter().filter(|record| !record.retired) {
593 let status = if record.version == active_version {
594 "active"
595 } else {
596 "previous"
597 };
598 let signing_id = Self::signing_method_id(did, record.version);
599 let agreement_id = Self::agreement_method_id(did, record.version);
600 verification_method.push(VerificationMethod {
601 id: signing_id.clone(),
602 method_type: "Ed25519VerificationKey2020".to_owned(),
603 controller: did.clone(),
604 public_key_hex: hex_encode(&record.key.signing_public()),
605 key_version: Some(record.version),
606 key_status: Some(status.to_owned()),
607 });
608 verification_method.push(VerificationMethod {
609 id: agreement_id.clone(),
610 method_type: "X25519KeyAgreementKey2020".to_owned(),
611 controller: did.clone(),
612 public_key_hex: hex_encode(&record.key.agreement_public()),
613 key_version: Some(record.version),
614 key_status: Some(status.to_owned()),
615 });
616
617 if record.version == active_version {
618 authentication.insert(0, signing_id);
619 key_agreement.insert(0, agreement_id);
620 } else {
621 authentication.push(signing_id);
622 key_agreement.push(agreement_id);
623 }
624 }
625
626 Ok(DidDocument {
627 id: did.clone(),
628 verification_method,
629 authentication,
630 key_agreement,
631 service: Vec::new(),
632 })
633 }
634
635 fn active_record(&self, did: &Did) -> Result<&Ed25519DidKeyRecord, DidError> {
636 self.keys
637 .get(did)
638 .and_then(|records| {
639 records
640 .iter()
641 .filter(|record| !record.retired)
642 .max_by_key(|record| record.version)
643 })
644 .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))
645 }
646
647 fn signing_method_id(did: &Did, version: u64) -> String {
648 if version == 1 {
649 format!("{did}#key-1")
650 } else {
651 format!("{did}#key-signing-v{version}")
652 }
653 }
654
655 fn agreement_method_id(did: &Did, version: u64) -> String {
656 if version == 1 {
657 format!("{did}#key-2")
658 } else {
659 format!("{did}#key-agreement-v{version}")
660 }
661 }
662
663 fn aead_key(shared_secret: &[u8; 32]) -> chacha20poly1305::Key {
664 let digest = sha256_tagged(b"typesec-did-aead", shared_secret);
665 chacha20poly1305::Key::from(digest)
666 }
667}
668
669impl DidKeyStore for Ed25519DidKeyStore {
670 fn sign(&self, signer: &Did, message: &[u8]) -> Result<String, DidError> {
671 use ed25519_dalek::Signer;
672 let record = self.active_record(signer)?;
673 Ok(hex_encode(&record.key.signing.sign(message).to_bytes()))
674 }
675
676 fn verify(
677 &self,
678 method: &VerificationMethod,
679 message: &[u8],
680 signature: &str,
681 ) -> Result<(), DidError> {
682 use ed25519_dalek::Verifier;
683 if self.retired_methods.contains(&method.id) {
684 return Err(DidError::RetiredKey(method.id.clone()));
685 }
686 let public: [u8; 32] = method
687 .public_key()?
688 .try_into()
689 .map_err(|_| DidError::InvalidKey("ed25519 public key must be 32 bytes".into()))?;
690 let verifying = ed25519_dalek::VerifyingKey::from_bytes(&public)
691 .map_err(|e| DidError::InvalidKey(e.to_string()))?;
692 let signature_bytes: [u8; 64] = hex_decode(signature)?
693 .try_into()
694 .map_err(|_| DidError::InvalidSignature)?;
695 verifying
696 .verify(
697 message,
698 &ed25519_dalek::Signature::from_bytes(&signature_bytes),
699 )
700 .map_err(|_| DidError::InvalidSignature)
701 }
702
703 fn encrypt_for(
704 &self,
705 sender: &Did,
706 recipient_public_key: &[u8],
707 plaintext: &[u8],
708 nonce: &[u8],
709 ) -> Result<String, DidError> {
710 use chacha20poly1305::KeyInit;
711 use chacha20poly1305::aead::Aead;
712 let sender_key = &self.active_record(sender)?.key;
713 let recipient: [u8; 32] = recipient_public_key
714 .try_into()
715 .map_err(|_| DidError::InvalidKey("x25519 public key must be 32 bytes".into()))?;
716 let shared = sender_key
717 .agreement
718 .diffie_hellman(&x25519_dalek::PublicKey::from(recipient));
719 let nonce: [u8; 12] = nonce.try_into().map_err(|_| DidError::InvalidNonce)?;
720 let cipher = chacha20poly1305::ChaCha20Poly1305::new(&Self::aead_key(shared.as_bytes()));
721 let ciphertext = cipher
722 .encrypt(&chacha20poly1305::Nonce::from(nonce), plaintext)
723 .map_err(|_| DidError::EncryptionFailed)?;
724 Ok(hex_encode(&ciphertext))
725 }
726
727 fn decrypt_for(
728 &self,
729 recipient: &Did,
730 sender_public_key: &[u8],
731 nonce: &[u8],
732 ciphertext_hex: &str,
733 ) -> Result<Vec<u8>, DidError> {
734 use chacha20poly1305::KeyInit;
735 use chacha20poly1305::aead::Aead;
736 let sender: [u8; 32] = sender_public_key
737 .try_into()
738 .map_err(|_| DidError::InvalidKey("x25519 public key must be 32 bytes".into()))?;
739 let nonce: [u8; 12] = nonce.try_into().map_err(|_| DidError::InvalidNonce)?;
740 let ciphertext = hex_decode(ciphertext_hex)?;
741 let records = self
742 .keys
743 .get(recipient)
744 .ok_or_else(|| DidError::MissingPrivateKey(recipient.to_string()))?;
745
746 for record in records.iter().filter(|record| !record.retired) {
747 let shared = record
748 .key
749 .agreement
750 .diffie_hellman(&x25519_dalek::PublicKey::from(sender));
751 let cipher =
752 chacha20poly1305::ChaCha20Poly1305::new(&Self::aead_key(shared.as_bytes()));
753 if let Ok(plaintext) =
754 cipher.decrypt(&chacha20poly1305::Nonce::from(nonce), ciphertext.as_slice())
755 {
756 return Ok(plaintext);
757 }
758 }
759
760 Err(DidError::DecryptionFailed)
761 }
762}
763
764#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
766pub struct DidMessageBody {
767 pub action: String,
769 pub resource: String,
771 pub privacy: String,
773 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub reply_to: Option<DidMessageReference>,
776}
777
778impl DidMessageBody {
779 pub fn infer_prompt(resource: impl Into<String>) -> Self {
781 Self {
782 action: "ai:infer".to_owned(),
783 resource: resource.into(),
784 privacy: "secret".to_owned(),
785 reply_to: None,
786 }
787 }
788
789 pub fn reply_to_prompt(prompt: &VerifiedDidPrompt) -> Self {
791 Self {
792 action: prompt.body.action.clone(),
793 resource: prompt.body.resource.clone(),
794 privacy: prompt.body.privacy.clone(),
795 reply_to: Some(prompt.prompt_ref.clone()),
796 }
797 }
798
799 pub fn agent_message(resource: impl Into<String>, privacy: impl Into<String>) -> Self {
801 Self {
802 action: "agent:message".to_owned(),
803 resource: resource.into(),
804 privacy: privacy.into(),
805 reply_to: None,
806 }
807 }
808
809 pub fn agent_delegate(resource: impl Into<String>, privacy: impl Into<String>) -> Self {
811 Self {
812 action: "agent:delegate".to_owned(),
813 resource: resource.into(),
814 privacy: privacy.into(),
815 reply_to: None,
816 }
817 }
818}
819
820#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
822#[serde(rename_all = "snake_case")]
823pub enum TypeDidMode {
824 Send,
826 RequestReply,
828}
829
830#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
832pub struct TypeDidConversation {
833 pub conversation_id: String,
835 pub mode: TypeDidMode,
837 pub profile: String,
839 pub protocol: String,
841 #[serde(default, skip_serializing_if = "Option::is_none")]
843 pub expires_at: Option<u64>,
844}
845
846impl TypeDidConversation {
847 pub fn new(
849 conversation_id: impl Into<String>,
850 mode: TypeDidMode,
851 profile: impl Into<String>,
852 protocol: impl Into<String>,
853 ) -> Self {
854 Self {
855 conversation_id: conversation_id.into(),
856 mode,
857 profile: profile.into(),
858 protocol: protocol.into(),
859 expires_at: None,
860 }
861 }
862
863 pub fn with_expires_at(mut self, expires_at: u64) -> Self {
865 self.expires_at = Some(expires_at);
866 self
867 }
868}
869
870#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
872pub struct TypeDidProfile {
873 pub id: String,
875 #[serde(default)]
877 pub did_methods: Vec<String>,
878 #[serde(default)]
880 pub signing: Vec<String>,
881 #[serde(default)]
883 pub key_agreement: Vec<String>,
884 #[serde(default)]
886 pub encryption: Vec<String>,
887 #[serde(default)]
889 pub transport_bindings: Vec<String>,
890 #[serde(default)]
892 pub modes: Vec<TypeDidMode>,
893 #[serde(default, skip_serializing_if = "Option::is_none")]
895 pub max_payload_bytes: Option<usize>,
896 #[serde(default)]
898 pub required_claims: Vec<String>,
899 #[serde(default)]
901 pub policy_actions: Vec<String>,
902 #[serde(default, skip_serializing_if = "Option::is_none")]
904 pub retention: Option<String>,
905 #[serde(default, skip_serializing_if = "Option::is_none")]
907 pub audit: Option<String>,
908}
909
910impl TypeDidProfile {
911 pub fn ed25519_x25519_chacha20() -> Self {
914 Self {
915 id: "typedid/v1/x25519-chacha20poly1305-ed25519".to_owned(),
916 did_methods: vec![
917 "did:web".to_owned(),
918 "did:key".to_owned(),
919 "did:indy".to_owned(),
920 ],
921 signing: vec!["Ed25519".to_owned()],
922 key_agreement: vec!["X25519".to_owned()],
923 encryption: vec!["ChaCha20-Poly1305".to_owned()],
924 transport_bindings: vec![
925 "a2a".to_owned(),
926 "acp".to_owned(),
927 "band".to_owned(),
928 "https".to_owned(),
929 "websocket".to_owned(),
930 ],
931 modes: vec![TypeDidMode::Send, TypeDidMode::RequestReply],
932 max_payload_bytes: Some(1024 * 1024),
933 required_claims: vec![
934 "org".to_owned(),
935 "agent_id".to_owned(),
936 "purpose".to_owned(),
937 ],
938 policy_actions: vec![
939 "agent:message".to_owned(),
940 "agent:delegate".to_owned(),
941 "ai:infer".to_owned(),
942 ],
943 retention: Some("sender-encrypted-payload-only".to_owned()),
944 audit: Some("envelope-metadata-and-policy-decision".to_owned()),
945 }
946 }
947
948 pub fn is_compatible_with(&self, remote: &Self, protocol: &str, mode: TypeDidMode) -> bool {
950 self.id == remote.id
951 && contains(&self.transport_bindings, protocol)
952 && contains(&remote.transport_bindings, protocol)
953 && self.modes.contains(&mode)
954 && remote.modes.contains(&mode)
955 && intersects(&self.did_methods, &remote.did_methods)
956 && intersects(&self.signing, &remote.signing)
957 && intersects(&self.key_agreement, &remote.key_agreement)
958 && intersects(&self.encryption, &remote.encryption)
959 }
960
961 pub fn negotiate<'a>(
963 local: &'a [Self],
964 remote: &[Self],
965 protocol: &str,
966 mode: TypeDidMode,
967 ) -> Result<&'a Self, DidError> {
968 local
969 .iter()
970 .find(|candidate| {
971 remote
972 .iter()
973 .any(|other| candidate.is_compatible_with(other, protocol, mode))
974 })
975 .ok_or(DidError::NoCompatibleTypeDidProfile)
976 }
977}
978
979pub trait TypeDidProfileResolver: Send + Sync {
981 fn resolve_profiles(&self, target: &str) -> Result<Vec<TypeDidProfile>, DidError>;
983}
984
985#[derive(Debug, Default, Clone)]
987pub struct StaticTypeDidProfileResolver {
988 profiles: HashMap<String, Vec<TypeDidProfile>>,
989}
990
991impl StaticTypeDidProfileResolver {
992 pub fn new() -> Self {
994 Self::default()
995 }
996
997 pub fn with_profiles(
999 mut self,
1000 target: impl Into<String>,
1001 profiles: Vec<TypeDidProfile>,
1002 ) -> Self {
1003 self.profiles.insert(target.into(), profiles);
1004 self
1005 }
1006}
1007
1008impl TypeDidProfileResolver for StaticTypeDidProfileResolver {
1009 fn resolve_profiles(&self, target: &str) -> Result<Vec<TypeDidProfile>, DidError> {
1010 self.profiles
1011 .get(target)
1012 .cloned()
1013 .ok_or_else(|| DidError::Unresolved(target.to_owned()))
1014 }
1015}
1016
1017#[derive(Debug, Clone)]
1019pub struct DidReplyBinding {
1020 pub prompt_body: DidMessageBody,
1022 pub prompt_ref: DidMessageReference,
1024}
1025
1026impl DidReplyBinding {
1027 pub fn for_prompt(prompt: &VerifiedDidPrompt) -> Self {
1029 Self {
1030 prompt_body: prompt.body.clone(),
1031 prompt_ref: prompt.prompt_ref.clone(),
1032 }
1033 }
1034}
1035
1036#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1038pub struct DidMessageReference {
1039 pub id: String,
1041 pub digest: String,
1043}
1044
1045#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1047pub struct DidEnvelope {
1048 pub id: String,
1050 #[serde(rename = "type")]
1052 pub message_type: String,
1053 pub from: Did,
1055 pub to: Vec<Did>,
1057 pub created_time: u64,
1059 pub expires_time: u64,
1061 pub body: DidMessageBody,
1063 #[serde(default, skip_serializing_if = "Option::is_none")]
1065 pub typedid: Option<TypeDidConversation>,
1066 pub kid: String,
1068 pub nonce: String,
1070 pub ciphertext: String,
1072 pub signature: String,
1074}
1075
1076impl DidEnvelope {
1077 pub fn prompt(
1079 id: impl Into<String>,
1080 from: Did,
1081 to: Did,
1082 body: DidMessageBody,
1083 plaintext: impl AsRef<[u8]>,
1084 resolver: &dyn DidResolver,
1085 key_store: &dyn DidKeyStore,
1086 ) -> Result<Self, DidError> {
1087 let id = id.into();
1088 let now = unix_time();
1089 let recipient_document = resolver.resolve(&to)?;
1090 let recipient_key = recipient_document.key_agreement_key()?;
1091 let sender_document = resolver.resolve(&from)?;
1092 let kid = sender_document
1093 .authentication
1094 .first()
1095 .cloned()
1096 .ok_or(DidError::MissingAuthentication)?;
1097 let nonce = random_nonce()?;
1098 let ciphertext = key_store.encrypt_for(
1099 &from,
1100 &recipient_key.public_key()?,
1101 plaintext.as_ref(),
1102 &nonce,
1103 )?;
1104 let mut envelope = Self {
1105 id,
1106 message_type: "https://typesec.dev/did/message/v1/prompt".to_owned(),
1107 from,
1108 to: vec![to],
1109 created_time: now,
1110 expires_time: now + 300,
1111 body,
1112 typedid: None,
1113 kid,
1114 nonce: hex_encode(&nonce),
1115 ciphertext,
1116 signature: String::new(),
1117 };
1118 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
1119 Ok(envelope)
1120 }
1121
1122 pub fn reply(
1124 reply_did: Did,
1125 from: Did,
1126 to: Did,
1127 binding: DidReplyBinding,
1128 plaintext: impl AsRef<[u8]>,
1129 resolver: &dyn DidResolver,
1130 key_store: &dyn DidKeyStore,
1131 ) -> Result<Self, DidError> {
1132 let DidReplyBinding {
1133 prompt_body,
1134 prompt_ref,
1135 } = binding;
1136 let now = unix_time();
1137 let recipient_document = resolver.resolve(&to)?;
1138 let recipient_key = recipient_document.key_agreement_key()?;
1139 let sender_document = resolver.resolve(&from)?;
1140 let kid = sender_document
1141 .authentication
1142 .first()
1143 .cloned()
1144 .ok_or(DidError::MissingAuthentication)?;
1145 let id = reply_did.to_string();
1146 let nonce = random_nonce()?;
1147 let ciphertext = key_store.encrypt_for(
1148 &from,
1149 &recipient_key.public_key()?,
1150 plaintext.as_ref(),
1151 &nonce,
1152 )?;
1153 let mut envelope = Self {
1154 id,
1155 message_type: "https://typesec.dev/did/message/v1/reply".to_owned(),
1156 from,
1157 to: vec![to],
1158 created_time: now,
1159 expires_time: now + 300,
1160 body: DidMessageBody {
1161 action: prompt_body.action.clone(),
1162 resource: prompt_body.resource.clone(),
1163 privacy: prompt_body.privacy.clone(),
1164 reply_to: Some(prompt_ref),
1165 },
1166 typedid: None,
1167 kid,
1168 nonce: hex_encode(&nonce),
1169 ciphertext,
1170 signature: String::new(),
1171 };
1172 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
1173 Ok(envelope)
1174 }
1175
1176 #[allow(clippy::too_many_arguments)]
1178 pub fn typedid(
1179 id: impl Into<String>,
1180 from: Did,
1181 to: Did,
1182 body: DidMessageBody,
1183 typedid: TypeDidConversation,
1184 plaintext: impl AsRef<[u8]>,
1185 resolver: &dyn DidResolver,
1186 key_store: &dyn DidKeyStore,
1187 ) -> Result<Self, DidError> {
1188 let mut envelope = Self::prompt(id, from, to, body, plaintext, resolver, key_store)?;
1189 envelope.message_type = "https://typesec.dev/did/message/v1/typedid".to_owned();
1190 envelope.typedid = Some(typedid);
1191 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
1192 Ok(envelope)
1193 }
1194
1195 pub fn typedid_reply(
1197 id: impl Into<String>,
1198 from: Did,
1199 to: Did,
1200 request: &VerifiedTypeDidMessage,
1201 plaintext: impl AsRef<[u8]>,
1202 resolver: &dyn DidResolver,
1203 key_store: &dyn DidKeyStore,
1204 ) -> Result<Self, DidError> {
1205 let mut body = request.body.clone();
1206 body.reply_to = Some(request.message_ref.clone());
1207 let conversation = TypeDidConversation {
1208 conversation_id: request.conversation.conversation_id.clone(),
1209 mode: TypeDidMode::RequestReply,
1210 profile: request.conversation.profile.clone(),
1211 protocol: request.conversation.protocol.clone(),
1212 expires_at: request.conversation.expires_at,
1213 };
1214 Self::typedid(
1215 id,
1216 from,
1217 to,
1218 body,
1219 conversation,
1220 plaintext,
1221 resolver,
1222 key_store,
1223 )
1224 }
1225
1226 pub fn reference(&self) -> DidMessageReference {
1228 let seed = format!("{}\n{}", self.signing_input(), self.signature);
1229 DidMessageReference {
1230 id: self.id.clone(),
1231 digest: hex_encode(&sha256_tagged(
1232 b"typesec-did-envelope-reference",
1233 seed.as_bytes(),
1234 )),
1235 }
1236 }
1237
1238 fn signing_input(&self) -> String {
1239 let reply_to = self
1240 .body
1241 .reply_to
1242 .as_ref()
1243 .map(|reference| format!("{}\n{}", reference.id, reference.digest))
1244 .unwrap_or_default();
1245 let base = format!(
1246 "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
1247 self.id,
1248 self.message_type,
1249 self.from,
1250 self.to
1251 .iter()
1252 .map(Did::as_str)
1253 .collect::<Vec<_>>()
1254 .join(","),
1255 self.created_time,
1256 self.expires_time,
1257 self.body.action,
1258 self.body.resource,
1259 self.body.privacy,
1260 reply_to
1261 );
1262 if let Some(typedid) = self.typedid.as_ref() {
1263 format!(
1264 "{}\n{}\n{}",
1265 base,
1266 canonical_typedid_conversation(typedid),
1267 self.ciphertext
1268 )
1269 } else {
1270 format!("{}\n{}", base, self.ciphertext)
1271 }
1272 }
1273}
1274
1275#[derive(Debug)]
1277pub struct VerifiedTypeDidMessage {
1278 pub subject: Did,
1280 pub message_ref: DidMessageReference,
1282 pub body: DidMessageBody,
1284 pub conversation: TypeDidConversation,
1286 pub resource: GenericResource,
1288 pub payload: SecureValue<Secret, Vec<u8>, GenericResource>,
1290}
1291
1292#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1299pub struct TypeDidAttestation {
1300 pub subject: Did,
1302 pub envelope_id: String,
1304 pub envelope_digest: String,
1306 pub action: String,
1308 pub resource: String,
1310 pub privacy: String,
1312 pub conversation_id: String,
1314 pub protocol: String,
1316 pub mode: TypeDidMode,
1318 pub profile: String,
1320 #[serde(default, skip_serializing_if = "Option::is_none")]
1322 pub expires_at: Option<u64>,
1323}
1324
1325impl VerifiedTypeDidMessage {
1326 pub fn attestation(&self) -> TypeDidAttestation {
1328 TypeDidAttestation {
1329 subject: self.subject.clone(),
1330 envelope_id: self.message_ref.id.clone(),
1331 envelope_digest: self.message_ref.digest.clone(),
1332 action: self.body.action.clone(),
1333 resource: self.body.resource.clone(),
1334 privacy: self.body.privacy.clone(),
1335 conversation_id: self.conversation.conversation_id.clone(),
1336 protocol: self.conversation.protocol.clone(),
1337 mode: self.conversation.mode,
1338 profile: self.conversation.profile.clone(),
1339 expires_at: self.conversation.expires_at,
1340 }
1341 }
1342}
1343
1344#[derive(Debug)]
1346pub struct VerifiedDidPrompt {
1347 pub subject: Did,
1349 pub prompt_ref: DidMessageReference,
1351 pub body: DidMessageBody,
1353 pub resource: GenericResource,
1355 pub prompt: SecureValue<Secret, String, GenericResource>,
1357}
1358
1359pub struct DidMessageGateway {
1361 resolver: Arc<dyn DidResolver>,
1362 key_store: Arc<dyn DidKeyStore>,
1363 recipient: Did,
1364}
1365
1366impl DidMessageGateway {
1367 pub fn new(
1369 resolver: Arc<dyn DidResolver>,
1370 key_store: Arc<dyn DidKeyStore>,
1371 recipient: Did,
1372 ) -> Self {
1373 Self {
1374 resolver,
1375 key_store,
1376 recipient,
1377 }
1378 }
1379
1380 pub fn open_prompt(&self, envelope: &DidEnvelope) -> Result<VerifiedDidPrompt, DidError> {
1382 let opened = self.open_bytes(envelope)?;
1383 let prompt = String::from_utf8(opened.plaintext).map_err(|_| DidError::InvalidUtf8)?;
1384 Ok(VerifiedDidPrompt {
1385 subject: opened.subject,
1386 prompt_ref: opened.message_ref,
1387 body: opened.body,
1388 prompt: SecureValue::protect(prompt, &opened.resource),
1389 resource: opened.resource,
1390 })
1391 }
1392
1393 fn open_bytes(&self, envelope: &DidEnvelope) -> Result<OpenedDidEnvelope, DidError> {
1394 if !envelope.to.iter().any(|did| did == &self.recipient) {
1395 return Err(DidError::WrongRecipient(self.recipient.to_string()));
1396 }
1397 let now = unix_time();
1398 if envelope.expires_time < now {
1399 return Err(DidError::Expired);
1400 }
1401
1402 let sender_document = self.resolver.resolve(&envelope.from)?;
1403 let sender_key = sender_document.authentication_key(&envelope.kid)?;
1404 self.key_store.verify(
1405 sender_key,
1406 envelope.signing_input().as_bytes(),
1407 &envelope.signature,
1408 )?;
1409
1410 let sender_agreement_keys = sender_document.key_agreement_keys()?;
1416 let nonce = hex_decode(&envelope.nonce)?;
1417 let mut plaintext = None;
1418 for sender_agreement_key in sender_agreement_keys {
1419 match self.key_store.decrypt_for(
1420 &self.recipient,
1421 &sender_agreement_key.public_key()?,
1422 &nonce,
1423 &envelope.ciphertext,
1424 ) {
1425 Ok(opened) => {
1426 plaintext = Some(opened);
1427 break;
1428 }
1429 Err(DidError::DecryptionFailed) => {}
1430 Err(err) => return Err(err),
1431 }
1432 }
1433 let plaintext = plaintext.ok_or(DidError::DecryptionFailed)?;
1434 let resource = GenericResource::new(&envelope.body.resource, "did-prompt");
1435
1436 Ok(OpenedDidEnvelope {
1437 subject: envelope.from.clone(),
1438 message_ref: envelope.reference(),
1439 body: envelope.body.clone(),
1440 resource,
1441 plaintext,
1442 })
1443 }
1444}
1445
1446#[derive(Debug)]
1447struct OpenedDidEnvelope {
1448 subject: Did,
1449 message_ref: DidMessageReference,
1450 body: DidMessageBody,
1451 resource: GenericResource,
1452 plaintext: Vec<u8>,
1453}
1454
1455pub struct TypeDidGateway {
1457 inner: DidMessageGateway,
1458}
1459
1460impl TypeDidGateway {
1461 pub fn new(
1463 resolver: Arc<dyn DidResolver>,
1464 key_store: Arc<dyn DidKeyStore>,
1465 recipient: Did,
1466 ) -> Self {
1467 Self {
1468 inner: DidMessageGateway::new(resolver, key_store, recipient),
1469 }
1470 }
1471
1472 pub fn open_message(&self, envelope: &DidEnvelope) -> Result<VerifiedTypeDidMessage, DidError> {
1474 let conversation = envelope
1475 .typedid
1476 .clone()
1477 .ok_or(DidError::MissingTypeDidMetadata)?;
1478 let opened = self.inner.open_bytes(envelope)?;
1479 Ok(VerifiedTypeDidMessage {
1480 subject: opened.subject,
1481 message_ref: opened.message_ref,
1482 body: opened.body,
1483 conversation,
1484 payload: SecureValue::protect(opened.plaintext, &opened.resource),
1485 resource: opened.resource,
1486 })
1487 }
1488}
1489
1490pub trait SecureEnvelopeAdapter {
1492 fn protocol(&self) -> &str;
1494
1495 fn content_type(&self) -> &'static str {
1497 "application/vnd.typedid.envelope+json"
1498 }
1499
1500 fn wrap(
1502 &self,
1503 request: TypeDidWrapRequest<'_>,
1504 resolver: &dyn DidResolver,
1505 key_store: &dyn DidKeyStore,
1506 ) -> Result<DidEnvelope, DidError> {
1507 let profile = TypeDidProfile::negotiate(
1508 request.local_profiles,
1509 request.remote_profiles,
1510 self.protocol(),
1511 request.mode,
1512 )?;
1513 let conversation = TypeDidConversation::new(
1514 request.conversation_id,
1515 request.mode,
1516 profile.id.clone(),
1517 self.protocol(),
1518 );
1519 DidEnvelope::typedid(
1520 request.id,
1521 request.from,
1522 request.to,
1523 request.body,
1524 conversation,
1525 request.payload,
1526 resolver,
1527 key_store,
1528 )
1529 }
1530}
1531
1532pub struct TypeDidWrapRequest<'a> {
1534 pub id: String,
1536 pub from: Did,
1538 pub to: Did,
1540 pub conversation_id: String,
1542 pub mode: TypeDidMode,
1544 pub body: DidMessageBody,
1546 pub payload: &'a [u8],
1548 pub local_profiles: &'a [TypeDidProfile],
1550 pub remote_profiles: &'a [TypeDidProfile],
1552}
1553
1554#[derive(Debug, Default, Clone, Copy)]
1556pub struct A2aTypeDidAdapter;
1557
1558impl SecureEnvelopeAdapter for A2aTypeDidAdapter {
1559 fn protocol(&self) -> &str {
1560 "a2a"
1561 }
1562}
1563
1564#[derive(Debug, Default, Clone, Copy)]
1566pub struct AcpTypeDidAdapter;
1567
1568impl SecureEnvelopeAdapter for AcpTypeDidAdapter {
1569 fn protocol(&self) -> &str {
1570 "acp"
1571 }
1572}
1573
1574#[derive(Debug, Default, Clone, Copy)]
1576pub struct BandSecureEnvelopeAdapter;
1577
1578impl SecureEnvelopeAdapter for BandSecureEnvelopeAdapter {
1579 fn protocol(&self) -> &str {
1580 "band"
1581 }
1582}
1583
1584#[derive(Debug, Default, Clone, Copy)]
1586pub struct HttpTypeDidAdapter;
1587
1588impl SecureEnvelopeAdapter for HttpTypeDidAdapter {
1589 fn protocol(&self) -> &str {
1590 "https"
1591 }
1592}
1593
1594pub struct DidOllamaClient {
1596 base_url: String,
1597 model: String,
1598 http: Arc<dyn HttpClient>,
1599}
1600
1601impl DidOllamaClient {
1602 pub fn new(base_url: impl Into<String>, model: impl Into<String>) -> Self {
1604 Self::with_http(base_url, model, Arc::new(ReqwestHttpClient::new()))
1605 }
1606
1607 pub fn with_http(
1609 base_url: impl Into<String>,
1610 model: impl Into<String>,
1611 http: Arc<dyn HttpClient>,
1612 ) -> Self {
1613 Self {
1614 base_url: base_url.into().trim_end_matches('/').to_owned(),
1615 model: model.into(),
1616 http,
1617 }
1618 }
1619
1620 pub fn chat_verified_prompt(
1622 &self,
1623 prompt: VerifiedDidPrompt,
1624 _infer: &Capability<AiCanInfer, GenericResource>,
1625 read: &Capability<CanReadSensitive, GenericResource>,
1626 ) -> Result<Value, DidError> {
1627 let plaintext = prompt.prompt.reveal(read)?;
1628 let body = json!({
1629 "model": self.model,
1630 "stream": false,
1631 "messages": [{
1632 "role": "user",
1633 "content": plaintext
1634 }]
1635 });
1636 self.http
1637 .post_json(&format!("{}/api/chat", self.base_url), &[], &body)
1638 .map_err(DidError::Http)
1639 }
1640
1641 pub fn chat_verified_prompt_bound(
1643 &self,
1644 prompt: VerifiedDidPrompt,
1645 reply_from: Did,
1646 resolver: &dyn DidResolver,
1647 key_store: &dyn DidKeyStore,
1648 _infer: &Capability<AiCanInfer, GenericResource>,
1649 read: &Capability<CanReadSensitive, GenericResource>,
1650 ) -> Result<DidEnvelope, DidError> {
1651 let reply_to = prompt.subject.clone();
1652 let binding = DidReplyBinding::for_prompt(&prompt);
1653 let plaintext = prompt.prompt.reveal(read)?;
1654 let body = json!({
1655 "model": self.model,
1656 "stream": false,
1657 "messages": [{
1658 "role": "user",
1659 "content": plaintext
1660 }]
1661 });
1662 let response = self
1663 .http
1664 .post_json(&format!("{}/api/chat", self.base_url), &[], &body)
1665 .map_err(DidError::Http)?;
1666 let reply = ollama_reply_content(&response)?;
1667 let reply_did = Did::key(sha256_tagged(
1668 b"typesec-did-ollama-reply",
1669 format!("{}\n{}", binding.prompt_ref.digest, reply).as_bytes(),
1670 ));
1671 DidEnvelope::reply(
1672 reply_did, reply_from, reply_to, binding, reply, resolver, key_store,
1673 )
1674 }
1675
1676 pub fn chat_wrapped_prompt(&self, envelope: &DidEnvelope) -> Result<Value, DidError> {
1678 let body = json!({
1679 "model": self.model,
1680 "stream": false,
1681 "did_envelope": envelope
1682 });
1683 self.http
1684 .post_json(&format!("{}/api/chat", self.base_url), &[], &body)
1685 .map_err(DidError::Http)
1686 }
1687}
1688
1689#[derive(Debug, thiserror::Error)]
1691pub enum DidError {
1692 #[error("invalid DID: {0}")]
1694 InvalidDid(String),
1695 #[error("unresolved DID: {0}")]
1697 Unresolved(String),
1698 #[error("missing private key for DID: {0}")]
1700 MissingPrivateKey(String),
1701 #[error("DID document has no authentication key")]
1703 MissingAuthentication,
1704 #[error("DID document has no key agreement key")]
1706 MissingKeyAgreement,
1707 #[error("missing verification method: {0}")]
1709 MissingVerificationMethod(String),
1710 #[error("missing key version {version} for DID {did}")]
1712 MissingKeyVersion {
1713 did: String,
1715 version: u64,
1717 },
1718 #[error("cannot retire active key version {version} for DID {did}")]
1720 CannotRetireActiveKey {
1721 did: String,
1723 version: u64,
1725 },
1726 #[error("retired verification method: {0}")]
1728 RetiredKey(String),
1729 #[error("invalid DID envelope signature")]
1731 InvalidSignature,
1732 #[error("DID envelope was not addressed to {0}")]
1734 WrongRecipient(String),
1735 #[error("DID envelope has expired")]
1737 Expired,
1738 #[error("invalid key material: {0}")]
1740 InvalidKey(String),
1741 #[error("invalid nonce: expected 12 bytes")]
1743 InvalidNonce,
1744 #[error("DID payload encryption failed")]
1746 EncryptionFailed,
1747 #[error("DID payload decryption failed")]
1749 DecryptionFailed,
1750 #[error("key generation failed: {0}")]
1752 KeyGen(String),
1753 #[error("capability does not cover this payload: {0}")]
1755 Capability(#[from] typesec_core::secure_value::SecureAccessError),
1756 #[error("invalid hex encoding")]
1758 InvalidHex,
1759 #[error("decrypted DID payload is not valid UTF-8")]
1761 InvalidUtf8,
1762 #[error("DID HTTP integration failed: {0}")]
1764 Http(Box<dyn std::error::Error + Send + Sync>),
1765 #[error("Ollama response did not contain message.content")]
1767 MissingOllamaReply,
1768 #[error("DID envelope is missing TypeDID metadata")]
1770 MissingTypeDidMetadata,
1771 #[error("no compatible TypeDID profile")]
1773 NoCompatibleTypeDidProfile,
1774}
1775
1776fn ollama_reply_content(response: &Value) -> Result<&str, DidError> {
1777 response
1778 .get("message")
1779 .and_then(|message| message.get("content"))
1780 .and_then(Value::as_str)
1781 .ok_or(DidError::MissingOllamaReply)
1782}
1783
1784fn unix_time() -> u64 {
1785 SystemTime::now()
1786 .duration_since(UNIX_EPOCH)
1787 .map(|duration| duration.as_secs())
1788 .unwrap_or_default()
1789}
1790
1791fn sha256_tagged(domain: &[u8], data: &[u8]) -> [u8; 32] {
1793 use sha2::Digest;
1794 let mut hasher = sha2::Sha256::new();
1795 hasher.update(domain);
1796 hasher.update([0u8]);
1797 hasher.update(data);
1798 hasher.finalize().into()
1799}
1800
1801fn random_nonce() -> Result<[u8; 12], DidError> {
1803 let mut nonce = [0u8; 12];
1804 getrandom::getrandom(&mut nonce).map_err(|e| DidError::KeyGen(e.to_string()))?;
1805 Ok(nonce)
1806}
1807
1808fn canonical_typedid_conversation(conversation: &TypeDidConversation) -> String {
1809 format!(
1810 "{}\n{:?}\n{}\n{}\n{}",
1811 conversation.conversation_id,
1812 conversation.mode,
1813 conversation.profile,
1814 conversation.protocol,
1815 conversation
1816 .expires_at
1817 .map(|expires_at| expires_at.to_string())
1818 .unwrap_or_default()
1819 )
1820}
1821
1822fn contains(values: &[String], needle: &str) -> bool {
1823 values.iter().any(|value| value == needle)
1824}
1825
1826fn intersects(left: &[String], right: &[String]) -> bool {
1827 left.iter().any(|value| right.contains(value))
1828}
1829
1830#[cfg(any(test, feature = "demo-crypto"))]
1831fn derive_shared_key(private_key: &[u8], public_key: &[u8], nonce: &[u8]) -> Vec<u8> {
1832 let mut seed = Vec::with_capacity(private_key.len() + public_key.len() + nonce.len());
1833 if private_key <= public_key {
1834 seed.extend_from_slice(private_key);
1835 seed.extend_from_slice(public_key);
1836 } else {
1837 seed.extend_from_slice(public_key);
1838 seed.extend_from_slice(private_key);
1839 }
1840 seed.extend_from_slice(nonce);
1841 derive_bytes(b"typesec-did-shared", &seed, 32)
1842}
1843
1844#[cfg(any(test, feature = "demo-crypto"))]
1846fn derive_bytes(domain: &[u8], seed: &[u8], len: usize) -> Vec<u8> {
1847 let mut out = Vec::with_capacity(len);
1848 let mut state: u64 = 0xcbf29ce484222325;
1849 for byte in domain.iter().chain(seed) {
1850 state ^= u64::from(*byte);
1851 state = state.wrapping_mul(0x100000001b3);
1852 }
1853 while out.len() < len {
1854 state ^= state >> 12;
1855 state ^= state << 25;
1856 state ^= state >> 27;
1857 state = state.wrapping_mul(0x2545f4914f6cdd1d);
1858 out.extend_from_slice(&state.to_le_bytes());
1859 }
1860 out.truncate(len);
1861 out
1862}
1863
1864#[cfg(any(test, feature = "demo-crypto"))]
1865fn xor_stream(input: &[u8], key: &[u8]) -> Vec<u8> {
1866 input
1867 .iter()
1868 .enumerate()
1869 .map(|(idx, byte)| byte ^ key[idx % key.len()])
1870 .collect()
1871}
1872
1873#[cfg(any(test, feature = "demo-crypto"))]
1874fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
1875 if left.len() != right.len() {
1876 return false;
1877 }
1878 left.iter()
1879 .zip(right)
1880 .fold(0u8, |acc, (a, b)| acc | (a ^ b))
1881 == 0
1882}
1883
1884fn hex_encode(bytes: &[u8]) -> String {
1885 const HEX: &[u8; 16] = b"0123456789abcdef";
1886 let mut out = String::with_capacity(bytes.len() * 2);
1887 for byte in bytes {
1888 out.push(HEX[(byte >> 4) as usize] as char);
1889 out.push(HEX[(byte & 0x0f) as usize] as char);
1890 }
1891 out
1892}
1893
1894fn hex_decode(value: &str) -> Result<Vec<u8>, DidError> {
1895 if !value.len().is_multiple_of(2) {
1896 return Err(DidError::InvalidHex);
1897 }
1898 let mut out = Vec::with_capacity(value.len() / 2);
1899 for chunk in value.as_bytes().chunks_exact(2) {
1900 let high = hex_nibble(chunk[0])?;
1901 let low = hex_nibble(chunk[1])?;
1902 out.push((high << 4) | low);
1903 }
1904 Ok(out)
1905}
1906
1907fn hex_nibble(byte: u8) -> Result<u8, DidError> {
1908 match byte {
1909 b'0'..=b'9' => Ok(byte - b'0'),
1910 b'a'..=b'f' => Ok(byte - b'a' + 10),
1911 b'A'..=b'F' => Ok(byte - b'A' + 10),
1912 _ => Err(DidError::InvalidHex),
1913 }
1914}
1915
1916#[cfg(test)]
1917mod tests {
1918 use std::sync::Arc;
1919
1920 use serde_json::json;
1921 use typesec_core::{
1922 PolicyEngine, Resource, ResourceId, SubjectId,
1923 permissions::{AiCanInfer, CanReadSensitive},
1924 policy::{PolicyResult, mint_capability},
1925 };
1926
1927 use super::*;
1928 use crate::http::RecordingHttpClient;
1929
1930 struct PromptPolicy;
1931
1932 impl PolicyEngine for PromptPolicy {
1933 fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
1934 let subject = subject.as_str();
1935 let resource = resource.as_str();
1936 if subject == "did:key:z616c696365"
1937 && matches!(action, "ai:infer" | "read_sensitive")
1938 && resource == "prompt/session/123"
1939 {
1940 PolicyResult::Allow
1941 } else {
1942 PolicyResult::Deny("not allowed".to_owned())
1943 }
1944 }
1945 }
1946
1947 fn fixture() -> (Did, Did, StaticDidResolver, DemoDidKeyStore) {
1948 let alice = Did::key(b"alice");
1949 let agent = Did::key(b"agent");
1950 let alice_key = DemoDidKeyPair::from_seed(b"alice");
1951 let agent_key = DemoDidKeyPair::from_seed(b"agent");
1952 let resolver = StaticDidResolver::new()
1953 .with_document(DidDocument::single_key(
1954 alice.clone(),
1955 alice_key.public_key.clone(),
1956 ))
1957 .with_document(DidDocument::single_key(
1958 agent.clone(),
1959 agent_key.public_key.clone(),
1960 ));
1961 let keys = DemoDidKeyStore::new()
1962 .with_key(alice.clone(), alice_key)
1963 .with_key(agent.clone(), agent_key);
1964 (alice, agent, resolver, keys)
1965 }
1966
1967 struct AgentPolicy {
1968 allowed_subject: String,
1969 }
1970
1971 impl PolicyEngine for AgentPolicy {
1972 fn check(&self, subject: &SubjectId, action: &str, resource: &ResourceId) -> PolicyResult {
1973 let subject = subject.as_str();
1974 let resource = resource.as_str();
1975 if subject == self.allowed_subject
1976 && matches!(
1977 action,
1978 "agent:message" | "agent:delegate" | "read_sensitive"
1979 )
1980 && resource == "room/acme-support"
1981 {
1982 PolicyResult::Allow
1983 } else {
1984 PolicyResult::Deny("agent message denied".to_owned())
1985 }
1986 }
1987 }
1988
1989 #[test]
1990 fn dids_parse_and_reject_bad_values() {
1991 assert!(Did::parse("did:web:example.com").is_ok());
1992 assert!(Did::parse("not-a-did").is_err());
1993 assert_eq!(
1994 Did::web("typesec.dev").unwrap().as_str(),
1995 "did:web:typesec.dev"
1996 );
1997 }
1998
1999 #[test]
2000 fn encrypted_prompt_opens_as_secret_secure_value() {
2001 let (alice, agent, resolver, keys) = fixture();
2002 let envelope = DidEnvelope::prompt(
2003 "msg-1",
2004 alice.clone(),
2005 agent.clone(),
2006 DidMessageBody::infer_prompt("prompt/session/123"),
2007 "summarize this confidential record",
2008 &resolver,
2009 &keys,
2010 )
2011 .expect("envelope");
2012 assert_ne!(envelope.ciphertext, "summarize this confidential record");
2013
2014 let gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2015 let verified = gateway.open_prompt(&envelope).expect("verified prompt");
2016 assert_eq!(verified.subject, alice);
2017 assert_eq!(verified.resource.resource_id(), "prompt/session/123");
2018 assert_eq!(
2019 SecureValue::<Secret, String, GenericResource>::label_name(),
2020 "secret"
2021 );
2022
2023 let infer = mint_capability::<AiCanInfer, _>(
2024 &PromptPolicy,
2025 verified.subject.as_str(),
2026 &verified.resource,
2027 )
2028 .expect("infer cap");
2029 let read = mint_capability::<CanReadSensitive, _>(
2030 &PromptPolicy,
2031 verified.subject.as_str(),
2032 &verified.resource,
2033 )
2034 .expect("read cap");
2035 assert_eq!(infer.resource_id(), "prompt/session/123");
2036 assert_eq!(
2037 verified.prompt.reveal(&read).expect("matching resource"),
2038 "summarize this confidential record"
2039 );
2040 }
2041
2042 #[test]
2043 fn did_ollama_client_sends_plaintext_only_after_capabilities() {
2044 let (alice, agent, resolver, keys) = fixture();
2045 let envelope = DidEnvelope::prompt(
2046 "msg-1",
2047 alice,
2048 agent.clone(),
2049 DidMessageBody::infer_prompt("prompt/session/123"),
2050 "private prompt",
2051 &resolver,
2052 &keys,
2053 )
2054 .expect("envelope");
2055 let gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2056 let verified = gateway.open_prompt(&envelope).expect("verified prompt");
2057 let infer = mint_capability::<AiCanInfer, _>(
2058 &PromptPolicy,
2059 verified.subject.as_str(),
2060 &verified.resource,
2061 )
2062 .expect("infer cap");
2063 let read = mint_capability::<CanReadSensitive, _>(
2064 &PromptPolicy,
2065 verified.subject.as_str(),
2066 &verified.resource,
2067 )
2068 .expect("read cap");
2069
2070 let http = RecordingHttpClient::new().with_response(
2071 "http://localhost:11434/api/chat",
2072 json!({ "message": { "content": "ok" } }),
2073 );
2074 let client = DidOllamaClient::with_http(
2075 "http://localhost:11434",
2076 "llama3.2",
2077 Arc::new(http.clone()),
2078 );
2079 let response = client
2080 .chat_verified_prompt(verified, &infer, &read)
2081 .expect("ollama call");
2082
2083 assert_eq!(response["message"]["content"], "ok");
2084 let requests = http.requests();
2085 assert_eq!(requests.len(), 1);
2086 assert_eq!(requests[0].url, "http://localhost:11434/api/chat");
2087 assert_eq!(
2088 requests[0].body.as_ref().unwrap()["messages"][0]["content"],
2089 "private prompt"
2090 );
2091 }
2092
2093 #[test]
2094 fn typedid_profile_negotiates_on_protocol_and_mode() {
2095 let local = vec![TypeDidProfile::ed25519_x25519_chacha20()];
2096 let remote = vec![TypeDidProfile::ed25519_x25519_chacha20()];
2097 let selected = TypeDidProfile::negotiate(&local, &remote, "a2a", TypeDidMode::RequestReply)
2098 .expect("compatible profile");
2099 assert_eq!(selected.id, "typedid/v1/x25519-chacha20poly1305-ed25519");
2100
2101 assert!(matches!(
2102 TypeDidProfile::negotiate(&local, &remote, "smtp", TypeDidMode::Send),
2103 Err(DidError::NoCompatibleTypeDidProfile)
2104 ));
2105 }
2106
2107 #[test]
2108 fn typedid_adapter_wraps_and_gateway_opens_opaque_payload() {
2109 let (alice, agent, resolver, keys) = fixture();
2110 let profiles = vec![TypeDidProfile::ed25519_x25519_chacha20()];
2111 let adapter = A2aTypeDidAdapter;
2112 let payload =
2113 br#"{"jsonrpc":"2.0","method":"message/send","params":{"text":"triage case"}}"#;
2114
2115 let envelope = adapter
2116 .wrap(
2117 TypeDidWrapRequest {
2118 id: "a2a-msg-1".to_owned(),
2119 from: alice.clone(),
2120 to: agent.clone(),
2121 conversation_id: "task/a2a-123".to_owned(),
2122 mode: TypeDidMode::RequestReply,
2123 body: DidMessageBody::agent_delegate("room/acme-support", "secret"),
2124 payload,
2125 local_profiles: &profiles,
2126 remote_profiles: &profiles,
2127 },
2128 &resolver,
2129 &keys,
2130 )
2131 .expect("wrapped envelope");
2132
2133 assert_eq!(
2134 adapter.content_type(),
2135 "application/vnd.typedid.envelope+json"
2136 );
2137 assert_eq!(
2138 envelope.message_type,
2139 "https://typesec.dev/did/message/v1/typedid"
2140 );
2141 assert_eq!(envelope.typedid.as_ref().unwrap().protocol, "a2a");
2142 assert_ne!(envelope.ciphertext.as_bytes(), payload);
2143
2144 let gateway = TypeDidGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2145 let verified = gateway.open_message(&envelope).expect("verified typedid");
2146 assert_eq!(verified.subject, alice);
2147 assert_eq!(verified.conversation.conversation_id, "task/a2a-123");
2148 assert_eq!(verified.body.action, "agent:delegate");
2149
2150 let read = mint_capability::<CanReadSensitive, _>(
2151 &AgentPolicy {
2152 allowed_subject: verified.subject.to_string(),
2153 },
2154 verified.subject.as_str(),
2155 &verified.resource,
2156 )
2157 .expect("read cap");
2158 assert_eq!(verified.payload.reveal(&read).expect("payload"), payload);
2159 }
2160
2161 #[test]
2162 fn typedid_verified_message_exposes_audit_safe_attestation() {
2163 let (alice, agent, resolver, keys) = ed25519_fixture();
2164 let envelope = DidEnvelope::typedid(
2165 "typedid-attestation-1",
2166 alice.clone(),
2167 agent.clone(),
2168 DidMessageBody::agent_message("lakecat:table:events", "internal"),
2169 TypeDidConversation::new(
2170 "conversation-1",
2171 TypeDidMode::RequestReply,
2172 TypeDidProfile::ed25519_x25519_chacha20().id,
2173 "a2a",
2174 )
2175 .with_expires_at(unix_time() + 300),
2176 b"secret payload",
2177 &resolver,
2178 &keys,
2179 )
2180 .expect("typedid envelope");
2181 let gateway = TypeDidGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2182 let verified = gateway.open_message(&envelope).expect("verified typedid");
2183 let attestation = verified.attestation();
2184
2185 assert_eq!(attestation.subject, alice);
2186 assert_eq!(attestation.envelope_id, "typedid-attestation-1");
2187 assert_eq!(attestation.action, "agent:message");
2188 assert_eq!(attestation.resource, "lakecat:table:events");
2189 assert_eq!(attestation.privacy, "internal");
2190 assert_eq!(attestation.protocol, "a2a");
2191 assert_eq!(attestation.mode, TypeDidMode::RequestReply);
2192 let serialized = serde_json::to_string(&attestation).unwrap();
2193 assert!(!serialized.contains("secret payload"));
2194 assert!(!serialized.contains(&envelope.signature));
2195 }
2196
2197 #[test]
2198 fn typedid_reply_is_bound_to_request_envelope() {
2199 let (alice, agent, resolver, keys) = fixture();
2200 let request = DidEnvelope::typedid(
2201 "band-room-msg-1",
2202 alice.clone(),
2203 agent.clone(),
2204 DidMessageBody::agent_message("room/acme-support", "secret"),
2205 TypeDidConversation::new(
2206 "room/acme-support",
2207 TypeDidMode::RequestReply,
2208 TypeDidProfile::ed25519_x25519_chacha20().id,
2209 "band",
2210 ),
2211 b"please coordinate with the support agent",
2212 &resolver,
2213 &keys,
2214 )
2215 .expect("request envelope");
2216 let request_ref = request.reference();
2217 let gateway = TypeDidGateway::new(
2218 Arc::new(resolver.clone()),
2219 Arc::new(keys.clone()),
2220 agent.clone(),
2221 );
2222 let verified = gateway.open_message(&request).expect("verified request");
2223 let reply = DidEnvelope::typedid_reply(
2224 "band-room-reply-1",
2225 agent.clone(),
2226 alice.clone(),
2227 &verified,
2228 b"support agent accepted the handoff",
2229 &resolver,
2230 &keys,
2231 )
2232 .expect("reply envelope");
2233
2234 assert_eq!(reply.typedid.as_ref().unwrap().protocol, "band");
2235 assert_eq!(reply.body.reply_to, Some(request_ref));
2236
2237 let reply_gateway = TypeDidGateway::new(Arc::new(resolver), Arc::new(keys), alice);
2238 let opened_reply = reply_gateway.open_message(&reply).expect("opened reply");
2239 assert_eq!(opened_reply.subject, agent);
2240 }
2241
2242 #[test]
2243 fn typedid_signature_covers_conversation_metadata() {
2244 let (alice, agent, resolver, keys) = fixture();
2245 let mut envelope = DidEnvelope::typedid(
2246 "acp-session-msg-1",
2247 alice,
2248 agent.clone(),
2249 DidMessageBody::agent_message("room/acme-support", "secret"),
2250 TypeDidConversation::new(
2251 "session/editor-1",
2252 TypeDidMode::Send,
2253 TypeDidProfile::ed25519_x25519_chacha20().id,
2254 "acp",
2255 ),
2256 b"review this private diff",
2257 &resolver,
2258 &keys,
2259 )
2260 .expect("typedid envelope");
2261 envelope.typedid.as_mut().unwrap().protocol = "band".to_owned();
2262
2263 let gateway = TypeDidGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2264 assert!(matches!(
2265 gateway.open_message(&envelope),
2266 Err(DidError::InvalidSignature)
2267 ));
2268 }
2269
2270 #[test]
2271 fn bound_ollama_reply_creates_signed_reply_envelope_for_prompt() {
2272 let (alice, agent, resolver, keys) = fixture();
2273 let prompt_envelope = DidEnvelope::prompt(
2274 "msg-1",
2275 alice.clone(),
2276 agent.clone(),
2277 DidMessageBody::infer_prompt("prompt/session/123"),
2278 "private prompt",
2279 &resolver,
2280 &keys,
2281 )
2282 .expect("prompt envelope");
2283 let prompt_ref = prompt_envelope.reference();
2284 let gateway = DidMessageGateway::new(
2285 Arc::new(resolver.clone()),
2286 Arc::new(keys.clone()),
2287 agent.clone(),
2288 );
2289 let verified = gateway
2290 .open_prompt(&prompt_envelope)
2291 .expect("verified prompt");
2292 let infer = mint_capability::<AiCanInfer, _>(
2293 &PromptPolicy,
2294 verified.subject.as_str(),
2295 &verified.resource,
2296 )
2297 .expect("infer cap");
2298 let read = mint_capability::<CanReadSensitive, _>(
2299 &PromptPolicy,
2300 verified.subject.as_str(),
2301 &verified.resource,
2302 )
2303 .expect("read cap");
2304
2305 let http = RecordingHttpClient::new().with_response(
2306 "http://localhost:11434/api/chat",
2307 json!({ "message": { "content": "bound reply" } }),
2308 );
2309 let client = DidOllamaClient::with_http(
2310 "http://localhost:11434",
2311 "llama3.2",
2312 Arc::new(http.clone()),
2313 );
2314 let reply_envelope = client
2315 .chat_verified_prompt_bound(verified, agent.clone(), &resolver, &keys, &infer, &read)
2316 .expect("bound reply");
2317
2318 assert!(reply_envelope.id.starts_with("did:key:z"));
2319 assert_eq!(
2320 reply_envelope.message_type,
2321 "https://typesec.dev/did/message/v1/reply"
2322 );
2323 assert_eq!(reply_envelope.from, agent);
2324 assert_eq!(reply_envelope.to, vec![alice.clone()]);
2325 assert_eq!(reply_envelope.body.resource, "prompt/session/123");
2326 assert_eq!(reply_envelope.body.privacy, "secret");
2327 assert_eq!(reply_envelope.body.reply_to, Some(prompt_ref));
2328 assert_ne!(reply_envelope.ciphertext, "bound reply");
2329
2330 let reply_gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), alice);
2331 let opened_reply = reply_gateway
2332 .open_prompt(&reply_envelope)
2333 .expect("verified reply");
2334 assert_eq!(opened_reply.subject, reply_envelope.from);
2335 assert_eq!(
2336 opened_reply
2337 .prompt
2338 .reveal(&read)
2339 .expect("matching resource"),
2340 "bound reply"
2341 );
2342 }
2343
2344 #[test]
2345 fn reply_signature_covers_prompt_reference() {
2346 let (alice, agent, resolver, keys) = fixture();
2347 let prompt_envelope = DidEnvelope::prompt(
2348 "msg-1",
2349 alice.clone(),
2350 agent.clone(),
2351 DidMessageBody::infer_prompt("prompt/session/123"),
2352 "private prompt",
2353 &resolver,
2354 &keys,
2355 )
2356 .expect("prompt envelope");
2357 let gateway = DidMessageGateway::new(
2358 Arc::new(resolver.clone()),
2359 Arc::new(keys.clone()),
2360 agent.clone(),
2361 );
2362 let verified = gateway
2363 .open_prompt(&prompt_envelope)
2364 .expect("verified prompt");
2365 let mut reply_envelope = DidEnvelope::reply(
2366 Did::key(b"reply-1"),
2367 agent,
2368 alice.clone(),
2369 DidReplyBinding::for_prompt(&verified),
2370 "bound reply",
2371 &resolver,
2372 &keys,
2373 )
2374 .expect("reply envelope");
2375 reply_envelope
2376 .body
2377 .reply_to
2378 .as_mut()
2379 .expect("prompt reference")
2380 .digest = "tampered".to_owned();
2381
2382 let reply_gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), alice);
2383 assert!(matches!(
2384 reply_gateway.open_prompt(&reply_envelope),
2385 Err(DidError::InvalidSignature)
2386 ));
2387 }
2388
2389 fn ed25519_fixture() -> (Did, Did, StaticDidResolver, Ed25519DidKeyStore) {
2390 let alice_key = Ed25519DidKey::from_seed(b"alice-ed25519");
2391 let agent_key = Ed25519DidKey::from_seed(b"agent-ed25519");
2392 let alice = Did::key(alice_key.signing_public());
2393 let agent = Did::key(agent_key.signing_public());
2394 let resolver = StaticDidResolver::new()
2395 .with_document(alice_key.document(alice.clone()))
2396 .with_document(agent_key.document(agent.clone()));
2397 let keys = Ed25519DidKeyStore::new()
2398 .with_key(alice.clone(), alice_key)
2399 .with_key(agent.clone(), agent_key);
2400 (alice, agent, resolver, keys)
2401 }
2402
2403 #[test]
2404 fn ed25519_envelope_roundtrip() {
2405 let (alice, agent, resolver, keys) = ed25519_fixture();
2406 let envelope = DidEnvelope::prompt(
2407 "msg-ed-1",
2408 alice.clone(),
2409 agent.clone(),
2410 DidMessageBody::infer_prompt("prompt/session/ed"),
2411 "confidential prompt over real crypto",
2412 &resolver,
2413 &keys,
2414 )
2415 .expect("envelope");
2416 assert_ne!(envelope.ciphertext, "confidential prompt over real crypto");
2417
2418 let gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2419 let verified = gateway.open_prompt(&envelope).expect("verified prompt");
2420 assert_eq!(verified.subject, alice);
2421
2422 let cap: typesec_core::Capability<CanReadSensitive, GenericResource> = mint_capability(
2423 &AllowAllForTest,
2424 verified.subject.as_str(),
2425 &verified.resource,
2426 )
2427 .expect("read cap");
2428 assert_eq!(
2429 verified.prompt.reveal(&cap).expect("matching resource"),
2430 "confidential prompt over real crypto"
2431 );
2432 }
2433
2434 #[test]
2435 fn ed25519_rejects_tampered_envelope() {
2436 let (alice, agent, resolver, keys) = ed25519_fixture();
2437 let mut envelope = DidEnvelope::prompt(
2438 "msg-ed-2",
2439 alice,
2440 agent.clone(),
2441 DidMessageBody::infer_prompt("prompt/session/ed"),
2442 "payload",
2443 &resolver,
2444 &keys,
2445 )
2446 .expect("envelope");
2447 envelope.body.resource = "prompt/session/other".to_owned();
2448
2449 let gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2450 assert!(matches!(
2451 gateway.open_prompt(&envelope),
2452 Err(DidError::InvalidSignature)
2453 ));
2454 }
2455
2456 #[test]
2457 fn ed25519_signature_is_not_forgeable_from_public_key() {
2458 let (alice, _agent, resolver, _keys) = ed25519_fixture();
2461 let document = resolver.resolve(&alice).expect("document");
2462 let auth_method = &document.verification_method[0];
2463
2464 let attacker_key = Ed25519DidKey::from_seed(b"attacker");
2467 let attacker_store = Ed25519DidKeyStore::new().with_key(alice.clone(), attacker_key);
2468 let forged = attacker_store.sign(&alice, b"message").expect("sign");
2469
2470 let honest_store = Ed25519DidKeyStore::new();
2471 assert!(matches!(
2472 honest_store.verify(auth_method, b"message", &forged),
2473 Err(DidError::InvalidSignature)
2474 ));
2475 }
2476
2477 #[test]
2478 fn ed25519_rotation_keeps_old_envelopes_until_retired() {
2479 let alice = Did::web("alice.example").expect("alice did");
2480 let agent = Did::web("agent.example").expect("agent did");
2481 let mut keys = Ed25519DidKeyStore::new()
2482 .with_key(alice.clone(), Ed25519DidKey::from_seed(b"alice-v1"))
2483 .with_key(agent.clone(), Ed25519DidKey::from_seed(b"agent-v1"));
2484 let resolver_v1 = StaticDidResolver::new()
2485 .with_document(keys.document(&alice).expect("alice v1 document"))
2486 .with_document(keys.document(&agent).expect("agent v1 document"));
2487
2488 let old_envelope = DidEnvelope::prompt(
2489 "msg-rot-1",
2490 alice.clone(),
2491 agent.clone(),
2492 DidMessageBody::infer_prompt("prompt/session/rot"),
2493 "old in-flight payload",
2494 &resolver_v1,
2495 &keys,
2496 )
2497 .expect("old envelope");
2498 assert_eq!(old_envelope.kid, format!("{alice}#key-1"));
2499
2500 assert_eq!(
2501 keys.rotate_key(&alice, Ed25519DidKey::from_seed(b"alice-v2"))
2502 .expect("rotate alice"),
2503 2
2504 );
2505 assert_eq!(
2506 keys.rotate_key(&agent, Ed25519DidKey::from_seed(b"agent-v2"))
2507 .expect("rotate agent"),
2508 2
2509 );
2510 assert_eq!(keys.active_key_version(&alice).expect("active alice"), 2);
2511
2512 let resolver_v2 = StaticDidResolver::new()
2513 .with_document(keys.document(&alice).expect("alice v2 document"))
2514 .with_document(keys.document(&agent).expect("agent v2 document"));
2515 let alice_doc = resolver_v2.resolve(&alice).expect("alice document");
2516 assert_eq!(
2517 alice_doc.authentication[0],
2518 format!("{alice}#key-signing-v2")
2519 );
2520 assert_eq!(
2521 alice_doc
2522 .verification_method
2523 .iter()
2524 .find(|method| method.id == old_envelope.kid)
2525 .and_then(|method| method.key_status.as_deref()),
2526 Some("previous")
2527 );
2528
2529 let gateway =
2530 DidMessageGateway::new(Arc::new(resolver_v2.clone()), Arc::new(keys.clone()), agent);
2531 let verified = gateway
2532 .open_prompt(&old_envelope)
2533 .expect("old envelope remains valid while previous key is advertised");
2534 assert_eq!(
2535 verified.resource.resource_id(),
2536 "prompt/session/rot",
2537 "old payload opened after sender and recipient rotation"
2538 );
2539
2540 let new_envelope = DidEnvelope::prompt(
2541 "msg-rot-2",
2542 alice.clone(),
2543 Did::web("agent.example").expect("agent did"),
2544 DidMessageBody::infer_prompt("prompt/session/rot-new"),
2545 "new payload",
2546 &resolver_v2,
2547 &keys,
2548 )
2549 .expect("new envelope");
2550 assert_eq!(new_envelope.kid, format!("{alice}#key-signing-v2"));
2551 }
2552
2553 #[test]
2554 fn ed25519_retired_key_rejects_old_signatures() {
2555 let alice = Did::web("alice-retired.example").expect("alice did");
2556 let agent = Did::web("agent-retired.example").expect("agent did");
2557 let mut keys = Ed25519DidKeyStore::new()
2558 .with_key(alice.clone(), Ed25519DidKey::from_seed(b"alice-retired-v1"))
2559 .with_key(agent.clone(), Ed25519DidKey::from_seed(b"agent-retired-v1"));
2560 let resolver_v1 = StaticDidResolver::new()
2561 .with_document(keys.document(&alice).expect("alice v1 document"))
2562 .with_document(keys.document(&agent).expect("agent v1 document"));
2563 let envelope = DidEnvelope::prompt(
2564 "msg-retired-1",
2565 alice.clone(),
2566 agent.clone(),
2567 DidMessageBody::infer_prompt("prompt/session/retired"),
2568 "payload",
2569 &resolver_v1,
2570 &keys,
2571 )
2572 .expect("envelope");
2573 let old_method = resolver_v1
2574 .resolve(&alice)
2575 .expect("alice document")
2576 .authentication_key(&envelope.kid)
2577 .expect("old auth method")
2578 .clone();
2579
2580 keys.rotate_key(&alice, Ed25519DidKey::from_seed(b"alice-retired-v2"))
2581 .expect("rotate alice");
2582 keys.retire_key(&alice, 1).expect("retire old alice key");
2583
2584 assert!(matches!(
2585 keys.verify(&old_method, envelope.signing_input().as_bytes(), &envelope.signature),
2586 Err(DidError::RetiredKey(method)) if method == old_method.id
2587 ));
2588 let rotated_doc = keys.document(&alice).expect("rotated alice document");
2589 assert!(
2590 !rotated_doc
2591 .authentication
2592 .iter()
2593 .any(|kid| kid == &envelope.kid)
2594 );
2595 }
2596
2597 struct AllowAllForTest;
2598 impl PolicyEngine for AllowAllForTest {
2599 fn check(&self, _: &SubjectId, _: &str, _: &ResourceId) -> PolicyResult {
2600 PolicyResult::Allow
2601 }
2602 }
2603
2604 #[test]
2605 fn wrapped_prompt_passthrough_keeps_envelope() {
2606 let (alice, agent, resolver, keys) = fixture();
2607 let envelope = DidEnvelope::prompt(
2608 "msg-1",
2609 alice,
2610 agent,
2611 DidMessageBody::infer_prompt("prompt/session/123"),
2612 "private prompt",
2613 &resolver,
2614 &keys,
2615 )
2616 .expect("envelope");
2617 let http = RecordingHttpClient::new().with_response(
2618 "http://localhost:11434/api/chat",
2619 json!({ "message": { "content": "ok" } }),
2620 );
2621 let client = DidOllamaClient::with_http(
2622 "http://localhost:11434",
2623 "codata-did",
2624 Arc::new(http.clone()),
2625 );
2626
2627 client.chat_wrapped_prompt(&envelope).expect("ollama call");
2628
2629 let requests = http.requests();
2630 assert_eq!(
2631 requests[0].body.as_ref().unwrap()["did_envelope"]["ciphertext"],
2632 envelope.ciphertext
2633 );
2634 }
2635}