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}