Skip to main content

typesec_integrations/did/
typedid.rs

1//! TypeDID conversation/profile negotiation and secure-envelope transport
2//! adapters.
3
4use std::collections::HashMap;
5
6use serde::{Deserialize, Serialize};
7
8use super::crypto::{contains, intersects};
9use super::document::DidResolver;
10use super::envelope::{DidEnvelope, DidMessageBody};
11use super::error::DidError;
12use super::identifier::Did;
13use super::keystore::DidKeyStore;
14
15/// TypeDID delivery mode for an agent message.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum TypeDidMode {
19    /// Fire-and-forget delivery; no TypeDID reply is required.
20    Send,
21    /// The receiver is expected to answer with a reply-bound TypeDID envelope.
22    RequestReply,
23}
24
25/// TypeDID conversation metadata bound into the envelope signature.
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct TypeDidConversation {
28    /// Stable task, session, room, or thread id from the outer protocol.
29    pub conversation_id: String,
30    /// Delivery mode.
31    pub mode: TypeDidMode,
32    /// Negotiated TypeDID profile id.
33    pub profile: String,
34    /// Outer protocol hint, such as `a2a`, `acp`, `band`, or `https`.
35    pub protocol: String,
36    /// Optional payload expiry copied from the negotiated profile or caller.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub expires_at: Option<u64>,
39}
40
41impl TypeDidConversation {
42    /// Construct TypeDID conversation metadata.
43    pub fn new(
44        conversation_id: impl Into<String>,
45        mode: TypeDidMode,
46        profile: impl Into<String>,
47        protocol: impl Into<String>,
48    ) -> Self {
49        Self {
50            conversation_id: conversation_id.into(),
51            mode,
52            profile: profile.into(),
53            protocol: protocol.into(),
54            expires_at: None,
55        }
56    }
57
58    /// Attach an absolute unix-seconds expiry to this conversation metadata.
59    pub fn with_expires_at(mut self, expires_at: u64) -> Self {
60        self.expires_at = Some(expires_at);
61        self
62    }
63}
64
65/// A negotiable TypeDID security profile.
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67pub struct TypeDidProfile {
68    /// Stable profile id.
69    pub id: String,
70    /// Supported DID methods, such as `did:web` or `did:key`.
71    #[serde(default)]
72    pub did_methods: Vec<String>,
73    /// Supported signing algorithms.
74    #[serde(default)]
75    pub signing: Vec<String>,
76    /// Supported key-agreement algorithms.
77    #[serde(default)]
78    pub key_agreement: Vec<String>,
79    /// Supported encryption profiles.
80    #[serde(default)]
81    pub encryption: Vec<String>,
82    /// Supported outer transport bindings.
83    #[serde(default)]
84    pub transport_bindings: Vec<String>,
85    /// Supported TypeDID send modes.
86    #[serde(default)]
87    pub modes: Vec<TypeDidMode>,
88    /// Optional maximum encrypted payload size.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub max_payload_bytes: Option<usize>,
91    /// Claims required by the remote boundary.
92    #[serde(default)]
93    pub required_claims: Vec<String>,
94    /// Policy actions this profile is willing to carry.
95    #[serde(default)]
96    pub policy_actions: Vec<String>,
97    /// Retention posture advertised by the receiver.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub retention: Option<String>,
100    /// Audit posture advertised by the receiver.
101    #[serde(default, skip_serializing_if = "Option::is_none")]
102    pub audit: Option<String>,
103}
104
105impl TypeDidProfile {
106    /// Default local TypeDID profile backed by the built-in Ed25519/X25519
107    /// key store.
108    pub fn ed25519_x25519_chacha20() -> Self {
109        Self {
110            id: "typedid/v1/x25519-chacha20poly1305-ed25519".to_owned(),
111            did_methods: vec![
112                "did:web".to_owned(),
113                "did:key".to_owned(),
114                "did:indy".to_owned(),
115            ],
116            signing: vec!["Ed25519".to_owned()],
117            key_agreement: vec!["X25519".to_owned()],
118            encryption: vec!["ChaCha20-Poly1305".to_owned()],
119            transport_bindings: vec![
120                "a2a".to_owned(),
121                "acp".to_owned(),
122                "band".to_owned(),
123                "https".to_owned(),
124                "websocket".to_owned(),
125            ],
126            modes: vec![TypeDidMode::Send, TypeDidMode::RequestReply],
127            max_payload_bytes: Some(1024 * 1024),
128            required_claims: vec![
129                "org".to_owned(),
130                "agent_id".to_owned(),
131                "purpose".to_owned(),
132            ],
133            policy_actions: vec![
134                "agent:message".to_owned(),
135                "agent:delegate".to_owned(),
136                "ai:infer".to_owned(),
137            ],
138            retention: Some("sender-encrypted-payload-only".to_owned()),
139            audit: Some("envelope-metadata-and-policy-decision".to_owned()),
140        }
141    }
142
143    /// Return true when this local profile can safely communicate with `remote`.
144    pub fn is_compatible_with(&self, remote: &Self, protocol: &str, mode: TypeDidMode) -> bool {
145        self.id == remote.id
146            && contains(&self.transport_bindings, protocol)
147            && contains(&remote.transport_bindings, protocol)
148            && self.modes.contains(&mode)
149            && remote.modes.contains(&mode)
150            && intersects(&self.did_methods, &remote.did_methods)
151            && intersects(&self.signing, &remote.signing)
152            && intersects(&self.key_agreement, &remote.key_agreement)
153            && intersects(&self.encryption, &remote.encryption)
154    }
155
156    /// Select the first local profile compatible with the remote boundary.
157    pub fn negotiate<'a>(
158        local: &'a [Self],
159        remote: &[Self],
160        protocol: &str,
161        mode: TypeDidMode,
162    ) -> Result<&'a Self, DidError> {
163        local
164            .iter()
165            .find(|candidate| {
166                remote
167                    .iter()
168                    .any(|other| candidate.is_compatible_with(other, protocol, mode))
169            })
170            .ok_or(DidError::NoCompatibleTypeDidProfile)
171    }
172}
173
174/// Resolves TypeDID profiles for a remote agent or boundary.
175pub trait TypeDidProfileResolver: Send + Sync {
176    /// Resolve profiles advertised by `target`.
177    fn resolve_profiles(&self, target: &str) -> Result<Vec<TypeDidProfile>, DidError>;
178}
179
180/// In-memory TypeDID profile resolver for examples and tests.
181#[derive(Debug, Default, Clone)]
182pub struct StaticTypeDidProfileResolver {
183    profiles: HashMap<String, Vec<TypeDidProfile>>,
184}
185
186impl StaticTypeDidProfileResolver {
187    /// Create an empty profile resolver.
188    pub fn new() -> Self {
189        Self::default()
190    }
191
192    /// Register profiles for a target DID, contact, agent card, or endpoint.
193    pub fn with_profiles(
194        mut self,
195        target: impl Into<String>,
196        profiles: Vec<TypeDidProfile>,
197    ) -> Self {
198        self.profiles.insert(target.into(), profiles);
199        self
200    }
201}
202
203impl TypeDidProfileResolver for StaticTypeDidProfileResolver {
204    fn resolve_profiles(&self, target: &str) -> Result<Vec<TypeDidProfile>, DidError> {
205        self.profiles
206            .get(target)
207            .cloned()
208            .ok_or_else(|| DidError::Unresolved(target.to_owned()))
209    }
210}
211
212/// Common interface for TypeDID secure-envelope transport adapters.
213pub trait SecureEnvelopeAdapter {
214    /// Adapter protocol name.
215    fn protocol(&self) -> &str;
216
217    /// Media type this adapter carries over its outer protocol.
218    fn content_type(&self) -> &'static str {
219        "application/vnd.typedid.envelope+json"
220    }
221
222    /// Wrap a payload in a TypeDID envelope for this adapter's protocol.
223    fn wrap(
224        &self,
225        request: TypeDidWrapRequest<'_>,
226        resolver: &dyn DidResolver,
227        key_store: &dyn DidKeyStore,
228    ) -> Result<DidEnvelope, DidError> {
229        let profile = TypeDidProfile::negotiate(
230            request.local_profiles,
231            request.remote_profiles,
232            self.protocol(),
233            request.mode,
234        )?;
235        // Enforce the negotiated payload-size cap (a DoS/amplification guard at
236        // the gateway boundary) rather than merely advertising it.
237        if let Some(max) = profile.max_payload_bytes
238            && request.payload.len() > max
239        {
240            return Err(DidError::PayloadTooLarge {
241                size: request.payload.len(),
242                max,
243            });
244        }
245        let conversation = TypeDidConversation::new(
246            request.conversation_id,
247            request.mode,
248            profile.id.clone(),
249            self.protocol(),
250        );
251        DidEnvelope::typedid(
252            request.id,
253            request.from,
254            request.to,
255            request.body,
256            conversation,
257            request.payload,
258            resolver,
259            key_store,
260        )
261    }
262}
263
264/// Inputs for wrapping a payload in a TypeDID transport adapter.
265pub struct TypeDidWrapRequest<'a> {
266    /// Envelope id.
267    pub id: String,
268    /// Sender DID.
269    pub from: Did,
270    /// Recipient DID.
271    pub to: Did,
272    /// Outer conversation/task/room/session id.
273    pub conversation_id: String,
274    /// Send mode.
275    pub mode: TypeDidMode,
276    /// Policy-visible body.
277    pub body: DidMessageBody,
278    /// Plaintext payload bytes.
279    pub payload: &'a [u8],
280    /// Local TypeDID profiles.
281    pub local_profiles: &'a [TypeDidProfile],
282    /// Remote TypeDID profiles.
283    pub remote_profiles: &'a [TypeDidProfile],
284}
285
286/// A2A TypeDID content adapter.
287#[derive(Debug, Default, Clone, Copy)]
288pub struct A2aTypeDidAdapter;
289
290impl SecureEnvelopeAdapter for A2aTypeDidAdapter {
291    fn protocol(&self) -> &str {
292        "a2a"
293    }
294}
295
296/// ACP TypeDID content adapter.
297#[derive(Debug, Default, Clone, Copy)]
298pub struct AcpTypeDidAdapter;
299
300impl SecureEnvelopeAdapter for AcpTypeDidAdapter {
301    fn protocol(&self) -> &str {
302        "acp"
303    }
304}
305
306/// BAND secure-envelope adapter for TypeDID payloads.
307#[derive(Debug, Default, Clone, Copy)]
308pub struct BandSecureEnvelopeAdapter;
309
310impl SecureEnvelopeAdapter for BandSecureEnvelopeAdapter {
311    fn protocol(&self) -> &str {
312        "band"
313    }
314}
315
316/// Direct HTTPS TypeDID content adapter.
317#[derive(Debug, Default, Clone, Copy)]
318pub struct HttpTypeDidAdapter;
319
320impl SecureEnvelopeAdapter for HttpTypeDidAdapter {
321    fn protocol(&self) -> &str {
322        "https"
323    }
324}