Skip to main content

zerodds_security/
authentication.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Authentication plugin SPI (OMG DDS-Security 1.1 §8.3).
5//!
6//! Responsible for:
7//! 1. **Identity validation** at participant start — validate the local
8//!    identity (e.g. X.509 cert + private key) against the trust anchor.
9//! 2. **Identity handshake** between two participants — challenge/
10//!    response with signed nonces, spec §8.3.2.
11//! 3. **SharedSecret creation** at the end of the handshake — input
12//!    for the CryptographicPlugin for key derivation.
13//!
14//! The SPI is **state-machine-light** — the plugin holds the
15//! handshake state itself. The caller (DCPS layer) only triggers
16//! `begin_handshake_request/reply`, `process_handshake_reply`,
17//! `process_handshake_final`.
18//!
19//! zerodds-lint: allow no_dyn_in_safe
20//! (The plugin SPI needs `Box<dyn AuthenticationPlugin>`.)
21
22extern crate alloc;
23
24use alloc::boxed::Box;
25use alloc::vec::Vec;
26
27use crate::error::SecurityResult;
28use crate::properties::PropertyList;
29
30/// Opaque handle for a validated identity. The plugin-internal
31/// state (X.509 cert, keys) is not exposed outward through this
32/// handle.
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
34pub struct IdentityHandle(pub u64);
35
36/// Opaque handle for a running handshake.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub struct HandshakeHandle(pub u64);
39
40/// Opaque handle for a shared secret (output of a completed
41/// handshake). Passed on to the `CryptographicPlugin`.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
43pub struct SharedSecretHandle(pub u64);
44
45/// Lookup bridge between [`AuthenticationPlugin`] and
46/// [`crate::crypto::CryptographicPlugin`].
47///
48/// After the handshake the authentication plugin produces a
49/// [`SharedSecretHandle`] and knows the associated 32 bytes from
50/// `x25519 + HKDF-SHA256`. The crypto plugin needs exactly these
51/// bytes to derive its per-peer master key — instead of generating a
52/// random key and routing it as an opaque token through the
53/// governance.
54///
55/// Implementors must be **Send + Sync** so that the crypto
56/// plugin can hold the provider via `Arc<dyn SharedSecretProvider>`.
57pub trait SharedSecretProvider: Send + Sync {
58    /// Returns the raw bytes of the shared key.
59    /// `None` if the handle is unknown (handshake not yet
60    /// completed or already discarded).
61    fn get_shared_secret(&self, handle: SharedSecretHandle) -> Option<alloc::vec::Vec<u8>>;
62
63    /// Returns `(challenge1, challenge2)` of the associated handshake
64    /// (challenge1 = initiator, challenge2 = replier). These feed into the
65    /// VolatileSecure key derivation (DDS-Security §9.5.3.5, `calculate_kx_keys`)
66    /// — both peers derive the same Kx key from `(SharedSecret, challenge1,
67    /// challenge2)`. Default `None` for providers without challenge tracking
68    /// (e.g. PSK/mock); their Kx path then uses the fallback.
69    fn get_shared_secret_challenges(
70        &self,
71        _handle: SharedSecretHandle,
72    ) -> Option<([u8; 32], [u8; 32])> {
73        None
74    }
75}
76
77/// Result of a handshake step.
78#[derive(Debug, Clone)]
79#[non_exhaustive]
80pub enum HandshakeStepOutcome {
81    /// The plugin wants to send another message to the peer.
82    SendMessage {
83        /// Opaque handshake token as a byte blob. Goes 1:1 into the
84        /// `AuthHandshake` built-in submessage (spec §9.3.2.3).
85        token: Vec<u8>,
86    },
87    /// Handshake completed successfully — `SharedSecretHandle`
88    /// usable.
89    Complete {
90        /// Shared key handle for the CryptographicPlugin.
91        secret: SharedSecretHandle,
92    },
93    /// Handshake still needs a message from the peer — nothing to do.
94    WaitingForPeer,
95}
96
97/// Authentication plugin trait. Spec §8.3.2.7.
98pub trait AuthenticationPlugin: Send + Sync {
99    /// Called once at participant start: validate the local identity
100    /// (certificate, key, trust anchor) and return a handle.
101    ///
102    /// # Spec
103    /// §8.3.2.7.1 `validate_local_identity`.
104    fn validate_local_identity(
105        &mut self,
106        props: &PropertyList,
107        participant_guid: [u8; 16],
108    ) -> SecurityResult<IdentityHandle>;
109
110    /// Called as soon as a remote participant has been discovered via
111    /// SPDP. The plugin validates the remote cert (from `remote_auth_token`)
112    /// against its trust store.
113    ///
114    /// # Spec
115    /// §8.3.2.7.2 `validate_remote_identity`.
116    fn validate_remote_identity(
117        &mut self,
118        local: IdentityHandle,
119        remote_participant_guid: [u8; 16],
120        remote_auth_token: &[u8],
121    ) -> SecurityResult<IdentityHandle>;
122
123    /// Cross-vendor quirk: determines whether the next handshake algorithm
124    /// strings (`c.dsign_algo`/`c.kagree_algo`) are emitted + hashed
125    /// NUL-terminated. OpenDDS compares them with `sizeof` (incl. `\0`) and
126    /// needs the NUL form; FastDDS (#3803) needs them WITHOUT; cyclone is
127    /// tolerant. Since the handshake runs per-peer, the discovery layer calls
128    /// this based on the peer's `VendorId` BEFORE `begin_handshake_request` or
129    /// `begin_handshake_reply`. Default no-op (NUL-free = spec/FastDDS/
130    /// Cyclone-conformant).
131    fn set_algo_nul_terminate(&mut self, _nul: bool) {}
132
133    /// Starts the handshake. Returns the first token that must be
134    /// sent to the peer.
135    ///
136    /// # Spec
137    /// §8.3.2.7.3 `begin_handshake_request`.
138    fn begin_handshake_request(
139        &mut self,
140        initiator: IdentityHandle,
141        replier: IdentityHandle,
142    ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)>;
143
144    /// Peer side of the handshake start. `request_token` is what the
145    /// initiator sent via `begin_handshake_request`.
146    ///
147    /// # Spec
148    /// §8.3.2.7.4 `begin_handshake_reply`.
149    fn begin_handshake_reply(
150        &mut self,
151        replier: IdentityHandle,
152        initiator: IdentityHandle,
153        request_token: &[u8],
154    ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)>;
155
156    /// Pass through follow-up handshake messages.
157    ///
158    /// # Spec
159    /// §8.3.2.7.5 `process_handshake`.
160    fn process_handshake(
161        &mut self,
162        handshake: HandshakeHandle,
163        token: &[u8],
164    ) -> SecurityResult<HandshakeStepOutcome>;
165
166    /// Ends the handshake and returns the final SharedSecret.
167    /// Failure aborts. Called by the caller after a `Complete`
168    /// outcome to pull the secret out of the plugin.
169    ///
170    /// Alternatively: the `Complete` outcome already contains the
171    /// handle — this method is only for polling integrations.
172    ///
173    /// # Spec
174    /// §8.3.2.7.8 `get_shared_secret`.
175    fn shared_secret(&self, handshake: HandshakeHandle) -> SecurityResult<SharedSecretHandle>;
176
177    /// Identity plugin name (e.g. "DDS:Auth:PKI-DH:1.2"). Announced in SPDP as
178    /// `dds.sec.auth.plugin_class`.
179    fn plugin_class_id(&self) -> &str;
180
181    /// Returns the `IdentityToken` for a local identity (spec
182    /// §9.3.2.4). Published in the SPDP announce as `PID_IDENTITY_TOKEN` (0x1001).
183    /// Default: empty token (= the plugin does not support the
184    /// feature).
185    ///
186    /// # Errors
187    /// Implementation-specific.
188    fn get_identity_token(&self, _local: IdentityHandle) -> SecurityResult<Vec<u8>> {
189        Ok(Vec::new())
190    }
191
192    /// Returns the `IdentityStatusToken` for a local identity
193    /// (spec §9.3.2.5.1.2). Default: empty.
194    ///
195    /// # Errors
196    /// Implementation-specific.
197    fn get_identity_status_token(&self, _local: IdentityHandle) -> SecurityResult<Vec<u8>> {
198        Ok(Vec::new())
199    }
200
201    /// Returns the `PermissionsToken` (spec §7.2.4, `PID_PERMISSIONS_TOKEN`
202    /// 0x1002) for the SPDP announce. Strictly per spec the
203    /// AccessControlPlugin produces it; since ZeroDDS holds the permissions in
204    /// the auth plugin (`set_local_permissions`, for the `c.perm` handshake),
205    /// the getter lives here. Default: empty (no permissions configured ⇒
206    /// AccessControl inactive ⇒ token omitted). Cross-vendor requirement:
207    /// secure vendors (cyclone/FastDDS) only validate a remote
208    /// if SPDP carries **both** tokens (identity + permissions).
209    fn get_permissions_token(&self) -> Vec<u8> {
210        Vec::new()
211    }
212
213    /// Sets the local `ParticipantBuiltinTopicData` as PL_CDR bytes that
214    /// are sent along in the handshake as `c.pdata` (spec §9.3.2.5.2). The
215    /// replier deserializes c.pdata as a ParameterList and binds the
216    /// participant_guid to the authenticated identity. Default: no-op.
217    fn set_local_participant_data(&mut self, _pdata: alloc::vec::Vec<u8>) {}
218
219    /// Sets the permissions credential and the permissions token on
220    /// a local identity (spec §9.3.2.4 + §9.3.2.5.4). Fed by the
221    /// caller layer with the output of the AccessControlPlugin.
222    ///
223    /// # Errors
224    /// Default: `Unsupported` (the plugin ignores the permissions bind).
225    fn set_permissions_credential_and_token(
226        &mut self,
227        _local: IdentityHandle,
228        _permissions_credential: &[u8],
229        _permissions_token: &[u8],
230    ) -> SecurityResult<()> {
231        Ok(())
232    }
233
234    /// Returns the `AuthenticatedPeerCredentialToken` (spec §9.3.2.5.6).
235    /// Fetched by the AccessControl layer after a successful handshake
236    /// to perform the caller subject match.
237    /// Default: empty.
238    ///
239    /// # Errors
240    /// Implementation-specific.
241    fn get_authenticated_peer_credential_token(
242        &self,
243        _handshake: HandshakeHandle,
244    ) -> SecurityResult<Vec<u8>> {
245        Ok(Vec::new())
246    }
247}
248
249/// Factory alias — avoids `Box<dyn ...>` boilerplate at call sites.
250pub type AuthPluginBox = Box<dyn AuthenticationPlugin>;
251
252#[cfg(test)]
253#[allow(clippy::expect_used)]
254mod tests {
255    use super::*;
256
257    // Compile check: the stub impl demonstrates that the trait is object-safe
258    // + Send+Sync satisfiable.
259    struct StubAuth;
260    impl AuthenticationPlugin for StubAuth {
261        fn validate_local_identity(
262            &mut self,
263            _props: &PropertyList,
264            _guid: [u8; 16],
265        ) -> SecurityResult<IdentityHandle> {
266            Ok(IdentityHandle(1))
267        }
268        fn validate_remote_identity(
269            &mut self,
270            _l: IdentityHandle,
271            _r: [u8; 16],
272            _t: &[u8],
273        ) -> SecurityResult<IdentityHandle> {
274            Ok(IdentityHandle(2))
275        }
276        fn begin_handshake_request(
277            &mut self,
278            _i: IdentityHandle,
279            _r: IdentityHandle,
280        ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)> {
281            Ok((HandshakeHandle(1), HandshakeStepOutcome::WaitingForPeer))
282        }
283        fn begin_handshake_reply(
284            &mut self,
285            _r: IdentityHandle,
286            _i: IdentityHandle,
287            _t: &[u8],
288        ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)> {
289            Ok((HandshakeHandle(1), HandshakeStepOutcome::WaitingForPeer))
290        }
291        fn process_handshake(
292            &mut self,
293            _h: HandshakeHandle,
294            _t: &[u8],
295        ) -> SecurityResult<HandshakeStepOutcome> {
296            Ok(HandshakeStepOutcome::Complete {
297                secret: SharedSecretHandle(1),
298            })
299        }
300        fn shared_secret(&self, _h: HandshakeHandle) -> SecurityResult<SharedSecretHandle> {
301            Ok(SharedSecretHandle(1))
302        }
303        fn plugin_class_id(&self) -> &str {
304            "DDS:Auth:Stub"
305        }
306    }
307
308    #[test]
309    fn stub_can_be_boxed() {
310        let _: AuthPluginBox = Box::new(StubAuth);
311    }
312}