1use std::{
16 collections::HashMap,
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}
102
103impl VerificationMethod {
104 pub fn local(id: impl Into<String>, controller: Did, public_key: impl AsRef<[u8]>) -> Self {
106 Self {
107 id: id.into(),
108 method_type: "TypesecDemoKey2026".to_owned(),
109 controller,
110 public_key_hex: hex_encode(public_key.as_ref()),
111 }
112 }
113
114 fn public_key(&self) -> Result<Vec<u8>, DidError> {
115 hex_decode(&self.public_key_hex)
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
121pub struct DidService {
122 pub id: String,
124 #[serde(rename = "type")]
126 pub service_type: String,
127 pub service_endpoint: String,
129}
130
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133pub struct DidDocument {
134 pub id: Did,
136 #[serde(default)]
138 pub verification_method: Vec<VerificationMethod>,
139 #[serde(default)]
141 pub authentication: Vec<String>,
142 #[serde(default)]
144 pub key_agreement: Vec<String>,
145 #[serde(default)]
147 pub service: Vec<DidService>,
148}
149
150impl DidDocument {
151 pub fn single_key(did: Did, public_key: impl AsRef<[u8]>) -> Self {
153 let key_id = format!("{did}#key-1");
154 Self {
155 id: did.clone(),
156 verification_method: vec![VerificationMethod::local(&key_id, did, public_key)],
157 authentication: vec![key_id.clone()],
158 key_agreement: vec![key_id],
159 service: Vec::new(),
160 }
161 }
162
163 pub fn with_signing_and_agreement_keys(
166 did: Did,
167 signing_public: impl AsRef<[u8]>,
168 agreement_public: impl AsRef<[u8]>,
169 ) -> Self {
170 let signing_id = format!("{did}#key-1");
171 let agreement_id = format!("{did}#key-2");
172 Self {
173 id: did.clone(),
174 verification_method: vec![
175 VerificationMethod {
176 id: signing_id.clone(),
177 method_type: "Ed25519VerificationKey2020".to_owned(),
178 controller: did.clone(),
179 public_key_hex: hex_encode(signing_public.as_ref()),
180 },
181 VerificationMethod {
182 id: agreement_id.clone(),
183 method_type: "X25519KeyAgreementKey2020".to_owned(),
184 controller: did,
185 public_key_hex: hex_encode(agreement_public.as_ref()),
186 },
187 ],
188 authentication: vec![signing_id],
189 key_agreement: vec![agreement_id],
190 service: Vec::new(),
191 }
192 }
193
194 fn method(&self, id: &str) -> Option<&VerificationMethod> {
195 self.verification_method
196 .iter()
197 .find(|method| method.id == id)
198 }
199
200 fn authentication_key(&self, kid: &str) -> Result<&VerificationMethod, DidError> {
201 if !self.authentication.iter().any(|id| id == kid) {
202 return Err(DidError::MissingVerificationMethod(kid.to_owned()));
203 }
204 self.method(kid)
205 .ok_or_else(|| DidError::MissingVerificationMethod(kid.to_owned()))
206 }
207
208 fn key_agreement_key(&self) -> Result<&VerificationMethod, DidError> {
209 let kid = self
210 .key_agreement
211 .first()
212 .ok_or(DidError::MissingKeyAgreement)?;
213 self.method(kid)
214 .ok_or_else(|| DidError::MissingVerificationMethod(kid.clone()))
215 }
216}
217
218pub trait DidResolver: Send + Sync {
220 fn resolve(&self, did: &Did) -> Result<DidDocument, DidError>;
222}
223
224#[derive(Debug, Default, Clone)]
226pub struct StaticDidResolver {
227 documents: HashMap<Did, DidDocument>,
228}
229
230impl StaticDidResolver {
231 pub fn new() -> Self {
233 Self::default()
234 }
235
236 pub fn with_document(mut self, document: DidDocument) -> Self {
238 self.documents.insert(document.id.clone(), document);
239 self
240 }
241}
242
243impl DidResolver for StaticDidResolver {
244 fn resolve(&self, did: &Did) -> Result<DidDocument, DidError> {
245 self.documents
246 .get(did)
247 .cloned()
248 .ok_or_else(|| DidError::Unresolved(did.to_string()))
249 }
250}
251
252pub trait DidKeyStore: Send + Sync {
254 fn sign(&self, signer: &Did, message: &[u8]) -> Result<String, DidError>;
256
257 fn verify(
259 &self,
260 method: &VerificationMethod,
261 message: &[u8],
262 signature: &str,
263 ) -> Result<(), DidError>;
264
265 fn encrypt_for(
267 &self,
268 sender: &Did,
269 recipient_public_key: &[u8],
270 plaintext: &[u8],
271 nonce: &[u8],
272 ) -> Result<String, DidError>;
273
274 fn decrypt_for(
276 &self,
277 recipient: &Did,
278 sender_public_key: &[u8],
279 nonce: &[u8],
280 ciphertext_hex: &str,
281 ) -> Result<Vec<u8>, DidError>;
282}
283
284#[cfg(any(test, feature = "demo-crypto"))]
289#[derive(Debug, Clone, PartialEq, Eq)]
290pub struct DemoDidKeyPair {
291 pub public_key: Vec<u8>,
293 private_key: Vec<u8>,
294}
295
296#[cfg(any(test, feature = "demo-crypto"))]
297impl DemoDidKeyPair {
298 pub fn from_seed(seed: impl AsRef<[u8]>) -> Self {
300 let private_key = derive_bytes(b"typesec-did-private", seed.as_ref(), 32);
301 let public_key = private_key.clone();
302 Self {
303 public_key,
304 private_key,
305 }
306 }
307}
308
309#[cfg(any(test, feature = "demo-crypto"))]
315#[derive(Debug, Default, Clone)]
316pub struct DemoDidKeyStore {
317 keys: HashMap<Did, DemoDidKeyPair>,
318}
319
320#[cfg(any(test, feature = "demo-crypto"))]
321impl DemoDidKeyStore {
322 pub fn new() -> Self {
324 Self::default()
325 }
326
327 pub fn with_key(mut self, did: Did, key: DemoDidKeyPair) -> Self {
329 self.keys.insert(did, key);
330 self
331 }
332
333 fn key(&self, did: &Did) -> Result<&DemoDidKeyPair, DidError> {
334 self.keys
335 .get(did)
336 .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))
337 }
338}
339
340#[cfg(any(test, feature = "demo-crypto"))]
341impl DidKeyStore for DemoDidKeyStore {
342 fn sign(&self, signer: &Did, message: &[u8]) -> Result<String, DidError> {
343 let key = self.key(signer)?;
344 Ok(hex_encode(&derive_bytes(&key.private_key, message, 32)))
345 }
346
347 fn verify(
348 &self,
349 method: &VerificationMethod,
350 message: &[u8],
351 signature: &str,
352 ) -> Result<(), DidError> {
353 let public = method.public_key()?;
354 let expected = hex_encode(&derive_bytes(&public, message, 32));
355 if constant_time_eq(expected.as_bytes(), signature.as_bytes()) {
356 Ok(())
357 } else {
358 Err(DidError::InvalidSignature)
359 }
360 }
361
362 fn encrypt_for(
363 &self,
364 sender: &Did,
365 recipient_public_key: &[u8],
366 plaintext: &[u8],
367 nonce: &[u8],
368 ) -> Result<String, DidError> {
369 let sender_key = self.key(sender)?;
370 let ciphertext = xor_stream(
371 plaintext,
372 &derive_shared_key(&sender_key.private_key, recipient_public_key, nonce),
373 );
374 Ok(hex_encode(&ciphertext))
375 }
376
377 fn decrypt_for(
378 &self,
379 recipient: &Did,
380 sender_public_key: &[u8],
381 nonce: &[u8],
382 ciphertext_hex: &str,
383 ) -> Result<Vec<u8>, DidError> {
384 let recipient_key = self.key(recipient)?;
385 let ciphertext = hex_decode(ciphertext_hex)?;
386 Ok(xor_stream(
387 &ciphertext,
388 &derive_shared_key(&recipient_key.private_key, sender_public_key, nonce),
389 ))
390 }
391}
392
393#[derive(Clone)]
401pub struct Ed25519DidKey {
402 signing: ed25519_dalek::SigningKey,
403 agreement: x25519_dalek::StaticSecret,
404}
405
406impl Ed25519DidKey {
407 pub fn generate() -> Result<Self, DidError> {
409 let mut signing_seed = [0u8; 32];
410 let mut agreement_seed = [0u8; 32];
411 getrandom::getrandom(&mut signing_seed).map_err(|e| DidError::KeyGen(e.to_string()))?;
412 getrandom::getrandom(&mut agreement_seed).map_err(|e| DidError::KeyGen(e.to_string()))?;
413 Ok(Self::from_seeds(signing_seed, agreement_seed))
414 }
415
416 pub fn from_seed(seed: impl AsRef<[u8]>) -> Self {
421 let signing_seed = sha256_tagged(b"typesec-ed25519-signing", seed.as_ref());
422 let agreement_seed = sha256_tagged(b"typesec-x25519-agreement", seed.as_ref());
423 Self::from_seeds(signing_seed, agreement_seed)
424 }
425
426 fn from_seeds(signing_seed: [u8; 32], agreement_seed: [u8; 32]) -> Self {
427 Self {
428 signing: ed25519_dalek::SigningKey::from_bytes(&signing_seed),
429 agreement: x25519_dalek::StaticSecret::from(agreement_seed),
430 }
431 }
432
433 pub fn signing_public(&self) -> [u8; 32] {
435 self.signing.verifying_key().to_bytes()
436 }
437
438 pub fn agreement_public(&self) -> [u8; 32] {
440 x25519_dalek::PublicKey::from(&self.agreement).to_bytes()
441 }
442
443 pub fn document(&self, did: Did) -> DidDocument {
445 DidDocument::with_signing_and_agreement_keys(
446 did,
447 self.signing_public(),
448 self.agreement_public(),
449 )
450 }
451}
452
453impl std::fmt::Debug for Ed25519DidKey {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 f.debug_struct("Ed25519DidKey")
456 .field("signing_public", &hex_encode(&self.signing_public()))
457 .field("agreement_public", &hex_encode(&self.agreement_public()))
458 .finish_non_exhaustive()
459 }
460}
461
462#[derive(Debug, Default, Clone)]
465pub struct Ed25519DidKeyStore {
466 keys: HashMap<Did, Ed25519DidKey>,
467}
468
469impl Ed25519DidKeyStore {
470 pub fn new() -> Self {
472 Self::default()
473 }
474
475 pub fn with_key(mut self, did: Did, key: Ed25519DidKey) -> Self {
477 self.keys.insert(did, key);
478 self
479 }
480
481 fn key(&self, did: &Did) -> Result<&Ed25519DidKey, DidError> {
482 self.keys
483 .get(did)
484 .ok_or_else(|| DidError::MissingPrivateKey(did.to_string()))
485 }
486
487 fn aead_key(shared_secret: &[u8; 32]) -> chacha20poly1305::Key {
488 let digest = sha256_tagged(b"typesec-did-aead", shared_secret);
489 chacha20poly1305::Key::from(digest)
490 }
491}
492
493impl DidKeyStore for Ed25519DidKeyStore {
494 fn sign(&self, signer: &Did, message: &[u8]) -> Result<String, DidError> {
495 use ed25519_dalek::Signer;
496 let key = self.key(signer)?;
497 Ok(hex_encode(&key.signing.sign(message).to_bytes()))
498 }
499
500 fn verify(
501 &self,
502 method: &VerificationMethod,
503 message: &[u8],
504 signature: &str,
505 ) -> Result<(), DidError> {
506 use ed25519_dalek::Verifier;
507 let public: [u8; 32] = method
508 .public_key()?
509 .try_into()
510 .map_err(|_| DidError::InvalidKey("ed25519 public key must be 32 bytes".into()))?;
511 let verifying = ed25519_dalek::VerifyingKey::from_bytes(&public)
512 .map_err(|e| DidError::InvalidKey(e.to_string()))?;
513 let signature_bytes: [u8; 64] = hex_decode(signature)?
514 .try_into()
515 .map_err(|_| DidError::InvalidSignature)?;
516 verifying
517 .verify(
518 message,
519 &ed25519_dalek::Signature::from_bytes(&signature_bytes),
520 )
521 .map_err(|_| DidError::InvalidSignature)
522 }
523
524 fn encrypt_for(
525 &self,
526 sender: &Did,
527 recipient_public_key: &[u8],
528 plaintext: &[u8],
529 nonce: &[u8],
530 ) -> Result<String, DidError> {
531 use chacha20poly1305::KeyInit;
532 use chacha20poly1305::aead::Aead;
533 let sender_key = self.key(sender)?;
534 let recipient: [u8; 32] = recipient_public_key
535 .try_into()
536 .map_err(|_| DidError::InvalidKey("x25519 public key must be 32 bytes".into()))?;
537 let shared = sender_key
538 .agreement
539 .diffie_hellman(&x25519_dalek::PublicKey::from(recipient));
540 let nonce: [u8; 12] = nonce.try_into().map_err(|_| DidError::InvalidNonce)?;
541 let cipher = chacha20poly1305::ChaCha20Poly1305::new(&Self::aead_key(shared.as_bytes()));
542 let ciphertext = cipher
543 .encrypt(&chacha20poly1305::Nonce::from(nonce), plaintext)
544 .map_err(|_| DidError::EncryptionFailed)?;
545 Ok(hex_encode(&ciphertext))
546 }
547
548 fn decrypt_for(
549 &self,
550 recipient: &Did,
551 sender_public_key: &[u8],
552 nonce: &[u8],
553 ciphertext_hex: &str,
554 ) -> Result<Vec<u8>, DidError> {
555 use chacha20poly1305::KeyInit;
556 use chacha20poly1305::aead::Aead;
557 let recipient_key = self.key(recipient)?;
558 let sender: [u8; 32] = sender_public_key
559 .try_into()
560 .map_err(|_| DidError::InvalidKey("x25519 public key must be 32 bytes".into()))?;
561 let shared = recipient_key
562 .agreement
563 .diffie_hellman(&x25519_dalek::PublicKey::from(sender));
564 let nonce: [u8; 12] = nonce.try_into().map_err(|_| DidError::InvalidNonce)?;
565 let ciphertext = hex_decode(ciphertext_hex)?;
566 let cipher = chacha20poly1305::ChaCha20Poly1305::new(&Self::aead_key(shared.as_bytes()));
567 cipher
568 .decrypt(&chacha20poly1305::Nonce::from(nonce), ciphertext.as_slice())
569 .map_err(|_| DidError::DecryptionFailed)
570 }
571}
572
573#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
575pub struct DidMessageBody {
576 pub action: String,
578 pub resource: String,
580 pub privacy: String,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
584 pub reply_to: Option<DidMessageReference>,
585}
586
587impl DidMessageBody {
588 pub fn infer_prompt(resource: impl Into<String>) -> Self {
590 Self {
591 action: "ai:infer".to_owned(),
592 resource: resource.into(),
593 privacy: "secret".to_owned(),
594 reply_to: None,
595 }
596 }
597
598 pub fn reply_to_prompt(prompt: &VerifiedDidPrompt) -> Self {
600 Self {
601 action: prompt.body.action.clone(),
602 resource: prompt.body.resource.clone(),
603 privacy: prompt.body.privacy.clone(),
604 reply_to: Some(prompt.prompt_ref.clone()),
605 }
606 }
607
608 pub fn agent_message(resource: impl Into<String>, privacy: impl Into<String>) -> Self {
610 Self {
611 action: "agent:message".to_owned(),
612 resource: resource.into(),
613 privacy: privacy.into(),
614 reply_to: None,
615 }
616 }
617
618 pub fn agent_delegate(resource: impl Into<String>, privacy: impl Into<String>) -> Self {
620 Self {
621 action: "agent:delegate".to_owned(),
622 resource: resource.into(),
623 privacy: privacy.into(),
624 reply_to: None,
625 }
626 }
627}
628
629#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
631#[serde(rename_all = "snake_case")]
632pub enum TypeDidMode {
633 Send,
635 RequestReply,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
641pub struct TypeDidConversation {
642 pub conversation_id: String,
644 pub mode: TypeDidMode,
646 pub profile: String,
648 pub protocol: String,
650 #[serde(default, skip_serializing_if = "Option::is_none")]
652 pub expires_at: Option<u64>,
653}
654
655impl TypeDidConversation {
656 pub fn new(
658 conversation_id: impl Into<String>,
659 mode: TypeDidMode,
660 profile: impl Into<String>,
661 protocol: impl Into<String>,
662 ) -> Self {
663 Self {
664 conversation_id: conversation_id.into(),
665 mode,
666 profile: profile.into(),
667 protocol: protocol.into(),
668 expires_at: None,
669 }
670 }
671
672 pub fn with_expires_at(mut self, expires_at: u64) -> Self {
674 self.expires_at = Some(expires_at);
675 self
676 }
677}
678
679#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
681pub struct TypeDidProfile {
682 pub id: String,
684 #[serde(default)]
686 pub did_methods: Vec<String>,
687 #[serde(default)]
689 pub signing: Vec<String>,
690 #[serde(default)]
692 pub key_agreement: Vec<String>,
693 #[serde(default)]
695 pub encryption: Vec<String>,
696 #[serde(default)]
698 pub transport_bindings: Vec<String>,
699 #[serde(default)]
701 pub modes: Vec<TypeDidMode>,
702 #[serde(default, skip_serializing_if = "Option::is_none")]
704 pub max_payload_bytes: Option<usize>,
705 #[serde(default)]
707 pub required_claims: Vec<String>,
708 #[serde(default)]
710 pub policy_actions: Vec<String>,
711 #[serde(default, skip_serializing_if = "Option::is_none")]
713 pub retention: Option<String>,
714 #[serde(default, skip_serializing_if = "Option::is_none")]
716 pub audit: Option<String>,
717}
718
719impl TypeDidProfile {
720 pub fn ed25519_x25519_chacha20() -> Self {
723 Self {
724 id: "typedid/v1/x25519-chacha20poly1305-ed25519".to_owned(),
725 did_methods: vec![
726 "did:web".to_owned(),
727 "did:key".to_owned(),
728 "did:indy".to_owned(),
729 ],
730 signing: vec!["Ed25519".to_owned()],
731 key_agreement: vec!["X25519".to_owned()],
732 encryption: vec!["ChaCha20-Poly1305".to_owned()],
733 transport_bindings: vec![
734 "a2a".to_owned(),
735 "acp".to_owned(),
736 "band".to_owned(),
737 "https".to_owned(),
738 "websocket".to_owned(),
739 ],
740 modes: vec![TypeDidMode::Send, TypeDidMode::RequestReply],
741 max_payload_bytes: Some(1024 * 1024),
742 required_claims: vec![
743 "org".to_owned(),
744 "agent_id".to_owned(),
745 "purpose".to_owned(),
746 ],
747 policy_actions: vec![
748 "agent:message".to_owned(),
749 "agent:delegate".to_owned(),
750 "ai:infer".to_owned(),
751 ],
752 retention: Some("sender-encrypted-payload-only".to_owned()),
753 audit: Some("envelope-metadata-and-policy-decision".to_owned()),
754 }
755 }
756
757 pub fn is_compatible_with(&self, remote: &Self, protocol: &str, mode: TypeDidMode) -> bool {
759 self.id == remote.id
760 && contains(&self.transport_bindings, protocol)
761 && contains(&remote.transport_bindings, protocol)
762 && self.modes.contains(&mode)
763 && remote.modes.contains(&mode)
764 && intersects(&self.did_methods, &remote.did_methods)
765 && intersects(&self.signing, &remote.signing)
766 && intersects(&self.key_agreement, &remote.key_agreement)
767 && intersects(&self.encryption, &remote.encryption)
768 }
769
770 pub fn negotiate<'a>(
772 local: &'a [Self],
773 remote: &[Self],
774 protocol: &str,
775 mode: TypeDidMode,
776 ) -> Result<&'a Self, DidError> {
777 local
778 .iter()
779 .find(|candidate| {
780 remote
781 .iter()
782 .any(|other| candidate.is_compatible_with(other, protocol, mode))
783 })
784 .ok_or(DidError::NoCompatibleTypeDidProfile)
785 }
786}
787
788pub trait TypeDidProfileResolver: Send + Sync {
790 fn resolve_profiles(&self, target: &str) -> Result<Vec<TypeDidProfile>, DidError>;
792}
793
794#[derive(Debug, Default, Clone)]
796pub struct StaticTypeDidProfileResolver {
797 profiles: HashMap<String, Vec<TypeDidProfile>>,
798}
799
800impl StaticTypeDidProfileResolver {
801 pub fn new() -> Self {
803 Self::default()
804 }
805
806 pub fn with_profiles(
808 mut self,
809 target: impl Into<String>,
810 profiles: Vec<TypeDidProfile>,
811 ) -> Self {
812 self.profiles.insert(target.into(), profiles);
813 self
814 }
815}
816
817impl TypeDidProfileResolver for StaticTypeDidProfileResolver {
818 fn resolve_profiles(&self, target: &str) -> Result<Vec<TypeDidProfile>, DidError> {
819 self.profiles
820 .get(target)
821 .cloned()
822 .ok_or_else(|| DidError::Unresolved(target.to_owned()))
823 }
824}
825
826#[derive(Debug, Clone)]
828pub struct DidReplyBinding {
829 pub prompt_body: DidMessageBody,
831 pub prompt_ref: DidMessageReference,
833}
834
835impl DidReplyBinding {
836 pub fn for_prompt(prompt: &VerifiedDidPrompt) -> Self {
838 Self {
839 prompt_body: prompt.body.clone(),
840 prompt_ref: prompt.prompt_ref.clone(),
841 }
842 }
843}
844
845#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
847pub struct DidMessageReference {
848 pub id: String,
850 pub digest: String,
852}
853
854#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
856pub struct DidEnvelope {
857 pub id: String,
859 #[serde(rename = "type")]
861 pub message_type: String,
862 pub from: Did,
864 pub to: Vec<Did>,
866 pub created_time: u64,
868 pub expires_time: u64,
870 pub body: DidMessageBody,
872 #[serde(default, skip_serializing_if = "Option::is_none")]
874 pub typedid: Option<TypeDidConversation>,
875 pub kid: String,
877 pub nonce: String,
879 pub ciphertext: String,
881 pub signature: String,
883}
884
885impl DidEnvelope {
886 pub fn prompt(
888 id: impl Into<String>,
889 from: Did,
890 to: Did,
891 body: DidMessageBody,
892 plaintext: impl AsRef<[u8]>,
893 resolver: &dyn DidResolver,
894 key_store: &dyn DidKeyStore,
895 ) -> Result<Self, DidError> {
896 let id = id.into();
897 let now = unix_time();
898 let recipient_document = resolver.resolve(&to)?;
899 let recipient_key = recipient_document.key_agreement_key()?;
900 let sender_document = resolver.resolve(&from)?;
901 let kid = sender_document
902 .authentication
903 .first()
904 .cloned()
905 .ok_or(DidError::MissingAuthentication)?;
906 let nonce = random_nonce()?;
907 let ciphertext = key_store.encrypt_for(
908 &from,
909 &recipient_key.public_key()?,
910 plaintext.as_ref(),
911 &nonce,
912 )?;
913 let mut envelope = Self {
914 id,
915 message_type: "https://typesec.dev/did/message/v1/prompt".to_owned(),
916 from,
917 to: vec![to],
918 created_time: now,
919 expires_time: now + 300,
920 body,
921 typedid: None,
922 kid,
923 nonce: hex_encode(&nonce),
924 ciphertext,
925 signature: String::new(),
926 };
927 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
928 Ok(envelope)
929 }
930
931 pub fn reply(
933 reply_did: Did,
934 from: Did,
935 to: Did,
936 binding: DidReplyBinding,
937 plaintext: impl AsRef<[u8]>,
938 resolver: &dyn DidResolver,
939 key_store: &dyn DidKeyStore,
940 ) -> Result<Self, DidError> {
941 let DidReplyBinding {
942 prompt_body,
943 prompt_ref,
944 } = binding;
945 let now = unix_time();
946 let recipient_document = resolver.resolve(&to)?;
947 let recipient_key = recipient_document.key_agreement_key()?;
948 let sender_document = resolver.resolve(&from)?;
949 let kid = sender_document
950 .authentication
951 .first()
952 .cloned()
953 .ok_or(DidError::MissingAuthentication)?;
954 let id = reply_did.to_string();
955 let nonce = random_nonce()?;
956 let ciphertext = key_store.encrypt_for(
957 &from,
958 &recipient_key.public_key()?,
959 plaintext.as_ref(),
960 &nonce,
961 )?;
962 let mut envelope = Self {
963 id,
964 message_type: "https://typesec.dev/did/message/v1/reply".to_owned(),
965 from,
966 to: vec![to],
967 created_time: now,
968 expires_time: now + 300,
969 body: DidMessageBody {
970 action: prompt_body.action.clone(),
971 resource: prompt_body.resource.clone(),
972 privacy: prompt_body.privacy.clone(),
973 reply_to: Some(prompt_ref),
974 },
975 typedid: None,
976 kid,
977 nonce: hex_encode(&nonce),
978 ciphertext,
979 signature: String::new(),
980 };
981 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
982 Ok(envelope)
983 }
984
985 #[allow(clippy::too_many_arguments)]
987 pub fn typedid(
988 id: impl Into<String>,
989 from: Did,
990 to: Did,
991 body: DidMessageBody,
992 typedid: TypeDidConversation,
993 plaintext: impl AsRef<[u8]>,
994 resolver: &dyn DidResolver,
995 key_store: &dyn DidKeyStore,
996 ) -> Result<Self, DidError> {
997 let mut envelope = Self::prompt(id, from, to, body, plaintext, resolver, key_store)?;
998 envelope.message_type = "https://typesec.dev/did/message/v1/typedid".to_owned();
999 envelope.typedid = Some(typedid);
1000 envelope.signature = key_store.sign(&envelope.from, envelope.signing_input().as_bytes())?;
1001 Ok(envelope)
1002 }
1003
1004 pub fn typedid_reply(
1006 id: impl Into<String>,
1007 from: Did,
1008 to: Did,
1009 request: &VerifiedTypeDidMessage,
1010 plaintext: impl AsRef<[u8]>,
1011 resolver: &dyn DidResolver,
1012 key_store: &dyn DidKeyStore,
1013 ) -> Result<Self, DidError> {
1014 let mut body = request.body.clone();
1015 body.reply_to = Some(request.message_ref.clone());
1016 let conversation = TypeDidConversation {
1017 conversation_id: request.conversation.conversation_id.clone(),
1018 mode: TypeDidMode::RequestReply,
1019 profile: request.conversation.profile.clone(),
1020 protocol: request.conversation.protocol.clone(),
1021 expires_at: request.conversation.expires_at,
1022 };
1023 Self::typedid(
1024 id,
1025 from,
1026 to,
1027 body,
1028 conversation,
1029 plaintext,
1030 resolver,
1031 key_store,
1032 )
1033 }
1034
1035 pub fn reference(&self) -> DidMessageReference {
1037 let seed = format!("{}\n{}", self.signing_input(), self.signature);
1038 DidMessageReference {
1039 id: self.id.clone(),
1040 digest: hex_encode(&sha256_tagged(
1041 b"typesec-did-envelope-reference",
1042 seed.as_bytes(),
1043 )),
1044 }
1045 }
1046
1047 fn signing_input(&self) -> String {
1048 let reply_to = self
1049 .body
1050 .reply_to
1051 .as_ref()
1052 .map(|reference| format!("{}\n{}", reference.id, reference.digest))
1053 .unwrap_or_default();
1054 let base = format!(
1055 "{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
1056 self.id,
1057 self.message_type,
1058 self.from,
1059 self.to
1060 .iter()
1061 .map(Did::as_str)
1062 .collect::<Vec<_>>()
1063 .join(","),
1064 self.created_time,
1065 self.expires_time,
1066 self.body.action,
1067 self.body.resource,
1068 self.body.privacy,
1069 reply_to
1070 );
1071 if let Some(typedid) = self.typedid.as_ref() {
1072 format!(
1073 "{}\n{}\n{}",
1074 base,
1075 canonical_typedid_conversation(typedid),
1076 self.ciphertext
1077 )
1078 } else {
1079 format!("{}\n{}", base, self.ciphertext)
1080 }
1081 }
1082}
1083
1084#[derive(Debug)]
1086pub struct VerifiedTypeDidMessage {
1087 pub subject: Did,
1089 pub message_ref: DidMessageReference,
1091 pub body: DidMessageBody,
1093 pub conversation: TypeDidConversation,
1095 pub resource: GenericResource,
1097 pub payload: SecureValue<Secret, Vec<u8>, GenericResource>,
1099}
1100
1101#[derive(Debug)]
1103pub struct VerifiedDidPrompt {
1104 pub subject: Did,
1106 pub prompt_ref: DidMessageReference,
1108 pub body: DidMessageBody,
1110 pub resource: GenericResource,
1112 pub prompt: SecureValue<Secret, String, GenericResource>,
1114}
1115
1116pub struct DidMessageGateway {
1118 resolver: Arc<dyn DidResolver>,
1119 key_store: Arc<dyn DidKeyStore>,
1120 recipient: Did,
1121}
1122
1123impl DidMessageGateway {
1124 pub fn new(
1126 resolver: Arc<dyn DidResolver>,
1127 key_store: Arc<dyn DidKeyStore>,
1128 recipient: Did,
1129 ) -> Self {
1130 Self {
1131 resolver,
1132 key_store,
1133 recipient,
1134 }
1135 }
1136
1137 pub fn open_prompt(&self, envelope: &DidEnvelope) -> Result<VerifiedDidPrompt, DidError> {
1139 let opened = self.open_bytes(envelope)?;
1140 let prompt = String::from_utf8(opened.plaintext).map_err(|_| DidError::InvalidUtf8)?;
1141 Ok(VerifiedDidPrompt {
1142 subject: opened.subject,
1143 prompt_ref: opened.message_ref,
1144 body: opened.body,
1145 prompt: SecureValue::protect(prompt, &opened.resource),
1146 resource: opened.resource,
1147 })
1148 }
1149
1150 fn open_bytes(&self, envelope: &DidEnvelope) -> Result<OpenedDidEnvelope, DidError> {
1151 if !envelope.to.iter().any(|did| did == &self.recipient) {
1152 return Err(DidError::WrongRecipient(self.recipient.to_string()));
1153 }
1154 let now = unix_time();
1155 if envelope.expires_time < now {
1156 return Err(DidError::Expired);
1157 }
1158
1159 let sender_document = self.resolver.resolve(&envelope.from)?;
1160 let sender_key = sender_document.authentication_key(&envelope.kid)?;
1161 self.key_store.verify(
1162 sender_key,
1163 envelope.signing_input().as_bytes(),
1164 &envelope.signature,
1165 )?;
1166
1167 let sender_agreement_key = sender_document.key_agreement_key()?;
1170 let nonce = hex_decode(&envelope.nonce)?;
1171 let plaintext = self.key_store.decrypt_for(
1172 &self.recipient,
1173 &sender_agreement_key.public_key()?,
1174 &nonce,
1175 &envelope.ciphertext,
1176 )?;
1177 let resource = GenericResource::new(&envelope.body.resource, "did-prompt");
1178
1179 Ok(OpenedDidEnvelope {
1180 subject: envelope.from.clone(),
1181 message_ref: envelope.reference(),
1182 body: envelope.body.clone(),
1183 resource,
1184 plaintext,
1185 })
1186 }
1187}
1188
1189#[derive(Debug)]
1190struct OpenedDidEnvelope {
1191 subject: Did,
1192 message_ref: DidMessageReference,
1193 body: DidMessageBody,
1194 resource: GenericResource,
1195 plaintext: Vec<u8>,
1196}
1197
1198pub struct TypeDidGateway {
1200 inner: DidMessageGateway,
1201}
1202
1203impl TypeDidGateway {
1204 pub fn new(
1206 resolver: Arc<dyn DidResolver>,
1207 key_store: Arc<dyn DidKeyStore>,
1208 recipient: Did,
1209 ) -> Self {
1210 Self {
1211 inner: DidMessageGateway::new(resolver, key_store, recipient),
1212 }
1213 }
1214
1215 pub fn open_message(&self, envelope: &DidEnvelope) -> Result<VerifiedTypeDidMessage, DidError> {
1217 let conversation = envelope
1218 .typedid
1219 .clone()
1220 .ok_or(DidError::MissingTypeDidMetadata)?;
1221 let opened = self.inner.open_bytes(envelope)?;
1222 Ok(VerifiedTypeDidMessage {
1223 subject: opened.subject,
1224 message_ref: opened.message_ref,
1225 body: opened.body,
1226 conversation,
1227 payload: SecureValue::protect(opened.plaintext, &opened.resource),
1228 resource: opened.resource,
1229 })
1230 }
1231}
1232
1233pub trait SecureEnvelopeAdapter {
1235 fn protocol(&self) -> &str;
1237
1238 fn content_type(&self) -> &'static str {
1240 "application/vnd.typedid.envelope+json"
1241 }
1242
1243 fn wrap(
1245 &self,
1246 request: TypeDidWrapRequest<'_>,
1247 resolver: &dyn DidResolver,
1248 key_store: &dyn DidKeyStore,
1249 ) -> Result<DidEnvelope, DidError> {
1250 let profile = TypeDidProfile::negotiate(
1251 request.local_profiles,
1252 request.remote_profiles,
1253 self.protocol(),
1254 request.mode,
1255 )?;
1256 let conversation = TypeDidConversation::new(
1257 request.conversation_id,
1258 request.mode,
1259 profile.id.clone(),
1260 self.protocol(),
1261 );
1262 DidEnvelope::typedid(
1263 request.id,
1264 request.from,
1265 request.to,
1266 request.body,
1267 conversation,
1268 request.payload,
1269 resolver,
1270 key_store,
1271 )
1272 }
1273}
1274
1275pub struct TypeDidWrapRequest<'a> {
1277 pub id: String,
1279 pub from: Did,
1281 pub to: Did,
1283 pub conversation_id: String,
1285 pub mode: TypeDidMode,
1287 pub body: DidMessageBody,
1289 pub payload: &'a [u8],
1291 pub local_profiles: &'a [TypeDidProfile],
1293 pub remote_profiles: &'a [TypeDidProfile],
1295}
1296
1297#[derive(Debug, Default, Clone, Copy)]
1299pub struct A2aTypeDidAdapter;
1300
1301impl SecureEnvelopeAdapter for A2aTypeDidAdapter {
1302 fn protocol(&self) -> &str {
1303 "a2a"
1304 }
1305}
1306
1307#[derive(Debug, Default, Clone, Copy)]
1309pub struct AcpTypeDidAdapter;
1310
1311impl SecureEnvelopeAdapter for AcpTypeDidAdapter {
1312 fn protocol(&self) -> &str {
1313 "acp"
1314 }
1315}
1316
1317#[derive(Debug, Default, Clone, Copy)]
1319pub struct BandSecureEnvelopeAdapter;
1320
1321impl SecureEnvelopeAdapter for BandSecureEnvelopeAdapter {
1322 fn protocol(&self) -> &str {
1323 "band"
1324 }
1325}
1326
1327#[derive(Debug, Default, Clone, Copy)]
1329pub struct HttpTypeDidAdapter;
1330
1331impl SecureEnvelopeAdapter for HttpTypeDidAdapter {
1332 fn protocol(&self) -> &str {
1333 "https"
1334 }
1335}
1336
1337pub struct DidOllamaClient {
1339 base_url: String,
1340 model: String,
1341 http: Arc<dyn HttpClient>,
1342}
1343
1344impl DidOllamaClient {
1345 pub fn new(base_url: impl Into<String>, model: impl Into<String>) -> Self {
1347 Self::with_http(base_url, model, Arc::new(ReqwestHttpClient::new()))
1348 }
1349
1350 pub fn with_http(
1352 base_url: impl Into<String>,
1353 model: impl Into<String>,
1354 http: Arc<dyn HttpClient>,
1355 ) -> Self {
1356 Self {
1357 base_url: base_url.into().trim_end_matches('/').to_owned(),
1358 model: model.into(),
1359 http,
1360 }
1361 }
1362
1363 pub fn chat_verified_prompt(
1365 &self,
1366 prompt: VerifiedDidPrompt,
1367 _infer: &Capability<AiCanInfer, GenericResource>,
1368 read: &Capability<CanReadSensitive, GenericResource>,
1369 ) -> Result<Value, DidError> {
1370 let plaintext = prompt.prompt.reveal(read)?;
1371 let body = json!({
1372 "model": self.model,
1373 "stream": false,
1374 "messages": [{
1375 "role": "user",
1376 "content": plaintext
1377 }]
1378 });
1379 self.http
1380 .post_json(&format!("{}/api/chat", self.base_url), &[], &body)
1381 .map_err(DidError::Http)
1382 }
1383
1384 pub fn chat_verified_prompt_bound(
1386 &self,
1387 prompt: VerifiedDidPrompt,
1388 reply_from: Did,
1389 resolver: &dyn DidResolver,
1390 key_store: &dyn DidKeyStore,
1391 _infer: &Capability<AiCanInfer, GenericResource>,
1392 read: &Capability<CanReadSensitive, GenericResource>,
1393 ) -> Result<DidEnvelope, DidError> {
1394 let reply_to = prompt.subject.clone();
1395 let binding = DidReplyBinding::for_prompt(&prompt);
1396 let plaintext = prompt.prompt.reveal(read)?;
1397 let body = json!({
1398 "model": self.model,
1399 "stream": false,
1400 "messages": [{
1401 "role": "user",
1402 "content": plaintext
1403 }]
1404 });
1405 let response = self
1406 .http
1407 .post_json(&format!("{}/api/chat", self.base_url), &[], &body)
1408 .map_err(DidError::Http)?;
1409 let reply = ollama_reply_content(&response)?;
1410 let reply_did = Did::key(sha256_tagged(
1411 b"typesec-did-ollama-reply",
1412 format!("{}\n{}", binding.prompt_ref.digest, reply).as_bytes(),
1413 ));
1414 DidEnvelope::reply(
1415 reply_did, reply_from, reply_to, binding, reply, resolver, key_store,
1416 )
1417 }
1418
1419 pub fn chat_wrapped_prompt(&self, envelope: &DidEnvelope) -> Result<Value, DidError> {
1421 let body = json!({
1422 "model": self.model,
1423 "stream": false,
1424 "did_envelope": envelope
1425 });
1426 self.http
1427 .post_json(&format!("{}/api/chat", self.base_url), &[], &body)
1428 .map_err(DidError::Http)
1429 }
1430}
1431
1432#[derive(Debug, thiserror::Error)]
1434pub enum DidError {
1435 #[error("invalid DID: {0}")]
1437 InvalidDid(String),
1438 #[error("unresolved DID: {0}")]
1440 Unresolved(String),
1441 #[error("missing private key for DID: {0}")]
1443 MissingPrivateKey(String),
1444 #[error("DID document has no authentication key")]
1446 MissingAuthentication,
1447 #[error("DID document has no key agreement key")]
1449 MissingKeyAgreement,
1450 #[error("missing verification method: {0}")]
1452 MissingVerificationMethod(String),
1453 #[error("invalid DID envelope signature")]
1455 InvalidSignature,
1456 #[error("DID envelope was not addressed to {0}")]
1458 WrongRecipient(String),
1459 #[error("DID envelope has expired")]
1461 Expired,
1462 #[error("invalid key material: {0}")]
1464 InvalidKey(String),
1465 #[error("invalid nonce: expected 12 bytes")]
1467 InvalidNonce,
1468 #[error("DID payload encryption failed")]
1470 EncryptionFailed,
1471 #[error("DID payload decryption failed")]
1473 DecryptionFailed,
1474 #[error("key generation failed: {0}")]
1476 KeyGen(String),
1477 #[error("capability does not cover this payload: {0}")]
1479 Capability(#[from] typesec_core::secure_value::SecureAccessError),
1480 #[error("invalid hex encoding")]
1482 InvalidHex,
1483 #[error("decrypted DID payload is not valid UTF-8")]
1485 InvalidUtf8,
1486 #[error("DID HTTP integration failed: {0}")]
1488 Http(Box<dyn std::error::Error + Send + Sync>),
1489 #[error("Ollama response did not contain message.content")]
1491 MissingOllamaReply,
1492 #[error("DID envelope is missing TypeDID metadata")]
1494 MissingTypeDidMetadata,
1495 #[error("no compatible TypeDID profile")]
1497 NoCompatibleTypeDidProfile,
1498}
1499
1500fn ollama_reply_content(response: &Value) -> Result<&str, DidError> {
1501 response
1502 .get("message")
1503 .and_then(|message| message.get("content"))
1504 .and_then(Value::as_str)
1505 .ok_or(DidError::MissingOllamaReply)
1506}
1507
1508fn unix_time() -> u64 {
1509 SystemTime::now()
1510 .duration_since(UNIX_EPOCH)
1511 .map(|duration| duration.as_secs())
1512 .unwrap_or_default()
1513}
1514
1515fn sha256_tagged(domain: &[u8], data: &[u8]) -> [u8; 32] {
1517 use sha2::Digest;
1518 let mut hasher = sha2::Sha256::new();
1519 hasher.update(domain);
1520 hasher.update([0u8]);
1521 hasher.update(data);
1522 hasher.finalize().into()
1523}
1524
1525fn random_nonce() -> Result<[u8; 12], DidError> {
1527 let mut nonce = [0u8; 12];
1528 getrandom::getrandom(&mut nonce).map_err(|e| DidError::KeyGen(e.to_string()))?;
1529 Ok(nonce)
1530}
1531
1532fn canonical_typedid_conversation(conversation: &TypeDidConversation) -> String {
1533 format!(
1534 "{}\n{:?}\n{}\n{}\n{}",
1535 conversation.conversation_id,
1536 conversation.mode,
1537 conversation.profile,
1538 conversation.protocol,
1539 conversation
1540 .expires_at
1541 .map(|expires_at| expires_at.to_string())
1542 .unwrap_or_default()
1543 )
1544}
1545
1546fn contains(values: &[String], needle: &str) -> bool {
1547 values.iter().any(|value| value == needle)
1548}
1549
1550fn intersects(left: &[String], right: &[String]) -> bool {
1551 left.iter().any(|value| right.contains(value))
1552}
1553
1554#[cfg(any(test, feature = "demo-crypto"))]
1555fn derive_shared_key(private_key: &[u8], public_key: &[u8], nonce: &[u8]) -> Vec<u8> {
1556 let mut seed = Vec::with_capacity(private_key.len() + public_key.len() + nonce.len());
1557 if private_key <= public_key {
1558 seed.extend_from_slice(private_key);
1559 seed.extend_from_slice(public_key);
1560 } else {
1561 seed.extend_from_slice(public_key);
1562 seed.extend_from_slice(private_key);
1563 }
1564 seed.extend_from_slice(nonce);
1565 derive_bytes(b"typesec-did-shared", &seed, 32)
1566}
1567
1568#[cfg(any(test, feature = "demo-crypto"))]
1570fn derive_bytes(domain: &[u8], seed: &[u8], len: usize) -> Vec<u8> {
1571 let mut out = Vec::with_capacity(len);
1572 let mut state: u64 = 0xcbf29ce484222325;
1573 for byte in domain.iter().chain(seed) {
1574 state ^= u64::from(*byte);
1575 state = state.wrapping_mul(0x100000001b3);
1576 }
1577 while out.len() < len {
1578 state ^= state >> 12;
1579 state ^= state << 25;
1580 state ^= state >> 27;
1581 state = state.wrapping_mul(0x2545f4914f6cdd1d);
1582 out.extend_from_slice(&state.to_le_bytes());
1583 }
1584 out.truncate(len);
1585 out
1586}
1587
1588#[cfg(any(test, feature = "demo-crypto"))]
1589fn xor_stream(input: &[u8], key: &[u8]) -> Vec<u8> {
1590 input
1591 .iter()
1592 .enumerate()
1593 .map(|(idx, byte)| byte ^ key[idx % key.len()])
1594 .collect()
1595}
1596
1597#[cfg(any(test, feature = "demo-crypto"))]
1598fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
1599 if left.len() != right.len() {
1600 return false;
1601 }
1602 left.iter()
1603 .zip(right)
1604 .fold(0u8, |acc, (a, b)| acc | (a ^ b))
1605 == 0
1606}
1607
1608fn hex_encode(bytes: &[u8]) -> String {
1609 const HEX: &[u8; 16] = b"0123456789abcdef";
1610 let mut out = String::with_capacity(bytes.len() * 2);
1611 for byte in bytes {
1612 out.push(HEX[(byte >> 4) as usize] as char);
1613 out.push(HEX[(byte & 0x0f) as usize] as char);
1614 }
1615 out
1616}
1617
1618fn hex_decode(value: &str) -> Result<Vec<u8>, DidError> {
1619 if !value.len().is_multiple_of(2) {
1620 return Err(DidError::InvalidHex);
1621 }
1622 let mut out = Vec::with_capacity(value.len() / 2);
1623 for chunk in value.as_bytes().chunks_exact(2) {
1624 let high = hex_nibble(chunk[0])?;
1625 let low = hex_nibble(chunk[1])?;
1626 out.push((high << 4) | low);
1627 }
1628 Ok(out)
1629}
1630
1631fn hex_nibble(byte: u8) -> Result<u8, DidError> {
1632 match byte {
1633 b'0'..=b'9' => Ok(byte - b'0'),
1634 b'a'..=b'f' => Ok(byte - b'a' + 10),
1635 b'A'..=b'F' => Ok(byte - b'A' + 10),
1636 _ => Err(DidError::InvalidHex),
1637 }
1638}
1639
1640#[cfg(test)]
1641mod tests {
1642 use std::sync::Arc;
1643
1644 use serde_json::json;
1645 use typesec_core::{
1646 PolicyEngine, Resource,
1647 permissions::{AiCanInfer, CanReadSensitive},
1648 policy::{PolicyResult, mint_capability},
1649 };
1650
1651 use super::*;
1652 use crate::http::RecordingHttpClient;
1653
1654 struct PromptPolicy;
1655
1656 impl PolicyEngine for PromptPolicy {
1657 fn check(&self, subject: &str, action: &str, resource: &str) -> PolicyResult {
1658 if subject == "did:key:z616c696365"
1659 && matches!(action, "ai:infer" | "read_sensitive")
1660 && resource == "prompt/session/123"
1661 {
1662 PolicyResult::Allow
1663 } else {
1664 PolicyResult::Deny("not allowed".to_owned())
1665 }
1666 }
1667 }
1668
1669 fn fixture() -> (Did, Did, StaticDidResolver, DemoDidKeyStore) {
1670 let alice = Did::key(b"alice");
1671 let agent = Did::key(b"agent");
1672 let alice_key = DemoDidKeyPair::from_seed(b"alice");
1673 let agent_key = DemoDidKeyPair::from_seed(b"agent");
1674 let resolver = StaticDidResolver::new()
1675 .with_document(DidDocument::single_key(
1676 alice.clone(),
1677 alice_key.public_key.clone(),
1678 ))
1679 .with_document(DidDocument::single_key(
1680 agent.clone(),
1681 agent_key.public_key.clone(),
1682 ));
1683 let keys = DemoDidKeyStore::new()
1684 .with_key(alice.clone(), alice_key)
1685 .with_key(agent.clone(), agent_key);
1686 (alice, agent, resolver, keys)
1687 }
1688
1689 struct AgentPolicy {
1690 allowed_subject: String,
1691 }
1692
1693 impl PolicyEngine for AgentPolicy {
1694 fn check(&self, subject: &str, action: &str, resource: &str) -> PolicyResult {
1695 if subject == self.allowed_subject
1696 && matches!(
1697 action,
1698 "agent:message" | "agent:delegate" | "read_sensitive"
1699 )
1700 && resource == "room/acme-support"
1701 {
1702 PolicyResult::Allow
1703 } else {
1704 PolicyResult::Deny("agent message denied".to_owned())
1705 }
1706 }
1707 }
1708
1709 #[test]
1710 fn dids_parse_and_reject_bad_values() {
1711 assert!(Did::parse("did:web:example.com").is_ok());
1712 assert!(Did::parse("not-a-did").is_err());
1713 assert_eq!(
1714 Did::web("typesec.dev").unwrap().as_str(),
1715 "did:web:typesec.dev"
1716 );
1717 }
1718
1719 #[test]
1720 fn encrypted_prompt_opens_as_secret_secure_value() {
1721 let (alice, agent, resolver, keys) = fixture();
1722 let envelope = DidEnvelope::prompt(
1723 "msg-1",
1724 alice.clone(),
1725 agent.clone(),
1726 DidMessageBody::infer_prompt("prompt/session/123"),
1727 "summarize this confidential record",
1728 &resolver,
1729 &keys,
1730 )
1731 .expect("envelope");
1732 assert_ne!(envelope.ciphertext, "summarize this confidential record");
1733
1734 let gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), agent);
1735 let verified = gateway.open_prompt(&envelope).expect("verified prompt");
1736 assert_eq!(verified.subject, alice);
1737 assert_eq!(verified.resource.resource_id(), "prompt/session/123");
1738 assert_eq!(
1739 SecureValue::<Secret, String, GenericResource>::label_name(),
1740 "secret"
1741 );
1742
1743 let infer = mint_capability::<AiCanInfer, _>(
1744 &PromptPolicy,
1745 verified.subject.as_str(),
1746 &verified.resource,
1747 )
1748 .expect("infer cap");
1749 let read = mint_capability::<CanReadSensitive, _>(
1750 &PromptPolicy,
1751 verified.subject.as_str(),
1752 &verified.resource,
1753 )
1754 .expect("read cap");
1755 assert_eq!(infer.resource_id(), "prompt/session/123");
1756 assert_eq!(
1757 verified.prompt.reveal(&read).expect("matching resource"),
1758 "summarize this confidential record"
1759 );
1760 }
1761
1762 #[test]
1763 fn did_ollama_client_sends_plaintext_only_after_capabilities() {
1764 let (alice, agent, resolver, keys) = fixture();
1765 let envelope = DidEnvelope::prompt(
1766 "msg-1",
1767 alice,
1768 agent.clone(),
1769 DidMessageBody::infer_prompt("prompt/session/123"),
1770 "private prompt",
1771 &resolver,
1772 &keys,
1773 )
1774 .expect("envelope");
1775 let gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), agent);
1776 let verified = gateway.open_prompt(&envelope).expect("verified prompt");
1777 let infer = mint_capability::<AiCanInfer, _>(
1778 &PromptPolicy,
1779 verified.subject.as_str(),
1780 &verified.resource,
1781 )
1782 .expect("infer cap");
1783 let read = mint_capability::<CanReadSensitive, _>(
1784 &PromptPolicy,
1785 verified.subject.as_str(),
1786 &verified.resource,
1787 )
1788 .expect("read cap");
1789
1790 let http = RecordingHttpClient::new().with_response(
1791 "http://localhost:11434/api/chat",
1792 json!({ "message": { "content": "ok" } }),
1793 );
1794 let client = DidOllamaClient::with_http(
1795 "http://localhost:11434",
1796 "llama3.2",
1797 Arc::new(http.clone()),
1798 );
1799 let response = client
1800 .chat_verified_prompt(verified, &infer, &read)
1801 .expect("ollama call");
1802
1803 assert_eq!(response["message"]["content"], "ok");
1804 let requests = http.requests();
1805 assert_eq!(requests.len(), 1);
1806 assert_eq!(requests[0].url, "http://localhost:11434/api/chat");
1807 assert_eq!(
1808 requests[0].body.as_ref().unwrap()["messages"][0]["content"],
1809 "private prompt"
1810 );
1811 }
1812
1813 #[test]
1814 fn typedid_profile_negotiates_on_protocol_and_mode() {
1815 let local = vec![TypeDidProfile::ed25519_x25519_chacha20()];
1816 let remote = vec![TypeDidProfile::ed25519_x25519_chacha20()];
1817 let selected = TypeDidProfile::negotiate(&local, &remote, "a2a", TypeDidMode::RequestReply)
1818 .expect("compatible profile");
1819 assert_eq!(selected.id, "typedid/v1/x25519-chacha20poly1305-ed25519");
1820
1821 assert!(matches!(
1822 TypeDidProfile::negotiate(&local, &remote, "smtp", TypeDidMode::Send),
1823 Err(DidError::NoCompatibleTypeDidProfile)
1824 ));
1825 }
1826
1827 #[test]
1828 fn typedid_adapter_wraps_and_gateway_opens_opaque_payload() {
1829 let (alice, agent, resolver, keys) = fixture();
1830 let profiles = vec![TypeDidProfile::ed25519_x25519_chacha20()];
1831 let adapter = A2aTypeDidAdapter;
1832 let payload =
1833 br#"{"jsonrpc":"2.0","method":"message/send","params":{"text":"triage case"}}"#;
1834
1835 let envelope = adapter
1836 .wrap(
1837 TypeDidWrapRequest {
1838 id: "a2a-msg-1".to_owned(),
1839 from: alice.clone(),
1840 to: agent.clone(),
1841 conversation_id: "task/a2a-123".to_owned(),
1842 mode: TypeDidMode::RequestReply,
1843 body: DidMessageBody::agent_delegate("room/acme-support", "secret"),
1844 payload,
1845 local_profiles: &profiles,
1846 remote_profiles: &profiles,
1847 },
1848 &resolver,
1849 &keys,
1850 )
1851 .expect("wrapped envelope");
1852
1853 assert_eq!(
1854 adapter.content_type(),
1855 "application/vnd.typedid.envelope+json"
1856 );
1857 assert_eq!(
1858 envelope.message_type,
1859 "https://typesec.dev/did/message/v1/typedid"
1860 );
1861 assert_eq!(envelope.typedid.as_ref().unwrap().protocol, "a2a");
1862 assert_ne!(envelope.ciphertext.as_bytes(), payload);
1863
1864 let gateway = TypeDidGateway::new(Arc::new(resolver), Arc::new(keys), agent);
1865 let verified = gateway.open_message(&envelope).expect("verified typedid");
1866 assert_eq!(verified.subject, alice);
1867 assert_eq!(verified.conversation.conversation_id, "task/a2a-123");
1868 assert_eq!(verified.body.action, "agent:delegate");
1869
1870 let read = mint_capability::<CanReadSensitive, _>(
1871 &AgentPolicy {
1872 allowed_subject: verified.subject.to_string(),
1873 },
1874 verified.subject.as_str(),
1875 &verified.resource,
1876 )
1877 .expect("read cap");
1878 assert_eq!(verified.payload.reveal(&read).expect("payload"), payload);
1879 }
1880
1881 #[test]
1882 fn typedid_reply_is_bound_to_request_envelope() {
1883 let (alice, agent, resolver, keys) = fixture();
1884 let request = DidEnvelope::typedid(
1885 "band-room-msg-1",
1886 alice.clone(),
1887 agent.clone(),
1888 DidMessageBody::agent_message("room/acme-support", "secret"),
1889 TypeDidConversation::new(
1890 "room/acme-support",
1891 TypeDidMode::RequestReply,
1892 TypeDidProfile::ed25519_x25519_chacha20().id,
1893 "band",
1894 ),
1895 b"please coordinate with the support agent",
1896 &resolver,
1897 &keys,
1898 )
1899 .expect("request envelope");
1900 let request_ref = request.reference();
1901 let gateway = TypeDidGateway::new(
1902 Arc::new(resolver.clone()),
1903 Arc::new(keys.clone()),
1904 agent.clone(),
1905 );
1906 let verified = gateway.open_message(&request).expect("verified request");
1907 let reply = DidEnvelope::typedid_reply(
1908 "band-room-reply-1",
1909 agent.clone(),
1910 alice.clone(),
1911 &verified,
1912 b"support agent accepted the handoff",
1913 &resolver,
1914 &keys,
1915 )
1916 .expect("reply envelope");
1917
1918 assert_eq!(reply.typedid.as_ref().unwrap().protocol, "band");
1919 assert_eq!(reply.body.reply_to, Some(request_ref));
1920
1921 let reply_gateway = TypeDidGateway::new(Arc::new(resolver), Arc::new(keys), alice);
1922 let opened_reply = reply_gateway.open_message(&reply).expect("opened reply");
1923 assert_eq!(opened_reply.subject, agent);
1924 }
1925
1926 #[test]
1927 fn typedid_signature_covers_conversation_metadata() {
1928 let (alice, agent, resolver, keys) = fixture();
1929 let mut envelope = DidEnvelope::typedid(
1930 "acp-session-msg-1",
1931 alice,
1932 agent.clone(),
1933 DidMessageBody::agent_message("room/acme-support", "secret"),
1934 TypeDidConversation::new(
1935 "session/editor-1",
1936 TypeDidMode::Send,
1937 TypeDidProfile::ed25519_x25519_chacha20().id,
1938 "acp",
1939 ),
1940 b"review this private diff",
1941 &resolver,
1942 &keys,
1943 )
1944 .expect("typedid envelope");
1945 envelope.typedid.as_mut().unwrap().protocol = "band".to_owned();
1946
1947 let gateway = TypeDidGateway::new(Arc::new(resolver), Arc::new(keys), agent);
1948 assert!(matches!(
1949 gateway.open_message(&envelope),
1950 Err(DidError::InvalidSignature)
1951 ));
1952 }
1953
1954 #[test]
1955 fn bound_ollama_reply_creates_signed_reply_envelope_for_prompt() {
1956 let (alice, agent, resolver, keys) = fixture();
1957 let prompt_envelope = DidEnvelope::prompt(
1958 "msg-1",
1959 alice.clone(),
1960 agent.clone(),
1961 DidMessageBody::infer_prompt("prompt/session/123"),
1962 "private prompt",
1963 &resolver,
1964 &keys,
1965 )
1966 .expect("prompt envelope");
1967 let prompt_ref = prompt_envelope.reference();
1968 let gateway = DidMessageGateway::new(
1969 Arc::new(resolver.clone()),
1970 Arc::new(keys.clone()),
1971 agent.clone(),
1972 );
1973 let verified = gateway
1974 .open_prompt(&prompt_envelope)
1975 .expect("verified prompt");
1976 let infer = mint_capability::<AiCanInfer, _>(
1977 &PromptPolicy,
1978 verified.subject.as_str(),
1979 &verified.resource,
1980 )
1981 .expect("infer cap");
1982 let read = mint_capability::<CanReadSensitive, _>(
1983 &PromptPolicy,
1984 verified.subject.as_str(),
1985 &verified.resource,
1986 )
1987 .expect("read cap");
1988
1989 let http = RecordingHttpClient::new().with_response(
1990 "http://localhost:11434/api/chat",
1991 json!({ "message": { "content": "bound reply" } }),
1992 );
1993 let client = DidOllamaClient::with_http(
1994 "http://localhost:11434",
1995 "llama3.2",
1996 Arc::new(http.clone()),
1997 );
1998 let reply_envelope = client
1999 .chat_verified_prompt_bound(verified, agent.clone(), &resolver, &keys, &infer, &read)
2000 .expect("bound reply");
2001
2002 assert!(reply_envelope.id.starts_with("did:key:z"));
2003 assert_eq!(
2004 reply_envelope.message_type,
2005 "https://typesec.dev/did/message/v1/reply"
2006 );
2007 assert_eq!(reply_envelope.from, agent);
2008 assert_eq!(reply_envelope.to, vec![alice.clone()]);
2009 assert_eq!(reply_envelope.body.resource, "prompt/session/123");
2010 assert_eq!(reply_envelope.body.privacy, "secret");
2011 assert_eq!(reply_envelope.body.reply_to, Some(prompt_ref));
2012 assert_ne!(reply_envelope.ciphertext, "bound reply");
2013
2014 let reply_gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), alice);
2015 let opened_reply = reply_gateway
2016 .open_prompt(&reply_envelope)
2017 .expect("verified reply");
2018 assert_eq!(opened_reply.subject, reply_envelope.from);
2019 assert_eq!(
2020 opened_reply
2021 .prompt
2022 .reveal(&read)
2023 .expect("matching resource"),
2024 "bound reply"
2025 );
2026 }
2027
2028 #[test]
2029 fn reply_signature_covers_prompt_reference() {
2030 let (alice, agent, resolver, keys) = fixture();
2031 let prompt_envelope = DidEnvelope::prompt(
2032 "msg-1",
2033 alice.clone(),
2034 agent.clone(),
2035 DidMessageBody::infer_prompt("prompt/session/123"),
2036 "private prompt",
2037 &resolver,
2038 &keys,
2039 )
2040 .expect("prompt envelope");
2041 let gateway = DidMessageGateway::new(
2042 Arc::new(resolver.clone()),
2043 Arc::new(keys.clone()),
2044 agent.clone(),
2045 );
2046 let verified = gateway
2047 .open_prompt(&prompt_envelope)
2048 .expect("verified prompt");
2049 let mut reply_envelope = DidEnvelope::reply(
2050 Did::key(b"reply-1"),
2051 agent,
2052 alice.clone(),
2053 DidReplyBinding::for_prompt(&verified),
2054 "bound reply",
2055 &resolver,
2056 &keys,
2057 )
2058 .expect("reply envelope");
2059 reply_envelope
2060 .body
2061 .reply_to
2062 .as_mut()
2063 .expect("prompt reference")
2064 .digest = "tampered".to_owned();
2065
2066 let reply_gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), alice);
2067 assert!(matches!(
2068 reply_gateway.open_prompt(&reply_envelope),
2069 Err(DidError::InvalidSignature)
2070 ));
2071 }
2072
2073 fn ed25519_fixture() -> (Did, Did, StaticDidResolver, Ed25519DidKeyStore) {
2074 let alice_key = Ed25519DidKey::from_seed(b"alice-ed25519");
2075 let agent_key = Ed25519DidKey::from_seed(b"agent-ed25519");
2076 let alice = Did::key(alice_key.signing_public());
2077 let agent = Did::key(agent_key.signing_public());
2078 let resolver = StaticDidResolver::new()
2079 .with_document(alice_key.document(alice.clone()))
2080 .with_document(agent_key.document(agent.clone()));
2081 let keys = Ed25519DidKeyStore::new()
2082 .with_key(alice.clone(), alice_key)
2083 .with_key(agent.clone(), agent_key);
2084 (alice, agent, resolver, keys)
2085 }
2086
2087 #[test]
2088 fn ed25519_envelope_roundtrip() {
2089 let (alice, agent, resolver, keys) = ed25519_fixture();
2090 let envelope = DidEnvelope::prompt(
2091 "msg-ed-1",
2092 alice.clone(),
2093 agent.clone(),
2094 DidMessageBody::infer_prompt("prompt/session/ed"),
2095 "confidential prompt over real crypto",
2096 &resolver,
2097 &keys,
2098 )
2099 .expect("envelope");
2100 assert_ne!(envelope.ciphertext, "confidential prompt over real crypto");
2101
2102 let gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2103 let verified = gateway.open_prompt(&envelope).expect("verified prompt");
2104 assert_eq!(verified.subject, alice);
2105
2106 let cap: typesec_core::Capability<CanReadSensitive, GenericResource> = mint_capability(
2107 &AllowAllForTest,
2108 verified.subject.as_str(),
2109 &verified.resource,
2110 )
2111 .expect("read cap");
2112 assert_eq!(
2113 verified.prompt.reveal(&cap).expect("matching resource"),
2114 "confidential prompt over real crypto"
2115 );
2116 }
2117
2118 #[test]
2119 fn ed25519_rejects_tampered_envelope() {
2120 let (alice, agent, resolver, keys) = ed25519_fixture();
2121 let mut envelope = DidEnvelope::prompt(
2122 "msg-ed-2",
2123 alice,
2124 agent.clone(),
2125 DidMessageBody::infer_prompt("prompt/session/ed"),
2126 "payload",
2127 &resolver,
2128 &keys,
2129 )
2130 .expect("envelope");
2131 envelope.body.resource = "prompt/session/other".to_owned();
2132
2133 let gateway = DidMessageGateway::new(Arc::new(resolver), Arc::new(keys), agent);
2134 assert!(matches!(
2135 gateway.open_prompt(&envelope),
2136 Err(DidError::InvalidSignature)
2137 ));
2138 }
2139
2140 #[test]
2141 fn ed25519_signature_is_not_forgeable_from_public_key() {
2142 let (alice, _agent, resolver, _keys) = ed25519_fixture();
2145 let document = resolver.resolve(&alice).expect("document");
2146 let auth_method = &document.verification_method[0];
2147
2148 let attacker_key = Ed25519DidKey::from_seed(b"attacker");
2151 let attacker_store = Ed25519DidKeyStore::new().with_key(alice.clone(), attacker_key);
2152 let forged = attacker_store.sign(&alice, b"message").expect("sign");
2153
2154 let honest_store = Ed25519DidKeyStore::new();
2155 assert!(matches!(
2156 honest_store.verify(auth_method, b"message", &forged),
2157 Err(DidError::InvalidSignature)
2158 ));
2159 }
2160
2161 struct AllowAllForTest;
2162 impl PolicyEngine for AllowAllForTest {
2163 fn check(&self, _: &str, _: &str, _: &str) -> PolicyResult {
2164 PolicyResult::Allow
2165 }
2166 }
2167
2168 #[test]
2169 fn wrapped_prompt_passthrough_keeps_envelope() {
2170 let (alice, agent, resolver, keys) = fixture();
2171 let envelope = DidEnvelope::prompt(
2172 "msg-1",
2173 alice,
2174 agent,
2175 DidMessageBody::infer_prompt("prompt/session/123"),
2176 "private prompt",
2177 &resolver,
2178 &keys,
2179 )
2180 .expect("envelope");
2181 let http = RecordingHttpClient::new().with_response(
2182 "http://localhost:11434/api/chat",
2183 json!({ "message": { "content": "ok" } }),
2184 );
2185 let client = DidOllamaClient::with_http(
2186 "http://localhost:11434",
2187 "codata-did",
2188 Arc::new(http.clone()),
2189 );
2190
2191 client.chat_wrapped_prompt(&envelope).expect("ollama call");
2192
2193 let requests = http.requests();
2194 assert_eq!(
2195 requests[0].body.as_ref().unwrap()["did_envelope"]["ciphertext"],
2196 envelope.ciphertext
2197 );
2198 }
2199}