#![cfg(all(target_os = "linux", feature = "security"))]
#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::type_complexity
)]
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use zerodds_dcps::runtime::{
DcpsRuntime, RuntimeConfig, UserReaderConfig, UserSample, UserWriterConfig,
};
use zerodds_rtps::wire_types::{EntityId, Guid, GuidPrefix, VendorId};
use zerodds_security::authentication::{
AuthenticationPlugin, SharedSecretHandle, SharedSecretProvider,
};
use zerodds_security_crypto::{AesGcmCryptoPlugin, Suite};
use zerodds_security_pki::{IdentityConfig, PkiAuthenticationPlugin};
use zerodds_security_runtime::SharedSecurityGate;
const GOV: &str = r#"<domain_access_rules><domain_rule><domains><id>0</id></domains><topic_access_rules><topic_rule><topic_expression>*</topic_expression></topic_rule></topic_access_rules></domain_rule></domain_access_rules>"#;
const GOV_DATA_ENCRYPT: &str = r#"<domain_access_rules><domain_rule><domains><id>0</id></domains><topic_access_rules><topic_rule><topic_expression>*</topic_expression><data_protection_kind>ENCRYPT</data_protection_kind></topic_rule></topic_access_rules></domain_rule></domain_access_rules>"#;
struct PkiProvider(Arc<Mutex<PkiAuthenticationPlugin>>);
impl SharedSecretProvider for PkiProvider {
fn get_shared_secret(&self, h: SharedSecretHandle) -> Option<Vec<u8>> {
self.0.lock().ok()?.get_shared_secret(h)
}
}
fn mint_foreign() -> (Vec<u8>, Vec<u8>) {
use rcgen::{CertificateParams, KeyPair};
let mut ca_p = CertificateParams::new(vec!["Foreign CA".to_string()]).unwrap();
ca_p.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let ca_key = KeyPair::generate().unwrap();
let ca_cert = ca_p.self_signed(&ca_key).unwrap();
let mut p = CertificateParams::new(vec!["eve-foreign".to_string()]).unwrap();
p.is_ca = rcgen::IsCa::NoCa;
let k = KeyPair::generate().unwrap();
let c = p.signed_by(&k, &ca_cert, &ca_key).unwrap();
(
[
c.pem().into_bytes(),
b"\n".to_vec(),
ca_cert.pem().into_bytes(),
]
.concat(),
k.serialize_pem().into_bytes(),
)
}
fn mint_ca_two() -> ((Vec<u8>, Vec<u8>), (Vec<u8>, Vec<u8>)) {
use rcgen::{CertificateParams, KeyPair};
let mut ca_p = CertificateParams::new(vec!["FU2 Live CA".to_string()]).unwrap();
ca_p.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
let ca_key = KeyPair::generate().unwrap();
let ca_cert = ca_p.self_signed(&ca_key).unwrap();
let ca_pem = ca_cert.pem().into_bytes();
let mint = |name: &str| -> (Vec<u8>, Vec<u8>) {
let mut p = CertificateParams::new(vec![name.to_string()]).unwrap();
p.is_ca = rcgen::IsCa::NoCa;
let k = KeyPair::generate().unwrap();
let c = p.signed_by(&k, &ca_cert, &ca_key).unwrap();
(
[c.pem().into_bytes(), b"\n".to_vec(), ca_pem.clone()].concat(),
k.serialize_pem().into_bytes(),
)
};
(mint("alice-live"), mint("bob-live"))
}
fn gate_for(pki: &Arc<Mutex<PkiAuthenticationPlugin>>, gov: &str) -> Arc<SharedSecurityGate> {
Arc::new(SharedSecurityGate::new(
0,
zerodds_security_permissions::parse_governance_xml(gov).unwrap(),
Box::new(AesGcmCryptoPlugin::with_secret_provider(
Suite::Aes128Gcm,
Arc::new(PkiProvider(pki.clone())) as Arc<dyn SharedSecretProvider>,
)),
))
}
fn start_secured(
cert: Vec<u8>,
key: Vec<u8>,
guid: [u8; 16],
prefix: GuidPrefix,
gov: &str,
) -> (Arc<DcpsRuntime>, Arc<SharedSecurityGate>) {
let pki = Arc::new(Mutex::new(PkiAuthenticationPlugin::new()));
let local = pki
.lock()
.unwrap()
.validate_with_config(
IdentityConfig {
identity_cert_pem: cert.clone(),
identity_ca_pem: cert,
identity_key_pem: Some(key),
},
guid,
)
.unwrap();
let gate = gate_for(&pki, gov);
let rt = DcpsRuntime::start(
0,
prefix,
RuntimeConfig {
security: Some(gate.clone()),
..RuntimeConfig::default()
},
)
.expect("start");
let auth: Arc<Mutex<dyn AuthenticationPlugin>> = pki.clone();
rt.enable_security_builtins_with_auth(VendorId::ZERODDS, auth, local);
(rt, gate)
}
fn wait_handshake(
gate_a: &Arc<SharedSecurityGate>,
gate_b: &Arc<SharedSecurityGate>,
a_key: [u8; 12],
b_key: [u8; 12],
timeout: Duration,
) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
if gate_a.slot_for(&b_key).ok().flatten().is_some()
&& gate_b.slot_for(&a_key).ok().flatten().is_some()
{
return true;
}
std::thread::sleep(Duration::from_millis(100));
}
false
}
fn writer_cfg(topic: &str) -> UserWriterConfig {
UserWriterConfig {
topic_name: topic.into(),
type_name: "RawBytes".into(),
reliable: true,
durability: zerodds_qos::DurabilityKind::Volatile,
deadline: Default::default(),
lifespan: Default::default(),
liveliness: Default::default(),
ownership: zerodds_qos::OwnershipKind::Shared,
ownership_strength: 0,
partition: vec![],
user_data: vec![],
topic_data: vec![],
group_data: vec![],
type_identifier: zerodds_types::TypeIdentifier::None,
data_representation_offer: None,
}
}
fn reader_cfg(topic: &str) -> UserReaderConfig {
UserReaderConfig {
topic_name: topic.into(),
type_name: "RawBytes".into(),
reliable: true,
durability: zerodds_qos::DurabilityKind::Volatile,
deadline: Default::default(),
liveliness: Default::default(),
ownership: zerodds_qos::OwnershipKind::Shared,
partition: vec![],
user_data: vec![],
topic_data: vec![],
group_data: vec![],
type_identifier: zerodds_types::TypeIdentifier::None,
type_consistency: zerodds_types::qos::TypeConsistencyEnforcement::default(),
data_representation_offer: None,
}
}
#[serial_test::serial(security_live)]
#[test]
fn secured_handshake_completes_live_over_udp() {
let ((a_cert, a_key), (b_cert, b_key)) = mint_ca_two();
let a_prefix = GuidPrefix::from_bytes([0x2A; 12]);
let b_prefix = GuidPrefix::from_bytes([0x1B; 12]);
let a_guid = Guid::new(a_prefix, EntityId::PARTICIPANT).to_bytes();
let b_guid = Guid::new(b_prefix, EntityId::PARTICIPANT).to_bytes();
let (rt_a, gate_a) = start_secured(a_cert, a_key, a_guid, a_prefix, GOV);
let (rt_b, gate_b) = start_secured(b_cert, b_key, b_guid, b_prefix, GOV);
assert!(
wait_handshake(
&gate_a,
&gate_b,
a_prefix.to_bytes(),
b_prefix.to_bytes(),
Duration::from_secs(30),
),
"live handshake: both gates must register the peer (a_disc={})",
rt_a.discovered_participants().len(),
);
rt_a.shutdown();
rt_b.shutdown();
}
#[serial_test::serial(security_live)]
#[test]
fn secured_user_data_round_trips_live_over_udp() {
let ((a_cert, a_key), (b_cert, b_key)) = mint_ca_two();
let a_prefix = GuidPrefix::from_bytes([0x3C; 12]);
let b_prefix = GuidPrefix::from_bytes([0x4D; 12]);
let a_guid = Guid::new(a_prefix, EntityId::PARTICIPANT).to_bytes();
let b_guid = Guid::new(b_prefix, EntityId::PARTICIPANT).to_bytes();
let (rt_a, gate_a) = start_secured(a_cert, a_key, a_guid, a_prefix, GOV_DATA_ENCRYPT);
let (rt_b, gate_b) = start_secured(b_cert, b_key, b_guid, b_prefix, GOV_DATA_ENCRYPT);
assert!(
wait_handshake(
&gate_a,
&gate_b,
a_prefix.to_bytes(),
b_prefix.to_bytes(),
Duration::from_secs(30),
),
"secured user-data: handshake not finished"
);
let writer_eid = rt_a
.register_user_writer(writer_cfg("SecuredData"))
.unwrap();
let (reader_eid, rx) = rt_b
.register_user_reader(reader_cfg("SecuredData"))
.unwrap();
let mut matched = false;
let deadline = Instant::now() + Duration::from_secs(15);
while Instant::now() < deadline {
if rt_b.user_reader_matched_count(reader_eid) >= 1
&& rt_a.user_writer_matched_count(writer_eid) >= 1
{
matched = true;
break;
}
std::thread::sleep(Duration::from_millis(50));
}
assert!(matched, "SEDP match (secured) did not come about");
let payload = b"secured-user-payload-0xC0FFEE".to_vec();
let mut got = None;
for _ in 0..20 {
rt_a.write_user_sample(writer_eid, payload.clone()).unwrap();
if let Ok(s) = rx.recv_timeout(Duration::from_millis(500)) {
got = Some(s);
break;
}
}
match got.expect("B must receive the secured sample") {
UserSample::Alive { payload: bytes, .. } => {
assert_eq!(bytes.as_slice(), payload.as_slice())
}
other => panic!("expected Alive, got {:?}", std::mem::discriminant(&other)),
}
rt_a.shutdown();
rt_b.shutdown();
}
#[serial_test::serial(security_live)]
#[test]
fn secured_user_data_unreadable_by_unauthenticated_peer_live() {
let ((a_cert, a_key), (b_cert, b_key)) = mint_ca_two();
let (c_cert, c_key) = mint_foreign();
let a_prefix = GuidPrefix::from_bytes([0x5E; 12]);
let b_prefix = GuidPrefix::from_bytes([0x6F; 12]);
let c_prefix = GuidPrefix::from_bytes([0x7A; 12]);
let a_guid = Guid::new(a_prefix, EntityId::PARTICIPANT).to_bytes();
let b_guid = Guid::new(b_prefix, EntityId::PARTICIPANT).to_bytes();
let c_guid = Guid::new(c_prefix, EntityId::PARTICIPANT).to_bytes();
let (rt_a, gate_a) = start_secured(a_cert, a_key, a_guid, a_prefix, GOV_DATA_ENCRYPT);
let (rt_b, gate_b) = start_secured(b_cert, b_key, b_guid, b_prefix, GOV_DATA_ENCRYPT);
let (rt_c, _gate_c) = start_secured(c_cert, c_key, c_guid, c_prefix, GOV_DATA_ENCRYPT);
assert!(
wait_handshake(
&gate_a,
&gate_b,
a_prefix.to_bytes(),
b_prefix.to_bytes(),
Duration::from_secs(30),
),
"A<->B handshake must finish"
);
let writer_eid = rt_a.register_user_writer(writer_cfg("SecuredNeg")).unwrap();
let (_b_reader, b_rx) = rt_b.register_user_reader(reader_cfg("SecuredNeg")).unwrap();
let (_c_reader, c_rx) = rt_c.register_user_reader(reader_cfg("SecuredNeg")).unwrap();
let deadline = Instant::now() + Duration::from_secs(15);
while Instant::now() < deadline && rt_a.user_writer_matched_count(writer_eid) < 2 {
std::thread::sleep(Duration::from_millis(50));
}
assert!(
rt_a.user_writer_matched_count(writer_eid) >= 2,
"A must match B AND C (otherwise the negative control is not meaningful)"
);
let payload = b"only-authenticated-may-read".to_vec();
let mut b_got = false;
for _ in 0..20 {
rt_a.write_user_sample(writer_eid, payload.clone()).unwrap();
if b_rx.recv_timeout(Duration::from_millis(400)).is_ok() {
b_got = true;
break;
}
}
assert!(b_got, "B (authenticated) must decrypt + read the sample");
let c_got = c_rx.recv_timeout(Duration::from_secs(2)).is_ok();
assert!(
!c_got,
"C (foreign CA) must NOT read the secured sample — proof that user DATA is encrypted. \
Diagnostics: gov_data={:?}, a_registered_c={} (should be false — otherwise the A<->C handshake succeeded despite the foreign CA)",
gate_a.data_protection(),
gate_a
.slot_for(&c_prefix.to_bytes())
.ok()
.flatten()
.is_some(),
);
rt_a.shutdown();
rt_b.shutdown();
rt_c.shutdown();
}