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}