Skip to main content

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>;