Skip to main content

zerodds_security_runtime/
gateway_bridge.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Gateway-Bridge-Helper.
5//!
6//! Architektur-Referenz: `docs/architecture/09_delegation.md` §3 (Use-
7//! Cases) + §5.3 (Bridge-Sub-Gateway-Chaining).
8//!
9//! Ein [`GatewayBridge`] sitzt typischerweise auf einer Wanne- oder
10//! Turm-Recheneinheit, die fuer mehrere Edge-Peers (Sensoren, ECUs)
11//! ohne eigenes Cert verantwortlich ist. Der Bridge:
12//!
13//! 1. **Stellt** Delegation-Links pro Edge-Peer aus, signiert mit dem
14//!    eigenen Gateway-Schluessel.
15//! 2. **Verwaltet** die aktiven Delegations in einer `BTreeMap`
16//!    (`edge_guid → DelegationLink`).
17//! 3. **Reicht** auf Anfrage die volle [`DelegationChain`] an den
18//!    Discovery-Layer (SPDP-Property), wahlweise als 1-Hop oder
19//!    n-Hop wenn der Bridge selbst Delegatee einer hoeheren Ebene ist
20//!    (Doppelstern Wanne+Turm).
21//! 4. **Widerruft** Delegations explizit (Revocation-Liste, die im
22//!    naechsten SPDP-Beacon mitgeschickt wird).
23//!
24//! Der Bridge **fuehrt keinen Forwarding-Pfad selbst** — er ist der
25//! Policy-/Datenmodell-Helper, das eigentliche Re-Sealing und
26//! Forwarding der RTPS-Frames passiert in der DCPS-Runtime (Plan
27//! §Stufe j-g, kommt spaeter).
28
29extern crate alloc;
30
31use alloc::collections::BTreeMap;
32use alloc::string::String;
33use alloc::vec::Vec;
34
35use zerodds_security_permissions::EdgeIdentityConfig;
36use zerodds_security_pki::{DelegationChain, DelegationError, DelegationLink, SignatureAlgorithm};
37
38/// Konfiguration eines Gateway-Bridges.
39///
40/// `gateway_guid` ist der 16-byte Subject-GUID, den die ausgestellten
41/// Delegations als `delegator_guid` tragen.
42/// `signing_key` ist das PKCS#8-DER-formatierte Privatkey-Material zum
43/// Signieren neuer Links — der Bridge haelt das im RAM, lade-Mechanismus
44/// liegt beim Caller (Filesystem, Secret-Manager, HSM).
45/// `algorithm` muss zum Trust-Anchor des Profile passen, gegen das die
46/// Chain spaeter validiert wird.
47#[derive(Debug, Clone)]
48pub struct GatewayBridgeConfig {
49    /// 16-byte Gateway-Participant-GUID.
50    pub gateway_guid: [u8; 16],
51    /// PKCS#8-DER-formatierter Privatkey.
52    pub signing_key: Vec<u8>,
53    /// Signatur-Algorithmus.
54    pub algorithm: SignatureAlgorithm,
55}
56
57/// Fehler aus Gateway-Bridge-Operationen.
58#[derive(Debug, Clone, PartialEq, Eq)]
59#[non_exhaustive]
60pub enum GatewayBridgeError {
61    /// Edge ist nicht registriert.
62    UnknownEdge {
63        /// 16-byte Edge-GUID.
64        edge_guid: [u8; 16],
65    },
66    /// Sign-Operation fehlgeschlagen (delegiert aus PKI-Crate).
67    DelegationFailed(DelegationError),
68}
69
70impl core::fmt::Display for GatewayBridgeError {
71    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
72        match self {
73            Self::UnknownEdge { edge_guid } => {
74                write!(f, "no active delegation for edge {edge_guid:?}")
75            }
76            Self::DelegationFailed(e) => write!(f, "delegation failed: {e}"),
77        }
78    }
79}
80
81#[cfg(feature = "std")]
82impl std::error::Error for GatewayBridgeError {}
83
84impl From<DelegationError> for GatewayBridgeError {
85    fn from(e: DelegationError) -> Self {
86        Self::DelegationFailed(e)
87    }
88}
89
90/// Result-Alias.
91pub type GatewayBridgeResult<T> = Result<T, GatewayBridgeError>;
92
93/// Gateway-Bridge-Helper.
94///
95/// Lifecycle:
96/// 1. [`GatewayBridge::new`] mit `GatewayBridgeConfig`.
97/// 2. Optional [`GatewayBridge::with_upstream`] um den eigenen Bridge
98///    in eine bestehende Chain (z.B. von Wanne-GW zum eigenen Turm-GW)
99///    einzuhaengen.
100/// 3. Pro Edge: [`GatewayBridge::delegate_for`] um eine neue Delegation
101///    auszustellen.
102/// 4. [`GatewayBridge::chain_for`] reicht die Chain als Output fuer
103///    SPDP/SEDP-Properties.
104/// 5. [`GatewayBridge::revoke_delegation`] entfernt einen Edge.
105#[derive(Debug, Clone)]
106pub struct GatewayBridge {
107    config: GatewayBridgeConfig,
108    /// Optional: Chain, die diesen Gateway als Delegatee einer hoeheren
109    /// Ebene legitimiert. Wird in [`Self::chain_for`] dem ausgestellten
110    /// Edge-Link voraus-geschoben.
111    upstream: Option<DelegationChain>,
112    /// Aktive Edge-Delegations (`edge_guid → Link`).
113    active: BTreeMap<[u8; 16], DelegationLink>,
114    /// Revocation-Liste: Edge-GUIDs, deren Delegation in der naechsten
115    /// SPDP-Welle als revoked annonciert werden soll. Caller leert
116    /// die Liste nach erfolgreichem Announce via
117    /// [`Self::take_revocations`].
118    revocations: Vec<[u8; 16]>,
119}
120
121impl GatewayBridge {
122    /// Konstruktor.
123    #[must_use]
124    pub fn new(config: GatewayBridgeConfig) -> Self {
125        Self {
126            config,
127            upstream: None,
128            active: BTreeMap::new(),
129            revocations: Vec::new(),
130        }
131    }
132
133    /// Setzt eine Upstream-Chain. Diese wird in [`Self::chain_for`]
134    /// vor den Edge-Link gehaengt — Sub-Gateway-Chaining fuer
135    /// Doppelstern (Turm-GW unter Wanne-GW).
136    ///
137    /// Validation der Upstream-Chain ist NICHT Teil des Bridge —
138    /// Caller muss vorher selbst `validate_chain` aufrufen, um
139    /// Mismatch-Profile-Fehlern vorzubeugen.
140    pub fn with_upstream(&mut self, upstream_chain: DelegationChain) {
141        self.upstream = Some(upstream_chain);
142    }
143
144    /// 16-byte Gateway-GUID (Read-only).
145    #[must_use]
146    pub fn gateway_guid(&self) -> [u8; 16] {
147        self.config.gateway_guid
148    }
149
150    /// Stellt eine neue Delegation fuer einen Edge-Peer aus. Wenn der
151    /// Edge bereits delegiert war, wird der alte Link ueberschrieben
152    /// (typisch bei Ephemeral-Edge-Rotation, Plan §Stufe j-f).
153    ///
154    /// `not_before` und `not_after` sind absolute Unix-Sekunden;
155    /// `topic_patterns`/`partition_patterns` sind die Glob-Whitelist,
156    /// die der Edge im engsten Scope haben darf.
157    ///
158    /// # Errors
159    /// [`GatewayBridgeError::DelegationFailed`] wenn der PKI-Sign-
160    /// Schritt fehlschlaegt (Cap-Verletzung, Key-Parse-Fehler).
161    pub fn delegate_for(
162        &mut self,
163        edge_guid: [u8; 16],
164        topic_patterns: Vec<String>,
165        partition_patterns: Vec<String>,
166        not_before: i64,
167        not_after: i64,
168    ) -> GatewayBridgeResult<&DelegationLink> {
169        let mut link = DelegationLink::new(
170            self.config.gateway_guid,
171            edge_guid,
172            topic_patterns,
173            partition_patterns,
174            not_before,
175            not_after,
176            self.config.algorithm,
177        )?;
178        link.sign(&self.config.signing_key)?;
179        self.active.insert(edge_guid, link);
180        // Edge wieder aktiv → ggf. aus Revocations entfernen, falls
181        // Re-Issue (z.B. nach Renewal).
182        self.revocations.retain(|g| g != &edge_guid);
183        // Lookup nach erfolgreichem insert kann nicht fehlschlagen,
184        // aber clippy::expect_used verbietet expect — wir liefern
185        // unwrap_or via einen frischen Lookup.
186        self.active
187            .get(&edge_guid)
188            .ok_or(GatewayBridgeError::UnknownEdge { edge_guid })
189    }
190
191    /// Widerruft die aktive Delegation fuer einen Edge. Der Edge wird
192    /// in die Revocation-Liste aufgenommen und kann ueber
193    /// [`Self::take_revocations`] dem Discovery-Layer mitgeteilt werden.
194    ///
195    /// # Errors
196    /// [`GatewayBridgeError::UnknownEdge`] wenn der Edge nicht aktiv ist.
197    pub fn revoke_delegation(&mut self, edge_guid: [u8; 16]) -> GatewayBridgeResult<()> {
198        if self.active.remove(&edge_guid).is_some() {
199            if !self.revocations.contains(&edge_guid) {
200                self.revocations.push(edge_guid);
201            }
202            Ok(())
203        } else {
204            Err(GatewayBridgeError::UnknownEdge { edge_guid })
205        }
206    }
207
208    /// Liefert die ausgehende Chain fuer einen Edge.
209    ///
210    /// 1-Hop-Bridge (kein Upstream): Chain = `[Edge-Link]`,
211    ///   `origin_guid = gateway_guid`.
212    /// n-Hop-Bridge (mit Upstream): Chain = `upstream.links ++
213    ///   [Edge-Link]`, `origin_guid = upstream.origin_guid`.
214    ///
215    /// Returns `None` wenn der Edge nicht aktiv ist.
216    #[must_use]
217    pub fn chain_for(&self, edge_guid: &[u8; 16]) -> Option<DelegationChain> {
218        let edge_link = self.active.get(edge_guid)?.clone();
219        match &self.upstream {
220            None => DelegationChain::new(self.config.gateway_guid, alloc::vec![edge_link]).ok(),
221            Some(up) => {
222                let mut links = up.links.clone();
223                links.push(edge_link);
224                DelegationChain::new(up.origin_guid, links).ok()
225            }
226        }
227    }
228
229    /// Anzahl aktiver Edge-Delegations.
230    #[must_use]
231    pub fn active_count(&self) -> usize {
232        self.active.len()
233    }
234
235    /// True wenn ein Edge aktiv delegiert ist.
236    #[must_use]
237    pub fn has_edge(&self, edge_guid: &[u8; 16]) -> bool {
238        self.active.contains_key(edge_guid)
239    }
240
241    /// Iteriert ueber alle aktiven Edge-Delegations.
242    pub fn iter_active(&self) -> impl Iterator<Item = (&[u8; 16], &DelegationLink)> {
243        self.active.iter()
244    }
245
246    /// Liest und leert die Revocation-Liste (Discovery-Layer ruft das
247    /// pro SPDP-Beacon-Tick auf).
248    pub fn take_revocations(&mut self) -> Vec<[u8; 16]> {
249        core::mem::take(&mut self.revocations)
250    }
251
252    /// Lese-Zugriff auf den Upstream-Chain (nuetzlich fuer Logging /
253    /// Metrics).
254    #[must_use]
255    pub fn upstream(&self) -> Option<&DelegationChain> {
256        self.upstream.as_ref()
257    }
258
259    /// Rotiert Ephemeral-Edge-Identities deren Lifetime abgelaufen ist.
260    ///
261    /// Workflow pro Ephemeral-Edge:
262    /// 1. Wenn Edge nicht aktiv → ueberspringen (init kommt via
263    ///    `delegate_for` durch den Caller).
264    /// 2. Wenn `now < link.not_after - lifetime/N` (N=Renewal-Window)
265    ///    → noch zu frisch, ueberspringen.
266    /// 3. Sonst: neue GuidPrefix ziehen (`prefix_generator(name)`),
267    ///    alten Edge revoken, neuen `delegate_for` mit `now`-basierten
268    ///    Zeitfenster ausstellen.
269    ///
270    /// `prefix_generator` ist ein Pluggable-Hook (z.B. ChaCha20-RNG
271    /// oder system-RNG); der Bridge ist deterministic-testbar weil
272    /// die Zufalls-Quelle vom Caller kommt.
273    ///
274    /// Returns Liste der rotierten Edge-Namen.
275    ///
276    /// # Errors
277    /// Propagiert [`GatewayBridgeError::DelegationFailed`] wenn ein
278    /// neu-Sign fehlschlaegt — bricht aber NICHT die Loop-Schleife ab,
279    /// fehlerhafte Edges werden im `Err`-Tail-Vec gesammelt und der
280    /// Aufrufer kann entscheiden.
281    pub fn rotate_ephemerals<F>(
282        &mut self,
283        identities: &[EdgeIdentityConfig],
284        now: i64,
285        topic_patterns: Vec<String>,
286        partition_patterns: Vec<String>,
287        mut prefix_generator: F,
288    ) -> (Vec<String>, Vec<(String, GatewayBridgeError)>)
289    where
290        F: FnMut(&str) -> [u8; 12],
291    {
292        let mut rotated = Vec::new();
293        let mut failed = Vec::new();
294        for cfg in identities.iter().filter(|c| c.is_ephemeral()) {
295            // Edge-GUID = 12-byte Prefix + 4-byte EntityId 0x00.0x00.0x01.0xC1 (DDS-konv).
296            // Wir nehmen hier nur den Prefix als Identifier-Schluessel —
297            // GuidPrefix selbst entscheidet die Edge-Identity.
298            let new_prefix = prefix_generator(&cfg.name);
299            // Reconstruct full 16-byte edge guid (Prefix+EntityId der
300            // Default-Participant-EntityId).
301            let mut edge_guid = [0u8; 16];
302            edge_guid[..12].copy_from_slice(&new_prefix);
303            edge_guid[12..].copy_from_slice(&[0x00, 0x00, 0x01, 0xC1]);
304
305            // Alle vorhandenen Edges mit gleichem Namen aufgreifen
306            // (matching ist hier prefix-basiert; in voller Impl haetten
307            // wir eine name-keyed Map). Da wir den name nicht im
308            // active-Map haben, gehen wir hier einfach raw vor:
309            // delegate_for ueberschreibt bei Konflikt.
310            let lifetime = i64::from(cfg.effective_lifetime());
311            let new_not_after = now.saturating_add(lifetime);
312            match self.delegate_for(
313                edge_guid,
314                topic_patterns.clone(),
315                partition_patterns.clone(),
316                now,
317                new_not_after,
318            ) {
319                Ok(_) => rotated.push(cfg.name.clone()),
320                Err(e) => failed.push((cfg.name.clone(), e)),
321            }
322        }
323        (rotated, failed)
324    }
325}
326
327#[cfg(test)]
328#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
329mod tests {
330    use super::*;
331    use alloc::string::ToString;
332    use ring::rand::SystemRandom;
333    use ring::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, KeyPair};
334    use zerodds_security_permissions::EdgeIdentityMode;
335
336    fn ecdsa_p256_keypair() -> (Vec<u8>, Vec<u8>) {
337        let rng = SystemRandom::new();
338        let pkcs8 =
339            EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &rng).expect("gen");
340        let sk = pkcs8.as_ref().to_vec();
341        let kp = EcdsaKeyPair::from_pkcs8(&ECDSA_P256_SHA256_FIXED_SIGNING, &sk, &rng).expect("p");
342        (sk, kp.public_key().as_ref().to_vec())
343    }
344
345    fn make_bridge(gw_guid: [u8; 16]) -> (GatewayBridge, Vec<u8>) {
346        let (sk, pk) = ecdsa_p256_keypair();
347        let cfg = GatewayBridgeConfig {
348            gateway_guid: gw_guid,
349            signing_key: sk,
350            algorithm: SignatureAlgorithm::EcdsaP256,
351        };
352        (GatewayBridge::new(cfg), pk)
353    }
354
355    #[test]
356    fn delegate_for_creates_signed_link() {
357        let gw = [0xAA; 16];
358        let edge = [0xBB; 16];
359        let (mut bridge, pk) = make_bridge(gw);
360        let link = bridge
361            .delegate_for(
362                edge,
363                alloc::vec!["sensor/*".to_string()],
364                alloc::vec![],
365                1_000,
366                9_000,
367            )
368            .expect("delegate")
369            .clone();
370        assert_eq!(link.delegator_guid, gw);
371        assert_eq!(link.delegatee_guid, edge);
372        assert_eq!(link.signature.len(), 64); // ECDSA-P256 fixed
373        link.verify(&pk).expect("verify");
374        assert_eq!(bridge.active_count(), 1);
375        assert!(bridge.has_edge(&edge));
376    }
377
378    #[test]
379    fn one_hop_chain_for_edge() {
380        let gw = [0xAA; 16];
381        let edge = [0xBB; 16];
382        let (mut bridge, _pk) = make_bridge(gw);
383        bridge
384            .delegate_for(
385                edge,
386                alloc::vec!["sensor/*".to_string()],
387                alloc::vec![],
388                0,
389                9_000,
390            )
391            .expect("delegate");
392        let chain = bridge.chain_for(&edge).expect("chain");
393        assert_eq!(chain.depth(), 1);
394        assert_eq!(chain.origin_guid, gw);
395        assert_eq!(chain.edge_guid(), Some(edge));
396    }
397
398    #[test]
399    fn chain_for_missing_edge_is_none() {
400        let gw = [0xAA; 16];
401        let (bridge, _) = make_bridge(gw);
402        assert!(bridge.chain_for(&[0xCC; 16]).is_none());
403    }
404
405    #[test]
406    fn revoke_delegation_removes_active_and_records_revocation() {
407        let gw = [0xAA; 16];
408        let edge = [0xBB; 16];
409        let (mut bridge, _pk) = make_bridge(gw);
410        bridge
411            .delegate_for(edge, alloc::vec![], alloc::vec![], 0, 9_000)
412            .expect("delegate");
413        bridge.revoke_delegation(edge).expect("revoke");
414        assert_eq!(bridge.active_count(), 0);
415        assert!(!bridge.has_edge(&edge));
416        let revocations = bridge.take_revocations();
417        assert_eq!(revocations, alloc::vec![edge]);
418        // Liste ist nach take leer.
419        assert!(bridge.take_revocations().is_empty());
420    }
421
422    #[test]
423    fn revoke_unknown_edge_is_error() {
424        let gw = [0xAA; 16];
425        let (mut bridge, _) = make_bridge(gw);
426        let err = bridge.revoke_delegation([0xFF; 16]).expect_err("must fail");
427        assert!(matches!(err, GatewayBridgeError::UnknownEdge { .. }));
428    }
429
430    #[test]
431    fn re_delegate_clears_pending_revocation() {
432        let gw = [0xAA; 16];
433        let edge = [0xBB; 16];
434        let (mut bridge, _) = make_bridge(gw);
435        bridge
436            .delegate_for(edge, alloc::vec![], alloc::vec![], 0, 9_000)
437            .expect("delegate");
438        bridge.revoke_delegation(edge).expect("revoke");
439        // Re-Delegate (z.B. nach Cert-Renewal) muss Revocation entfernen.
440        bridge
441            .delegate_for(edge, alloc::vec![], alloc::vec![], 100, 10_000)
442            .expect("redelegate");
443        assert!(bridge.take_revocations().is_empty());
444        assert!(bridge.has_edge(&edge));
445    }
446
447    #[test]
448    fn sub_gateway_chaining_two_hops() {
449        // Wanne-GW (gw1) delegates an Turm-GW (gw2). Turm-GW bridges
450        // edge `turm-imu`. Resulting chain has 2 links und origin = gw1.
451        let gw1 = [0x11; 16];
452        let gw2 = [0x22; 16];
453        let edge = [0x33; 16];
454
455        // gw1 erzeugt einen Upstream-Link gw1 -> gw2.
456        let (sk1, _pk1) = ecdsa_p256_keypair();
457        let mut upstream_link = DelegationLink::new(
458            gw1,
459            gw2,
460            alloc::vec!["*".to_string()],
461            alloc::vec![],
462            0,
463            9_000,
464            SignatureAlgorithm::EcdsaP256,
465        )
466        .expect("upstream link");
467        upstream_link.sign(&sk1).expect("sign upstream");
468        let upstream_chain =
469            DelegationChain::new(gw1, alloc::vec![upstream_link]).expect("upstream chain");
470
471        // Turm-Bridge.
472        let (mut turm_bridge, _pk2) = make_bridge(gw2);
473        turm_bridge.with_upstream(upstream_chain);
474
475        turm_bridge
476            .delegate_for(
477                edge,
478                alloc::vec!["sensor/imu".to_string()],
479                alloc::vec![],
480                100,
481                8_000,
482            )
483            .expect("turm delegate");
484        let chain = turm_bridge.chain_for(&edge).expect("chain");
485        assert_eq!(chain.depth(), 2);
486        assert_eq!(chain.origin_guid, gw1);
487        assert_eq!(chain.edge_guid(), Some(edge));
488        // Letzter Link ist gw2 -> edge.
489        assert_eq!(chain.links.last().unwrap().delegator_guid, gw2);
490        assert_eq!(chain.links.last().unwrap().delegatee_guid, edge);
491    }
492
493    #[test]
494    fn iter_active_lists_all_delegations() {
495        let gw = [0xAA; 16];
496        let (mut bridge, _) = make_bridge(gw);
497        for i in 0..5u8 {
498            let mut edge = [0u8; 16];
499            edge[0] = i;
500            bridge
501                .delegate_for(edge, alloc::vec![], alloc::vec![], 0, 9_000)
502                .expect("delegate");
503        }
504        assert_eq!(bridge.active_count(), 5);
505        let collected: Vec<[u8; 16]> = bridge.iter_active().map(|(g, _)| *g).collect();
506        assert_eq!(collected.len(), 5);
507    }
508
509    #[test]
510    fn upstream_accessor_reflects_state() {
511        let gw = [0xAA; 16];
512        let (mut bridge, _) = make_bridge(gw);
513        assert!(bridge.upstream().is_none());
514        let (sk, _pk) = ecdsa_p256_keypair();
515        let mut up_link = DelegationLink::new(
516            [0x11; 16],
517            gw,
518            alloc::vec!["*".to_string()],
519            alloc::vec![],
520            0,
521            9_000,
522            SignatureAlgorithm::EcdsaP256,
523        )
524        .unwrap();
525        up_link.sign(&sk).unwrap();
526        let chain = DelegationChain::new([0x11; 16], alloc::vec![up_link]).unwrap();
527        bridge.with_upstream(chain.clone());
528        assert_eq!(bridge.upstream(), Some(&chain));
529    }
530
531    #[test]
532    fn bridge_two_hop_chain_validates_via_chain_check() {
533        use alloc::collections::BTreeSet;
534        use zerodds_security_permissions::{
535            DelegationProfile, TrustAnchor, TrustPolicy, validate_chain,
536        };
537
538        // Setup: gw1 (Wanne), gw2 (Turm), edge.
539        let gw1 = [0x11; 16];
540        let gw2 = [0x22; 16];
541        let edge = [0x33; 16];
542
543        // Wanne-GW Schluessel-Pair.
544        let (sk1, pk1) = ecdsa_p256_keypair();
545        // Turm-GW Schluessel-Pair (anderes Pair!).
546        let (sk2, pk2) = ecdsa_p256_keypair();
547
548        // Wanne erzeugt Upstream-Link gw1 -> gw2 (signiert mit sk1).
549        let mut upstream_link = DelegationLink::new(
550            gw1,
551            gw2,
552            alloc::vec!["*".to_string()],
553            alloc::vec![],
554            0,
555            9_000,
556            SignatureAlgorithm::EcdsaP256,
557        )
558        .unwrap();
559        upstream_link.sign(&sk1).unwrap();
560        let upstream = DelegationChain::new(gw1, alloc::vec![upstream_link]).unwrap();
561
562        // Turm-Bridge (signiert mit sk2).
563        let cfg = GatewayBridgeConfig {
564            gateway_guid: gw2,
565            signing_key: sk2,
566            algorithm: SignatureAlgorithm::EcdsaP256,
567        };
568        let mut turm_bridge = GatewayBridge::new(cfg);
569        turm_bridge.with_upstream(upstream);
570        turm_bridge
571            .delegate_for(
572                edge,
573                alloc::vec!["sensor/imu".to_string()],
574                alloc::vec![],
575                100,
576                8_000,
577            )
578            .unwrap();
579
580        let chain = turm_bridge.chain_for(&edge).expect("chain");
581
582        // Profile mit Trust-Anchor = gw1 (pk1).
583        let mut algos = BTreeSet::new();
584        algos.insert(SignatureAlgorithm::EcdsaP256.wire_id());
585        let profile = DelegationProfile {
586            name: "vehicle".to_string(),
587            trust_policy: TrustPolicy::DirectOrDelegated,
588            trust_anchors: alloc::vec![TrustAnchor {
589                subject_guid: gw1,
590                verify_public_key: pk1,
591                algorithm: SignatureAlgorithm::EcdsaP256,
592            }],
593            max_chain_depth: 3,
594            allowed_algorithms: algos,
595            require_ocsp: false,
596        };
597
598        // Resolver liefert pk2 fuer gw2.
599        let resolver = move |g: &[u8; 16]| -> Option<(Vec<u8>, SignatureAlgorithm)> {
600            if g == &gw2 {
601                Some((pk2.clone(), SignatureAlgorithm::EcdsaP256))
602            } else {
603                None
604            }
605        };
606
607        let validated = validate_chain(&chain, &profile, 5_000, resolver).expect("validate");
608        assert_eq!(validated.chain_depth, 2);
609        assert_eq!(validated.edge_guid, edge);
610        // Scope-Intersection: "*" und "sensor/imu" → "sensor/imu".
611        assert!(
612            validated
613                .effective_topic_patterns
614                .contains(&"sensor/imu".to_string())
615        );
616    }
617
618    // ---- RC1: rotate_ephemerals ----
619
620    #[test]
621    fn rotate_ephemerals_creates_delegations_for_ephemeral_only() {
622        let gw = [0xAA; 16];
623        let (mut bridge, _pk) = make_bridge(gw);
624
625        let identities = alloc::vec![
626            EdgeIdentityConfig {
627                name: "static-edge".into(),
628                mode: EdgeIdentityMode::Static,
629                guid_prefix: Some([0x01; 12]),
630                lifetime_seconds: None,
631            },
632            EdgeIdentityConfig {
633                name: "ephemeral-edge".into(),
634                mode: EdgeIdentityMode::Ephemeral,
635                guid_prefix: None,
636                lifetime_seconds: Some(60),
637            },
638        ];
639
640        let mut counter = 0u8;
641        let prefix_gen = |_name: &str| -> [u8; 12] {
642            counter += 1;
643            [counter; 12]
644        };
645        let (rotated, failed) = bridge.rotate_ephemerals(
646            &identities,
647            1_000,
648            alloc::vec!["sensor/*".to_string()],
649            alloc::vec![],
650            prefix_gen,
651        );
652        assert_eq!(rotated, alloc::vec!["ephemeral-edge".to_string()]);
653        assert!(failed.is_empty());
654        // Static-Edge wurde NICHT delegiert (Caller managed Static
655        // selbst).
656        assert_eq!(bridge.active_count(), 1);
657    }
658
659    #[test]
660    fn rotate_ephemerals_uses_provided_prefix_generator() {
661        let gw = [0xAA; 16];
662        let (mut bridge, _) = make_bridge(gw);
663
664        let identities = alloc::vec![EdgeIdentityConfig {
665            name: "rot-edge".into(),
666            mode: EdgeIdentityMode::Ephemeral,
667            guid_prefix: None,
668            lifetime_seconds: Some(120),
669        }];
670
671        let captured_name: alloc::sync::Arc<core::sync::atomic::AtomicBool> =
672            alloc::sync::Arc::new(core::sync::atomic::AtomicBool::new(false));
673        let captured_clone = captured_name.clone();
674        let prefix_gen = move |name: &str| -> [u8; 12] {
675            if name == "rot-edge" {
676                captured_clone.store(true, core::sync::atomic::Ordering::SeqCst);
677            }
678            [0xDE; 12]
679        };
680
681        let (rotated, _) =
682            bridge.rotate_ephemerals(&identities, 5_000, alloc::vec![], alloc::vec![], prefix_gen);
683        assert_eq!(rotated.len(), 1);
684        assert!(captured_name.load(core::sync::atomic::Ordering::SeqCst));
685
686        // Edge ist mit dem generierten Prefix aktiv.
687        let mut expected_guid = [0u8; 16];
688        expected_guid[..12].copy_from_slice(&[0xDE; 12]);
689        expected_guid[12..].copy_from_slice(&[0x00, 0x00, 0x01, 0xC1]);
690        assert!(bridge.has_edge(&expected_guid));
691    }
692
693    #[test]
694    fn rotate_ephemerals_uses_lifetime_for_not_after() {
695        let gw = [0xAA; 16];
696        let (mut bridge, _) = make_bridge(gw);
697        let identities = alloc::vec![EdgeIdentityConfig {
698            name: "imu".into(),
699            mode: EdgeIdentityMode::Ephemeral,
700            guid_prefix: None,
701            lifetime_seconds: Some(300),
702        }];
703        let (_rotated, _) =
704            bridge.rotate_ephemerals(&identities, 1_000, alloc::vec![], alloc::vec![], |_| {
705                [0xAB; 12]
706            });
707        let mut edge_guid = [0u8; 16];
708        edge_guid[..12].copy_from_slice(&[0xAB; 12]);
709        edge_guid[12..].copy_from_slice(&[0x00, 0x00, 0x01, 0xC1]);
710        let link = bridge.iter_active().find(|(g, _)| *g == &edge_guid);
711        assert!(link.is_some());
712        let (_g, l) = link.unwrap();
713        assert_eq!(l.not_before, 1_000);
714        assert_eq!(l.not_after, 1_300);
715    }
716
717    #[test]
718    fn rotate_ephemerals_repeated_calls_replace_old_delegation() {
719        let gw = [0xAA; 16];
720        let (mut bridge, _) = make_bridge(gw);
721        let identities = alloc::vec![EdgeIdentityConfig {
722            name: "ecu".into(),
723            mode: EdgeIdentityMode::Ephemeral,
724            guid_prefix: None,
725            lifetime_seconds: Some(60),
726        }];
727
728        // Erste Rotation mit Prefix [0x11; 12].
729        let (_, _) =
730            bridge.rotate_ephemerals(&identities, 1_000, alloc::vec![], alloc::vec![], |_| {
731                [0x11; 12]
732            });
733        // Zweite Rotation mit Prefix [0x22; 12] — alter Edge bleibt
734        // im active map (delegate_for ueberschreibt nur exact gleiche
735        // GUID). Beide sind aktiv. In Production wuerde der
736        // Discovery-Layer den alten via take_revocations evicten.
737        let (_, _) =
738            bridge.rotate_ephemerals(&identities, 2_000, alloc::vec![], alloc::vec![], |_| {
739                [0x22; 12]
740            });
741        assert_eq!(bridge.active_count(), 2);
742    }
743
744    #[test]
745    fn delegation_link_too_many_topics_propagates_as_bridge_error() {
746        let gw = [0xAA; 16];
747        let edge = [0xBB; 16];
748        let (mut bridge, _) = make_bridge(gw);
749        let topics: Vec<String> = (0..200).map(|i| alloc::format!("t{i}")).collect();
750        let err = bridge
751            .delegate_for(edge, topics, alloc::vec![], 0, 9_000)
752            .expect_err("must fail");
753        assert!(matches!(err, GatewayBridgeError::DelegationFailed(_)));
754    }
755}