Skip to main content

co_identity/types/didcomm/
message.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 1io BRANDGUARDIAN GmbH
3
4use crate::{
5	library::didcomm_receive::didcomm_receive, DidCommContext, DidCommHeader, Identity, IdentityResolver,
6	PrivateIdentity, PrivateIdentityResolver, ReceiveError,
7};
8use anyhow::anyhow;
9use co_primitives::{from_json_string, Did};
10use didcomm_rs::{Jwe, Jws, MessageType};
11use serde::{de::DeserializeOwned, Deserialize, Serialize};
12use serde_json::value::RawValue;
13
14/// DIDComm Message Envelope
15///
16/// See: https://identity.foundation/didcomm-messaging/spec/v2.1/#iana-media-types
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Message {
19	/// Unsiged JSON encoded message.
20	///
21	/// Envelope: `plaintext` (no envelope)
22	/// Media Type: `application/didcomm-plain+json`
23	PlainJson { header: DidCommHeader, body: String },
24
25	/// Signed JSON encoded message.
26	///
27	/// The identity has been verified when this is constructed.
28	///
29	/// Envelope: `signed(plaintext)`
30	/// Media Type: `application/didcomm-signed+json`
31	SignedJson { sender: Did, header: DidCommHeader, body: String },
32
33	/// Encrypted JSON encoded message.
34	///
35	/// Guarantees confidentiality and integrity without revealing the identity of the sender.
36	///
37	/// Envelope: `anoncrypt(plaintext)`
38	/// Media Type: `application/didcomm-encrypted+json`
39	AnonCryptJson { header: DidCommHeader, body: String },
40
41	/// Encrypted authenticated JSON encoded message.
42	///
43	/// Guarantees confidentiality and integrity. Also proves the identity of the sender – but in a way that only the
44	/// recipient can verify. This is the default wrapping choice, and SHOULD be used unless a different goal is
45	/// clearly identified. By design, this combination and all other combinations that use encryption in their
46	/// outermost layer share an identical IANA media type, because only the recipient should care about the
47	/// difference. Media Type: `application/didcomm-encrypted+json`
48	///
49	/// The identity has been verified when this is constructed.
50	///
51	/// Envelope: `authcrypt(plaintext)`
52	/// Media Type: `application/didcomm-encrypted+json`
53	AuthCryptJson { sender: Did, header: DidCommHeader, body: String },
54}
55impl Message {
56	/// Receive message from data.
57	pub async fn receive<I, P>(sender_resolver: I, recipent_resolver: P, data: &[u8]) -> Result<Message, ReceiveError>
58	where
59		I: IdentityResolver + Send + Sync + 'static,
60		P: PrivateIdentityResolver + Send + Sync + 'static,
61	{
62		let message = std::str::from_utf8(data).map_err(|e| ReceiveError::UnknownFormat(e.into()))?;
63		let message_type = get_message_type(message).map_err(ReceiveError::UnknownFormat)?;
64		if message_type == MessageType::DidCommJwe {
65			let jwe: Jwe = serde_json::from_str(message).map_err(|e| ReceiveError::UnknownFormat(e.into()))?;
66
67			// for anoncrypt this is usually the ephemeral sender did
68			let sender_identity = if let Some(sender_kid) = &jwe.get_skid() {
69				Some(
70					sender_resolver
71						.resolve(sender_kid)
72						.await
73						.map_err(|e| ReceiveError::BadDid(sender_kid.to_owned(), e.into()))?,
74				)
75			} else {
76				None
77			};
78
79			// get recipents
80			let recipents = jwe
81				.recipients
82				.unwrap_or_else(|| jwe.recipient.map(|item| vec![item]).unwrap_or_default());
83			let recipent_resolver_ref = &recipent_resolver;
84
85			// try to receive message
86			for recipent in &recipents {
87				let recipent_did = match &recipent.header.kid {
88					Some(kid) => kid,
89					None => continue,
90				};
91				let recipent_identity = match recipent_resolver_ref.resolve_private(recipent_did).await {
92					Ok(i) => i,
93					Err(_) => continue,
94				};
95				let recipent_didcomm_context = match recipent_identity.didcomm_private() {
96					Some(i) => i,
97					None => continue,
98				};
99				let (header, body) = recipent_didcomm_context.receive(&sender_resolver, message).await?;
100
101				// result
102				// when the encryption sender key is equal to the from header we have authcrypt
103				// See: https://identity.foundation/didcomm-messaging/spec/v2.1/#message-headers
104				if let Some(from) = &header.from {
105					if let Some(sender_identity) = sender_identity {
106						if from == sender_identity.identity() {
107							return Ok(Message::AuthCryptJson {
108								sender: sender_identity.identity().to_owned(),
109								header,
110								body,
111							});
112						}
113					}
114				}
115				return Ok(Message::AnonCryptJson { header, body });
116			}
117			return Err(ReceiveError::NoRecipent);
118		}
119		if message_type == MessageType::DidCommJws {
120			let (header, body) = didcomm_receive(None, &sender_resolver, message).await?;
121
122			// resolve sender
123			let sender = verify_signing_identity(&sender_resolver, &header, message).await?;
124
125			// result
126			return Ok(Message::SignedJson { sender, header, body });
127		}
128		if message_type == MessageType::DidCommRaw {
129			let plain_message: DidCommMessage =
130				serde_json::from_str(message).map_err(|e| ReceiveError::UnknownFormat(e.into()))?;
131			return Ok(Message::PlainJson {
132				header: plain_message.header,
133				body: plain_message.body.map(|r| r.get()).unwrap_or("null").to_owned(),
134			});
135		}
136		Err(ReceiveError::UnknownFormat(anyhow!("Expected JSON as JWE, JWS or plain DIDComm")))
137	}
138
139	/// Return message header.
140	pub fn header(&self) -> &DidCommHeader {
141		match self {
142			Message::PlainJson { header, body: _ } => header,
143			Message::SignedJson { sender: _, header, body: _ } => header,
144			Message::AnonCryptJson { header, body: _ } => header,
145			Message::AuthCryptJson { sender: _, header, body: _ } => header,
146		}
147	}
148
149	/// Return Body as JSON string.
150	pub fn body(&self) -> &str {
151		match self {
152			Message::PlainJson { header: _, body } => body,
153			Message::SignedJson { sender: _, header: _, body } => body,
154			Message::AnonCryptJson { header: _, body } => body,
155			Message::AuthCryptJson { sender: _, header: _, body } => body,
156		}
157	}
158
159	/// Try to deserialize message to T.
160	pub fn body_deserialize<T: DeserializeOwned>(&self) -> Result<T, anyhow::Error> {
161		Ok(from_json_string(self.body())?)
162	}
163
164	/// Test if message is validated.
165	pub fn is_validated_sender(&self) -> bool {
166		self.sender().is_some()
167	}
168
169	/// Get validated sender.
170	pub fn sender(&self) -> Option<&Did> {
171		match self {
172			Message::AuthCryptJson { sender, header, body: _ } if Some(sender) == header.from.as_ref() => Some(sender),
173			Message::SignedJson { sender, header, body: _ } if Some(sender) == header.from.as_ref() => Some(sender),
174			_ => None,
175		}
176	}
177
178	// Convert into inner.
179	pub fn into_inner(self) -> (DidCommHeader, String) {
180		match self {
181			Message::PlainJson { header, body } => (header, body),
182			Message::SignedJson { sender: _, header, body } => (header, body),
183			Message::AnonCryptJson { header, body } => (header, body),
184			Message::AuthCryptJson { sender: _, header, body } => (header, body),
185		}
186	}
187}
188
189/// Helper type to check if received message is plain, signed or encrypted
190///
191/// Source: https://github.com/dkuhnert/didcomm-rs/blob/main/src/messages/helpers/receive.rs
192#[derive(Serialize, Deserialize, Debug)]
193struct UnknownReceivedMessage<'a> {
194	#[serde(borrow)]
195	pub signature: Option<&'a RawValue>,
196
197	#[serde(borrow)]
198	pub signatures: Option<&'a RawValue>,
199
200	#[serde(borrow)]
201	pub iv: Option<&'a RawValue>,
202}
203
204/// Tries to parse message and checks for well known fields to derive message type.
205///
206/// Source: https://github.com/dkuhnert/didcomm-rs/blob/main/src/messages/helpers/receive.rs
207fn get_message_type(message: &str) -> Result<MessageType, anyhow::Error> {
208	// try to skip parsing by using known fields from jwe/jws
209	let to_check: UnknownReceivedMessage = serde_json::from_str(message)?;
210	if to_check.iv.is_some() {
211		return Ok(MessageType::DidCommJwe);
212	}
213	if to_check.signatures.is_some() || to_check.signature.is_some() {
214		return Ok(MessageType::DidCommJws);
215	}
216	let didcomm_message: Option<didcomm_rs::Message> = serde_json::from_str(message).ok();
217	if let Some(didcomm_message) = didcomm_message {
218		return Ok(didcomm_message.get_jwm_header().typ.clone());
219	}
220	let _plain_message: DidCommMessage = serde_json::from_str(message)?;
221	Ok(MessageType::DidCommRaw)
222}
223
224/// Verify plain text `from` header matches any signature `kid`.
225/// We accept if:
226/// - the `kid` is a known from public key.
227/// - the `kid` is the same value as the `from` header.
228async fn verify_signing_identity<I>(
229	sender_resolver: &I,
230	header: &DidCommHeader,
231	message: &str,
232) -> Result<Did, ReceiveError>
233where
234	I: IdentityResolver + Send + Sync + 'static,
235{
236	if let Some(from) = &header.from {
237		// from
238		let sender_identity = sender_resolver
239			.resolve(from)
240			.await
241			.map_err(|e| ReceiveError::BadDid(from.to_owned(), e.into()))?;
242		let sender_context = sender_identity
243			.didcomm_public()
244			.ok_or_else(|| ReceiveError::BadDid(from.to_owned(), anyhow!("No didcomm public context.")))?;
245		let sender_public_key = sender_context
246			.verification_method()
247			.public_key_bytes()
248			.map_err(|err| ReceiveError::BadDid(from.to_owned(), err))?;
249		let sender_kid = hex::encode(&sender_public_key);
250
251		// check signature kid
252		let jws: Jws = serde_json::from_str(message).map_err(|e| ReceiveError::UnknownFormat(e.into()))?;
253		let signatures = if let Some(signatures) = jws.signatures {
254			signatures
255		} else if let Some(signature) = jws.signature {
256			vec![signature]
257		} else {
258			vec![]
259		};
260		for signature in signatures {
261			if let Some(kid) = &signature.get_kid() {
262				if kid == &sender_kid || kid == sender_identity.identity() {
263					return Ok(from.clone());
264				}
265			}
266		}
267		Err(ReceiveError::InvalidSigningKeyId(anyhow!("Can not match from header")))
268	} else {
269		Err(ReceiveError::UnknownFormat(anyhow!("No from header")))
270	}
271}
272
273#[derive(Debug, Serialize, Deserialize)]
274struct DidCommMessage<'a> {
275	#[serde(flatten)]
276	header: DidCommHeader,
277	#[serde(borrow)]
278	body: Option<&'a RawValue>,
279}