use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use zerodds_security::authentication::{SharedSecretHandle, SharedSecretProvider};
use zerodds_security::error::SecurityError;
use zerodds_security_crypto::{AesGcmCryptoPlugin, Suite};
use zerodds_security_permissions::{
CmsPkcs7Verifier, Governance, Permissions, PermissionsError, XmlSignatureVerifier,
open_signed_permissions, parse_governance_xml,
};
use zerodds_security_pki::{IdentityConfig, IdentityHandle, PkiAuthenticationPlugin, PkiError};
use crate::SharedSecurityGate;
struct SharedPkiSecretProvider(Arc<Mutex<PkiAuthenticationPlugin>>);
impl SharedSecretProvider for SharedPkiSecretProvider {
fn get_shared_secret(&self, handle: SharedSecretHandle) -> Option<Vec<u8>> {
self.0.lock().ok()?.get_shared_secret(handle)
}
fn get_shared_secret_challenges(
&self,
handle: SharedSecretHandle,
) -> Option<([u8; 32], [u8; 32])> {
self.0.lock().ok()?.get_shared_secret_challenges(handle)
}
}
#[derive(Debug)]
pub enum SecurityProfileError {
Io {
path: PathBuf,
source: std::io::Error,
},
Pki(PkiError),
PkiSecurity(SecurityError),
Permissions(PermissionsError),
GovernanceUtf8(core::str::Utf8Error),
}
impl core::fmt::Display for SecurityProfileError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Io { path, source } => {
write!(f, "security-profile io {}: {source}", path.display())
}
Self::Pki(e) => write!(f, "security-profile pki: {e}"),
Self::PkiSecurity(e) => write!(f, "security-profile pki: {e}"),
Self::Permissions(e) => write!(f, "security-profile permissions: {e}"),
Self::GovernanceUtf8(e) => write!(f, "security-profile governance utf-8: {e}"),
}
}
}
impl std::error::Error for SecurityProfileError {}
impl From<PkiError> for SecurityProfileError {
fn from(e: PkiError) -> Self {
Self::Pki(e)
}
}
impl From<SecurityError> for SecurityProfileError {
fn from(e: SecurityError) -> Self {
Self::PkiSecurity(e)
}
}
impl From<PermissionsError> for SecurityProfileError {
fn from(e: PermissionsError) -> Self {
Self::Permissions(e)
}
}
#[derive(Debug, Clone)]
pub struct SecurityProfileConfig {
pub domain_id: u32,
pub identity_ca_pem: PathBuf,
pub identity_cert_pem: PathBuf,
pub identity_key_pem: PathBuf,
pub permissions_ca_pem: PathBuf,
pub governance_p7s: PathBuf,
pub permissions_p7s: PathBuf,
}
pub struct SecurityProfile {
pub gate: Arc<SharedSecurityGate>,
pub pki: Arc<Mutex<PkiAuthenticationPlugin>>,
pub identity_handle: IdentityHandle,
pub adjusted_participant_guid: [u8; 16],
pub governance: Governance,
pub permissions: Permissions,
}
impl core::fmt::Debug for SecurityProfile {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SecurityProfile")
.field("gate", &self.gate)
.field("identity_handle", &self.identity_handle)
.field("governance", &self.governance)
.field("permissions", &"<redacted>")
.field("pki", &"<redacted PkiAuthenticationPlugin>")
.finish()
}
}
impl SecurityProfile {
pub fn from_files(
cfg: &SecurityProfileConfig,
participant_guid: [u8; 16],
) -> Result<Self, SecurityProfileError> {
let identity_cert_pem = read_path(&cfg.identity_cert_pem)?;
let identity_ca_pem = read_path(&cfg.identity_ca_pem)?;
let identity_key_pem = read_path(&cfg.identity_key_pem)?;
let permissions_ca_pem = read_path(&cfg.permissions_ca_pem)?;
let governance_p7s = read_path(&cfg.governance_p7s)?;
let permissions_p7s = read_path(&cfg.permissions_p7s)?;
let cert_der = zerodds_security_pki::first_cert_der(&identity_cert_pem)?;
let adjusted_prefix =
zerodds_security_pki::adjust_participant_guid_prefix(&participant_guid, &cert_der)?;
let mut adjusted_participant_guid = participant_guid;
adjusted_participant_guid[..12].copy_from_slice(&adjusted_prefix);
let mut pki = PkiAuthenticationPlugin::new();
let identity_handle = pki.validate_with_config(
IdentityConfig {
identity_cert_pem,
identity_ca_pem,
identity_key_pem: Some(identity_key_pem),
},
adjusted_participant_guid,
)?;
let verifier = CmsPkcs7Verifier::new(&permissions_ca_pem)?;
let governance_xml_bytes = verifier.verify_and_extract(&governance_p7s)?;
let governance_xml = core::str::from_utf8(&governance_xml_bytes)
.map_err(SecurityProfileError::GovernanceUtf8)?;
let governance = parse_governance_xml(governance_xml)?;
let permissions = open_signed_permissions(&permissions_p7s, &verifier)?;
pki.set_local_permissions(permissions_p7s.clone());
let pki = Arc::new(Mutex::new(pki));
let provider: Arc<dyn SharedSecretProvider> =
Arc::new(SharedPkiSecretProvider(Arc::clone(&pki)));
let sign_suite = |k: zerodds_security_permissions::ProtectionKind| {
if matches!(k, zerodds_security_permissions::ProtectionKind::Sign) {
Suite::Aes256Gmac
} else {
Suite::Aes256Gcm
}
};
let domain_rule = governance.find_domain_rule(cfg.domain_id);
let rtps_kind = domain_rule
.map(|r| r.rtps_protection_kind)
.unwrap_or_default();
let data_kind = domain_rule
.and_then(|r| r.topic_rules.first())
.map(|t| t.data_protection_kind)
.unwrap_or_default();
let metadata_kind = domain_rule
.and_then(|r| r.topic_rules.first())
.map(|t| t.metadata_protection_kind)
.unwrap_or_default();
let mut crypto = AesGcmCryptoPlugin::with_secret_provider(Suite::Aes256Gcm, provider);
let metadata_suite = if matches!(
metadata_kind,
zerodds_security_permissions::ProtectionKind::None
) {
None
} else {
Some(sign_suite(metadata_kind))
};
crypto.set_local_protection_suites(
Some(sign_suite(rtps_kind)),
Some(sign_suite(data_kind)),
metadata_suite,
);
let gate = Arc::new(SharedSecurityGate::new(
cfg.domain_id,
governance.clone(),
Box::new(crypto),
));
Ok(Self {
gate,
pki,
identity_handle,
adjusted_participant_guid,
governance,
permissions,
})
}
pub fn from_enclave_dir(
enclave_dir: impl AsRef<Path>,
domain_id: u32,
participant_guid: [u8; 16],
) -> Result<Self, SecurityProfileError> {
let dir = enclave_dir.as_ref();
let cfg = SecurityProfileConfig {
domain_id,
identity_ca_pem: dir.join("identity_ca.cert.pem"),
identity_cert_pem: dir.join("cert.pem"),
identity_key_pem: dir.join("key.pem"),
permissions_ca_pem: dir.join("permissions_ca.cert.pem"),
governance_p7s: dir.join("governance.p7s"),
permissions_p7s: dir.join("permissions.p7s"),
};
Self::from_files(&cfg, participant_guid)
}
pub fn from_env(participant_guid: [u8; 16]) -> Result<Option<Self>, SecurityProfileError> {
let dir = match std::env::var("ZERODDS_SECURITY_DIR") {
Ok(d) if !d.is_empty() => d,
_ => return Ok(None),
};
let domain_id = std::env::var("ROS_DOMAIN_ID")
.ok()
.and_then(|s| s.trim().parse::<u32>().ok())
.unwrap_or(0);
Self::from_enclave_dir(dir, domain_id, participant_guid).map(Some)
}
}
fn read_path(p: &Path) -> Result<Vec<u8>, SecurityProfileError> {
std::fs::read(p).map_err(|source| SecurityProfileError::Io {
path: p.to_path_buf(),
source,
})
}
#[must_use]
pub fn strip_file_url(s: &str) -> String {
s.strip_prefix("file://")
.map(|rest| rest.trim_start_matches('/').to_string())
.map(|rest| {
if s.starts_with("file:///") {
format!("/{rest}")
} else {
rest
}
})
.unwrap_or_else(|| s.to_string())
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn strip_file_url_handles_triple_slash() {
assert_eq!(
strip_file_url("file:///etc/dds/certs/ca.pem"),
"/etc/dds/certs/ca.pem"
);
}
#[test]
fn strip_file_url_handles_double_slash() {
assert_eq!(
strip_file_url("file://relative/path.pem"),
"relative/path.pem"
);
}
#[test]
fn strip_file_url_passes_plain_path() {
assert_eq!(strip_file_url("/tmp/whatever.pem"), "/tmp/whatever.pem");
}
#[test]
fn missing_file_returns_io_error() {
let cfg = SecurityProfileConfig {
domain_id: 0,
identity_ca_pem: PathBuf::from("/zerodds/_does_not_exist_ca.pem"),
identity_cert_pem: PathBuf::from("/zerodds/_does_not_exist_cert.pem"),
identity_key_pem: PathBuf::from("/zerodds/_does_not_exist_key.pem"),
permissions_ca_pem: PathBuf::from("/zerodds/_does_not_exist_pca.pem"),
governance_p7s: PathBuf::from("/zerodds/_does_not_exist_gov.p7s"),
permissions_p7s: PathBuf::from("/zerodds/_does_not_exist_perm.p7s"),
};
match SecurityProfile::from_files(&cfg, [0u8; 16]) {
Err(SecurityProfileError::Io { .. }) => {}
Err(e) => panic!("expected Io error, got: {e:?}"),
Ok(_) => panic!("expected Err, got Ok"),
}
}
fn scratch_dir(tag: &str) -> PathBuf {
use std::sync::atomic::{AtomicU32, Ordering};
static N: AtomicU32 = AtomicU32::new(0);
let dir = std::env::temp_dir().join(format!(
"zerodds_enclave_{tag}_{}_{}",
std::process::id(),
N.fetch_add(1, Ordering::Relaxed)
));
std::fs::create_dir_all(&dir).expect("mkdir scratch");
dir
}
#[test]
fn enclave_dir_resolves_all_sros2_filenames() {
let dir = scratch_dir("all");
for f in [
"cert.pem",
"key.pem",
"identity_ca.cert.pem",
"permissions_ca.cert.pem",
"governance.p7s",
"permissions.p7s",
] {
std::fs::write(dir.join(f), b"dummy").expect("write");
}
let res = SecurityProfile::from_enclave_dir(&dir, 0, [0u8; 16]);
std::fs::remove_dir_all(&dir).ok();
match res {
Err(SecurityProfileError::Io { path, .. }) => {
panic!(
"filename mapping wrong — unexpected Io on {}",
path.display()
)
}
Err(_) => {} Ok(_) => panic!("dummy content must not build a valid profile"),
}
}
#[test]
fn enclave_dir_missing_cert_is_io_naming_cert() {
let dir = scratch_dir("nocert");
let res = SecurityProfile::from_enclave_dir(&dir, 0, [0u8; 16]);
std::fs::remove_dir_all(&dir).ok();
match res {
Err(SecurityProfileError::Io { path, .. }) => {
assert!(
path.ends_with("cert.pem"),
"Io path should be the enclave cert.pem, got {}",
path.display()
);
}
other => panic!("expected Io on cert.pem, got {other:?}"),
}
}
#[test]
fn from_env_unset_returns_none() {
if std::env::var("ZERODDS_SECURITY_DIR").is_ok() {
return;
}
match SecurityProfile::from_env([0u8; 16]) {
Ok(None) => {}
other => panic!("expected Ok(None) when ZERODDS_SECURITY_DIR unset, got {other:?}"),
}
}
}