Skip to main content

co_identity/types/didcomm/
header.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2// Copyright (C) 2026 1io BRANDGUARDIAN GmbH
3
4use crate::{DidCommPrivateContext, DidCommPublicContext, Identity, PrivateIdentity};
5use anyhow::anyhow;
6use co_primitives::CoDateRef;
7use serde::{Deserialize, Serialize};
8use std::collections::{BTreeMap, BTreeSet};
9
10/// See: https://identity.foundation/didcomm-messaging/spec/#message-headers
11#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
12pub struct DidCommHeader {
13	/// REQUIRED. Message ID. The id attribute value MUST be unique to the sender, across all messages they send. See
14	/// Threading > Message IDs for constraints on this value.
15	pub id: String,
16
17	/// REQUIRED. A URI that associates the body of a plaintext message with a published and versioned schema. Useful
18	/// for message handling in application-level protocols. The type attribute value MUST be a valid message type URI,
19	/// that when resolved gives human readable information about the message category.
20	#[serde(rename = "type")]
21	pub message_type: String,
22
23	/// OPTIONAL. Identifier(s) for recipients. MUST be an array of strings where each element is a valid DID or DID
24	/// URL (without the fragment component) that identifies a member of the message's intended audience. These values
25	/// are useful for recipients to know which of their keys can be used for decryption. It is not possible for one
26	/// recipient to verify that the message was sent to a different recipient.
27	///
28	/// When Alice sends the same plaintext message to Bob and Carol, it is by inspecting this header that the
29	/// recipients learn the message was sent to both of them. If the header is omitted, each recipient SHOULD assume
30	/// they are the only recipient (much like an email sent only to BCC: addresses).
31	///
32	/// For signed messages, there are specific requirements around properly defining the to header outlined in the
33	/// DIDComm Signed Message definition above. This prevents certain kind of forwarding attacks, where a message that
34	/// was not meant for a given recipient is forwarded along with its signature to a recipient which then might
35	/// blindly trust it because of the signature.
36	///
37	/// Upon reception of a message with a defined to header, the recipient SHOULD verify that their own identifier
38	/// appears in the list. Implementations MUST NOT fail to accept a message when this is not the case, but SHOULD
39	/// give a warning to their user as it could indicate malicious intent from the sender.
40	///
41	/// The to header cannot be used for routing, since it is encrypted at every intermediate point in a route.
42	/// Instead, the forward message contains a next attribute in its body that specifies the target for the next
43	/// routing operation.
44	#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
45	pub to: BTreeSet<String>,
46
47	/// OPTIONAL when the message is to be encrypted via anoncrypt; REQUIRED when the message is encrypted via
48	/// authcrypt. Sender identifier. The from attribute MUST be a string that is a valid DID or DID URL (without the
49	/// fragment component) which identifies the sender of the message. When a message is encrypted, the sender key
50	/// MUST be authorized for encryption by this DID. Authorization of the encryption key for this DID MUST be
51	/// verified by message recipient with the proper proof purposes. When the sender wishes to be anonymous using
52	/// authcrypt, it is recommended to use a new DID created for the purpose to avoid correlation with any other
53	/// behavior or identity. Peer DIDs are lightweight and require no ledger writes, and therefore a good method to
54	/// use for this purpose.
55	#[serde(default, skip_serializing_if = "Option::is_none")]
56	pub from: Option<String>,
57
58	/// OPTIONAL. Thread identifier. Uniquely identifies the thread that the message belongs to. If not included, the
59	/// id property of the message MUST be treated as the value of the thid. See Threads for details.
60	#[serde(default, skip_serializing_if = "Option::is_none")]
61	pub thid: Option<String>,
62
63	/// OPTIONAL. Parent thread identifier. If the message is a child of a thread the pthid will uniquely identify
64	/// which thread is the parent. See Parent Threads for details.
65	#[serde(default, skip_serializing_if = "Option::is_none")]
66	pub pthid: Option<String>,
67
68	/// OPTIONAL but recommended. Message Created Time. This attribute is used for the sender to express when they
69	/// created the message, expressed in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z) as an integer. This
70	/// allows the recipient to guess about transport latency and clock divergence. The difference between when a
71	/// message is created and when it is sent is assumed to be negligible; this lets timeout logic start from this
72	/// value.
73	#[serde(default, skip_serializing_if = "Option::is_none")]
74	pub created_time: Option<u64>,
75
76	/// OPTIONAL. Message Expires Time. This attribute is used for the sender to express when they will consider the
77	/// message to be expired, expressed in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z) as an integer. By
78	/// default, the meaning of “expired” is that the sender will abort the protocol if it doesn’t get a response by
79	/// this time. However, protocols can nuance this in their formal spec. For example, an online auction protocol
80	/// might specify that timed out bids must be ignored instead of triggering a cancellation of the whole auction.
81	/// When omitted from any given message, the message is considered to have no expiration by the sender.
82	#[serde(default, skip_serializing_if = "Option::is_none")]
83	pub expires_time: Option<u64>,
84
85	/// OPTIONAL. Custom fields.
86	#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
87	pub fields: BTreeMap<String, String>,
88}
89impl DidCommHeader {
90	/// Create new DidCommHeader with an
91	pub fn new(date: &CoDateRef, message_type: impl Into<String>) -> Self {
92		Self {
93			id: Self::create_message_id(),
94			created_time: Some(date.now_duration().as_secs()),
95			message_type: message_type.into(),
96			..Default::default()
97		}
98	}
99
100	/// Create new DidCommHeader for a message with sender `from` and single recipient `to`.
101	pub fn create<F, T>(
102		date: &CoDateRef,
103		from: &F,
104		to: &T,
105		message_type: impl Into<String>,
106	) -> anyhow::Result<(DidCommPrivateContext, DidCommPublicContext, Self)>
107	where
108		F: PrivateIdentity + Send + Sync + 'static,
109		T: Identity + Send + Sync + 'static,
110	{
111		let mut header = DidCommHeader::new(date, message_type.into());
112		header.from = Some(from.identity().to_owned());
113		header.to = [to.identity().to_owned()].into_iter().collect();
114		Ok((from.try_didcomm_private()?, to.try_didcomm_public()?, header))
115	}
116
117	/// Create new DidCommHeader for a message with sender `from` and unknown recipent(s).
118	pub fn create_from<F>(
119		date: &CoDateRef,
120		from: &F,
121		message_type: impl Into<String>,
122	) -> anyhow::Result<(DidCommPrivateContext, Self)>
123	where
124		F: PrivateIdentity + Send + Sync + 'static,
125	{
126		let mut header = DidCommHeader::new(date, message_type.into());
127		header.from = Some(from.identity().to_owned());
128		Ok((from.try_didcomm_private()?, header))
129	}
130
131	/// Create random message id.
132	pub fn create_message_id() -> String {
133		uuid::Uuid::new_v4().to_string()
134	}
135
136	pub fn with_fields(mut self, fields: impl IntoIterator<Item = (String, String)>) -> Result<Self, anyhow::Error> {
137		for (key, value) in fields {
138			match key.as_str() {
139				"id" | "type" | "to" | "from" | "thid" | "pthid" | "created_time" | "expires_time" => {
140					return Err(anyhow!("Reserved key: {}", key));
141				},
142				_ => {},
143			}
144			self.fields.insert(key, value);
145		}
146		Ok(self)
147	}
148}
149
150#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
151pub struct PeerDidCommHeader {
152	/// OPTIONAL. The PeerId encoded as a string of the producer of the message.
153	/// This is used to verifiable correlate a Did and a PeerId.
154	#[serde(rename = "fpid", default, skip_serializing_if = "Option::is_none")]
155	pub from_peer_id: Option<String>,
156
157	/// Header.
158	#[serde(flatten)]
159	pub header: DidCommHeader,
160}
161impl From<DidCommHeader> for PeerDidCommHeader {
162	fn from(mut header: DidCommHeader) -> Self {
163		Self { from_peer_id: header.fields.remove("fpid"), header }
164	}
165}
166impl From<PeerDidCommHeader> for DidCommHeader {
167	fn from(value: PeerDidCommHeader) -> Self {
168		let mut header = value.header;
169		if let Some(value) = value.from_peer_id {
170			header.fields.insert("fpid".to_owned(), value);
171		}
172		header
173	}
174}
175
176#[cfg(test)]
177mod tests {
178	use crate::{DidCommHeader, PeerDidCommHeader};
179	use co_primitives::{from_json_string, to_json_string};
180
181	#[test]
182	fn test_serialize_peer() {
183		let header = DidCommHeader { message_type: "test".to_owned(), ..Default::default() };
184		let mut header_with_field = header.clone();
185		header_with_field.fields.insert("fpid".to_owned(), "peer".to_owned());
186		let peer_header = PeerDidCommHeader { header, from_peer_id: Some("peer".to_owned()) };
187		let json = to_json_string(&peer_header).unwrap();
188		let header_from_json: DidCommHeader = from_json_string(&json).unwrap();
189		let peer_header_from_json: PeerDidCommHeader = from_json_string(&json).unwrap();
190		assert_eq!(peer_header_from_json, peer_header);
191		assert_eq!(header_from_json, header_with_field);
192	}
193}