zerodds_security/crypto.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Cryptographic plugin SPI (OMG DDS-Security 1.1 §8.5).
5//!
6//! The SPI is split into three sub-interfaces (spec structure):
7//! * **KeyFactory** (§8.5.1.7) — key derivation from `SharedSecret`.
8//! * **KeyExchange** (§8.5.1.8) — exchange key tokens between peers.
9//! * **Transform** (§8.5.1.9) — the concrete encrypt/decrypt/sign/verify.
10//!
11//! We bundle the three into one trait (`CryptographicPlugin`), so that
12//! users only have to hold a single `Box<dyn ...>`. Backend impls
13//! (rustls, ring, mbedtls) implement all three sub-interfaces.
14//!
15//! zerodds-lint: allow no_dyn_in_safe
16//! (The plugin SPI needs `Box<dyn CryptographicPlugin>`.)
17
18extern crate alloc;
19
20use alloc::boxed::Box;
21use alloc::vec::Vec;
22
23use crate::authentication::{IdentityHandle, SharedSecretHandle};
24use crate::error::SecurityResult;
25
26/// Opaque handle for derived key material (master key
27/// of a participant/endpoint).
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub struct CryptoHandle(pub u64);
30
31/// Receiver-specific MAC (spec §7.3.6.3 `ReceiverSpecificMAC`).
32///
33/// When a sender sends a ciphertext to N receivers with the **same
34/// suite but different keys**, a 16-byte truncated HMAC is computed
35/// per receiver. The wire representation is
36/// a sequence of `(key_id, mac)` pairs in the SEC_POSTFIX.
37///
38/// `key_id` is the spec-conformant 4-byte ID (typically the low 32 bits
39/// of the sender-side [`CryptoHandle`] for this receiver), by which
40/// the receiver finds its specific MAC entry.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub struct ReceiverMac {
43 /// 4-byte `CryptoTransformKeyId` from §7.3.6.3.
44 pub key_id: u32,
45 /// 16-byte truncated HMAC-SHA256 over the ciphertext.
46 pub mac: [u8; 16],
47}
48
49impl ReceiverMac {
50 /// Wire size of a single `ReceiverMac` entry (spec §7.3.6.3).
51 pub const WIRE_SIZE: usize = 4 + 16;
52}
53
54/// Cryptographic plugin (spec §8.5.1). In v1.3 this is a pure
55/// **interface** — production impls live in `zerodds-security-crypto`
56/// (AES-GCM + HMAC), `zerodds-security-keyexchange` (DH key exchange,
57/// spec §9.5.3) and `zerodds-security-rtps` (RTPS header AAD wrapper,
58/// spec §7.3.5).
59pub trait CryptographicPlugin: Send + Sync {
60 // -------- KeyFactory (§8.5.1.7) --------
61
62 /// Optional second (payload) token of a local datawriter endpoint,
63 /// when metadata and data protection have different suites (cyclone
64 /// dual-key model: `writer_key_material_message` + `writer_key_material_payload`).
65 /// `None` = single key (all profiles with metadata == data).
66 fn endpoint_payload_token(&self, _handle: CryptoHandle) -> Option<alloc::vec::Vec<u8>> {
67 None
68 }
69
70 /// Creates participant crypto material from the handshake
71 /// SharedSecret.
72 fn register_local_participant(
73 &mut self,
74 identity: IdentityHandle,
75 properties: &[(&str, &str)],
76 ) -> SecurityResult<CryptoHandle>;
77
78 /// Creates crypto material for a remote participant.
79 fn register_matched_remote_participant(
80 &mut self,
81 local: CryptoHandle,
82 remote_identity: IdentityHandle,
83 shared_secret: SharedSecretHandle,
84 ) -> SecurityResult<CryptoHandle>;
85
86 /// Creates crypto material for a local DataWriter/Reader.
87 fn register_local_endpoint(
88 &mut self,
89 participant: CryptoHandle,
90 is_writer: bool,
91 properties: &[(&str, &str)],
92 ) -> SecurityResult<CryptoHandle>;
93
94 // -------- KeyExchange (§8.5.1.8) --------
95
96 /// Creates the `ParticipantCryptoTokens` blob that is sent to the
97 /// remote participant (contains encrypted
98 /// key material).
99 fn create_local_participant_crypto_tokens(
100 &mut self,
101 local: CryptoHandle,
102 remote: CryptoHandle,
103 ) -> SecurityResult<Vec<u8>>;
104
105 /// Processes the tokens from the remote participant. Afterwards the
106 /// keys for encrypted submessages are mutually known.
107 fn set_remote_participant_crypto_tokens(
108 &mut self,
109 local: CryptoHandle,
110 remote: CryptoHandle,
111 tokens: &[u8],
112 ) -> SecurityResult<()>;
113
114 // -------- Transform (§8.5.1.9) --------
115
116 /// Encrypt + sign an RTPS submessage. Input: plain submessage
117 /// bytes. Output: `SecureSubmessage` payload (ciphertext + tag).
118 ///
119 /// `aad_extension` is the spec-conformant AAD extension (spec §10.5.2
120 /// Tab.78). Submessage protection (§8.5.1.9.2) provides
121 /// `SubmessageHeader || SecureSubmessageHeader` bytes here;
122 /// RTPS message protection (§8.5.1.9.7) the RTPS header (20 bytes) +
123 /// SecureRTPSSubmessageHeader. Empty (`&[]`) is only spec-conformant
124 /// if the caller explicitly accepts spec §8.1 Tab.78 without header
125 /// coverage (e.g. pre-shared-key path without header auth).
126 ///
127 /// Spec §8.5.1.9.1 `encode_serialized_payload`.
128 fn encrypt_submessage(
129 &self,
130 local: CryptoHandle,
131 remote_list: &[CryptoHandle],
132 plaintext: &[u8],
133 aad_extension: &[u8],
134 ) -> SecurityResult<Vec<u8>>;
135
136 /// Decrypt + Verify. Output: plain submessage bytes. `aad_extension`
137 /// must be byte-identical to the sender AAD (otherwise tag mismatch).
138 ///
139 /// Spec §8.5.1.9.4 `decode_serialized_payload`.
140 fn decrypt_submessage(
141 &self,
142 local: CryptoHandle,
143 remote: CryptoHandle,
144 ciphertext: &[u8],
145 aad_extension: &[u8],
146 ) -> SecurityResult<Vec<u8>>;
147
148 /// Encrypt+Sign with `Receiver-Specific-MACs` (spec
149 /// §7.3.6.3). Produces **one** ciphertext (sender key) plus
150 /// one 16-byte truncated HMAC per remote.
151 ///
152 /// The `receivers` list contains `(handle, key_id)` per receiver:
153 /// * `handle` — CryptoHandle to the MAC key in the plugin slot
154 /// (typically the per-peer key derived from
155 /// `register_matched_remote_participant`).
156 /// * `key_id` — 4-byte wire identifier the receiver looks up in
157 /// the MAC list (must be synchronized between sender and receiver,
158 /// typically the low 32 bits of the peer GuidPrefix).
159 ///
160 /// Default impl: falls back to `encrypt_submessage` and
161 /// returns an empty MAC list — plugins without multi-MAC support
162 /// thereby signal the caller "please use the multi-cipher
163 /// fan-out".
164 fn encrypt_submessage_multi(
165 &self,
166 local: CryptoHandle,
167 receivers: &[(CryptoHandle, u32)],
168 plaintext: &[u8],
169 aad_extension: &[u8],
170 ) -> SecurityResult<(Vec<u8>, Vec<ReceiverMac>)> {
171 let handles: Vec<CryptoHandle> = receivers.iter().map(|(h, _)| *h).collect();
172 let ciphertext = self.encrypt_submessage(local, &handles, plaintext, aad_extension)?;
173 Ok((ciphertext, Vec::new()))
174 }
175
176 /// Verify Receiver-Specific-MAC + Decrypt.
177 ///
178 /// `own_key_id` is the wire ID under which the receiver is found in
179 /// the MAC list; `own_mac_key_handle` is the slot with the
180 /// associated HMAC key.
181 ///
182 /// When `macs.is_empty()` this delegates to [`Self::decrypt_submessage`]
183 /// (backward compat).
184 ///
185 /// # Errors
186 /// * `CryptoFailed` if no MAC entry matches the `own_key_id`
187 /// or the MAC comparison fails.
188 #[allow(clippy::too_many_arguments)]
189 fn decrypt_submessage_with_receiver_mac(
190 &self,
191 local: CryptoHandle,
192 remote: CryptoHandle,
193 own_key_id: u32,
194 own_mac_key_handle: CryptoHandle,
195 ciphertext: &[u8],
196 macs: &[ReceiverMac],
197 aad_extension: &[u8],
198 ) -> SecurityResult<Vec<u8>> {
199 let _ = (own_key_id, own_mac_key_handle);
200 if macs.is_empty() {
201 return self.decrypt_submessage(local, remote, ciphertext, aad_extension);
202 }
203 Err(crate::error::SecurityError::new(
204 crate::error::SecurityErrorKind::NotImplemented,
205 "plugin does not implement receiver-specific mac verification",
206 ))
207 }
208
209 // -------- KeyExchange channel protection (§9.5.3.5) --------
210 //
211 // The `DCPSParticipantVolatileMessageSecure` channel (over which the
212 // ParticipantCryptoTokens are exchanged) is protected with a **Kx key**
213 // derived from the handshake SharedSecret —
214 // separate from the data key (which only arrives via token). This
215 // lets both sides exchange tokens confidentially before the
216 // data key is established.
217
218 /// Encrypt+Sign a VolatileSecure payload with the **Kx key** of the
219 /// peer slot (`handle` from `register_matched_remote_participant`).
220 ///
221 /// Default: `NotImplemented` — plugins without Kx channel support.
222 ///
223 /// # Errors
224 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
225 fn encode_kx_submessage(
226 &self,
227 handle: CryptoHandle,
228 plaintext: &[u8],
229 aad_extension: &[u8],
230 ) -> SecurityResult<Vec<u8>> {
231 let _ = (handle, plaintext, aad_extension);
232 Err(crate::error::SecurityError::new(
233 crate::error::SecurityErrorKind::NotImplemented,
234 "plugin does not implement key-exchange-channel protection",
235 ))
236 }
237
238 /// Decrypt+Verify a VolatileSecure payload with the **Kx key**.
239 /// Counterpart to [`Self::encode_kx_submessage`].
240 ///
241 /// # Errors
242 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
243 fn decode_kx_submessage(
244 &self,
245 handle: CryptoHandle,
246 ciphertext: &[u8],
247 aad_extension: &[u8],
248 ) -> SecurityResult<Vec<u8>> {
249 let _ = (handle, ciphertext, aad_extension);
250 Err(crate::error::SecurityError::new(
251 crate::error::SecurityErrorKind::NotImplemented,
252 "plugin does not implement key-exchange-channel protection",
253 ))
254 }
255
256 /// **Cyclone-conformant** VolatileSecure submessage protection (DDS-Security
257 /// §9.5.3): encodes `plaintext` as a `SEC_PREFIX` + `SEC_BODY` + `SEC_POSTFIX`
258 /// submessage sequence with the **Kx key** of the handle (AES256-GCM, empty AAD,
259 /// 20-byte CryptoHeader, common_mac in the postfix). Wire-byte-identical to
260 /// cyclone `encode_datawriter_submessage` — for the cross-vendor
261 /// crypto token exchange over `ParticipantVolatileMessageSecure`.
262 ///
263 /// # Errors
264 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
265 fn encode_kx_datawriter_submessage(
266 &self,
267 handle: CryptoHandle,
268 plaintext: &[u8],
269 ) -> SecurityResult<Vec<u8>> {
270 let _ = (handle, plaintext);
271 Err(crate::error::SecurityError::new(
272 crate::error::SecurityErrorKind::NotImplemented,
273 "plugin does not implement cyclone-format kx submessage protection",
274 ))
275 }
276
277 /// Counterpart to [`Self::encode_kx_datawriter_submessage`]: decodes a
278 /// `SEC_PREFIX`/`SEC_BODY`/`SEC_POSTFIX` sequence with the Kx key.
279 ///
280 /// # Errors
281 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
282 fn decode_kx_datawriter_submessage(
283 &self,
284 handle: CryptoHandle,
285 wire: &[u8],
286 ) -> SecurityResult<Vec<u8>> {
287 let _ = (handle, wire);
288 Err(crate::error::SecurityError::new(
289 crate::error::SecurityErrorKind::NotImplemented,
290 "plugin does not implement cyclone-format kx submessage protection",
291 ))
292 }
293
294 /// Like [`Self::encode_kx_datawriter_submessage`], but with the **data key**
295 /// of the slot (regular slot, not Kx) — for user DATA submessage
296 /// protection (`metadata_protection_kind=ENCRYPT`, §9.5.3.3). Wire-identical
297 /// to cyclone `encode_datawriter_submessage`.
298 ///
299 /// # Errors
300 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
301 fn encode_data_datawriter_submessage(
302 &self,
303 handle: CryptoHandle,
304 plaintext: &[u8],
305 ) -> SecurityResult<Vec<u8>> {
306 let _ = (handle, plaintext);
307 Err(crate::error::SecurityError::new(
308 crate::error::SecurityErrorKind::NotImplemented,
309 "plugin does not implement cyclone-format data submessage protection",
310 ))
311 }
312
313 /// Counterpart to [`Self::encode_data_datawriter_submessage`].
314 ///
315 /// # Errors
316 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
317 fn decode_data_datawriter_submessage(
318 &self,
319 handle: CryptoHandle,
320 wire: &[u8],
321 ) -> SecurityResult<Vec<u8>> {
322 let _ = (handle, wire);
323 Err(crate::error::SecurityError::new(
324 crate::error::SecurityErrorKind::NotImplemented,
325 "plugin does not implement cyclone-format data submessage protection",
326 ))
327 }
328
329 /// Decodes a SEC_* submessage by the `transformation_key_id` in the
330 /// CryptoHeader (DDS-Security §9.5.2.1.1) — finds the matching remote
331 /// key material itself, without the caller knowing the endpoint handle.
332 /// Needed because every remote endpoint (incl. the secure built-in
333 /// discovery endpoints) has its own per-endpoint key and the receiver
334 /// can only map the key via the key_id on the wire (multiple keys per peer).
335 ///
336 /// # Errors
337 /// `NotImplemented` (default) or `BadArgument` (no key for the key_id).
338 fn decode_data_by_key_id(&self, wire: &[u8]) -> SecurityResult<Vec<u8>> {
339 let _ = wire;
340 Err(crate::error::SecurityError::new(
341 crate::error::SecurityErrorKind::NotImplemented,
342 "plugin does not implement key-id-based data submessage decode",
343 ))
344 }
345
346 /// DDS-Security §8.4.2.4 / §7.3.7 RTPS message protection (SRTPS),
347 /// cyclone-conformant: wraps the whole RTPS message (`[header(20) | body]`) in
348 /// SRTPS_PREFIX(CryptoHeader with transformation_key_id) / SEC_BODY /
349 /// SRTPS_POSTFIX. Unlike `encode_secured_rtps_message`, the
350 /// SRTPS_PREFIX carries the real CryptoTransformIdentifier -> the receiver
351 /// finds the key by key_id (cross-vendor / cyclone interop).
352 ///
353 /// # Errors
354 /// `NotImplemented` (default) or crypto/argument error.
355 fn encode_rtps_message_cyclone(
356 &self,
357 local: CryptoHandle,
358 message: &[u8],
359 ) -> SecurityResult<Vec<u8>> {
360 let _ = (local, message);
361 Err(crate::error::SecurityError::new(
362 crate::error::SecurityErrorKind::NotImplemented,
363 "plugin does not implement cyclone SRTPS encode",
364 ))
365 }
366
367 /// Counterpart to [`Self::encode_rtps_message_cyclone`]: key_id-based
368 /// SRTPS decode (remote_by_key_id, fallback to local slot for self-test).
369 ///
370 /// # Errors
371 /// `NotImplemented` (default) or crypto/argument error.
372 fn decode_rtps_message_cyclone(&self, message: &[u8]) -> SecurityResult<Vec<u8>> {
373 let _ = message;
374 Err(crate::error::SecurityError::new(
375 crate::error::SecurityErrorKind::NotImplemented,
376 "plugin does not implement cyclone SRTPS decode",
377 ))
378 }
379
380 /// §8.5.1.9.1 / §9.5.3.3.1 `encode_serialized_payload` (data_protection,
381 /// INNER payload layer). Protects the SerializedPayload of an endpoint
382 /// (`handle` = per-endpoint writer key) without a submessage frame.
383 ///
384 /// # Errors
385 /// `NotImplemented` (default) or crypto/argument error.
386 fn encode_serialized_payload(
387 &self,
388 handle: CryptoHandle,
389 payload: &[u8],
390 ) -> SecurityResult<Vec<u8>> {
391 let _ = (handle, payload);
392 Err(crate::error::SecurityError::new(
393 crate::error::SecurityErrorKind::NotImplemented,
394 "plugin does not implement encode_serialized_payload",
395 ))
396 }
397
398 /// §8.5.1.9.4 / §9.5.3.3.1 `decode_serialized_payload`; key via the
399 /// `transformation_key_id` in the CryptoHeader.
400 ///
401 /// # Errors
402 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
403 fn decode_serialized_payload(&self, encoded: &[u8]) -> SecurityResult<Vec<u8>> {
404 let _ = encoded;
405 Err(crate::error::SecurityError::new(
406 crate::error::SecurityErrorKind::NotImplemented,
407 "plugin does not implement decode_serialized_payload",
408 ))
409 }
410
411 /// Like [`Self::decode_serialized_payload`], but with an EXPLICIT remote handle
412 /// instead of a key_id lookup. Needed for peers that index their remote key
413 /// material via the GuidPrefix slot table (token exchange) instead of a unique
414 /// `transformation_key_id` (zero↔zero fallback, analogous to
415 /// `decode_data_datawriter_submessage`).
416 ///
417 /// # Errors
418 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
419 fn decode_serialized_payload_with(
420 &self,
421 handle: CryptoHandle,
422 encoded: &[u8],
423 ) -> SecurityResult<Vec<u8>> {
424 let _ = (handle, encoded);
425 Err(crate::error::SecurityError::new(
426 crate::error::SecurityErrorKind::NotImplemented,
427 "plugin does not implement decode_serialized_payload_with",
428 ))
429 }
430
431 /// Like [`Self::decode_serialized_payload_with`], but opens the
432 /// SerializedPayload with the **Kx/participant key material** of the handle
433 /// (BuiltinParticipantVolatileMessageSecure key, §10.5.2.1.2 Tab. 73) instead
434 /// of the per-endpoint DataWriter key. Needed for vendors (cyclone) that
435 /// encrypt the data_protection payload with the SharedSecret-derived participant
436 /// key (transformation_key_id=0) instead of the datawriter key.
437 ///
438 /// # Errors
439 /// `NotImplemented` (default) or `BadArgument`/`CryptoFailed`.
440 fn decode_serialized_payload_kx(
441 &self,
442 handle: CryptoHandle,
443 encoded: &[u8],
444 ) -> SecurityResult<Vec<u8>> {
445 let _ = (handle, encoded);
446 Err(crate::error::SecurityError::new(
447 crate::error::SecurityErrorKind::NotImplemented,
448 "plugin does not implement decode_serialized_payload_kx",
449 ))
450 }
451
452 /// Plugin class id (e.g. "DDS:Crypto:AES-GCM-GMAC:1.2").
453 fn plugin_class_id(&self) -> &str;
454}
455
456/// Factory alias.
457pub type CryptoPluginBox = Box<dyn CryptographicPlugin>;