pub use zerodds_bridge_security::{
Acl, AclEntry, AclOp, AuthError, AuthMode, AuthSubject, SecurityConfig, SecurityCtx,
SecurityError, authorize, build_ctx,
};
pub fn ctx_from_daemon_config(
cfg: &super::config::DaemonConfig,
) -> Result<SecurityCtx, SecurityError> {
let mut sc = SecurityConfig::default();
sc.auth_mode = cfg.auth_mode.clone();
if let (Some(tok), Some(subj)) = (
cfg.auth_bearer_token.as_ref(),
cfg.auth_bearer_subject.as_ref(),
) {
sc.bearer_tokens.insert(tok.clone(), subj.clone());
} else if let Some(tok) = cfg.auth_bearer_token.as_ref() {
sc.bearer_tokens.insert(tok.clone(), "anonymous".into());
}
for (topic, (read, write)) in &cfg.topic_acl {
sc.topic_acl.insert(
topic.clone(),
AclEntry {
read: read.clone(),
write: write.clone(),
},
);
}
forbid_tls_until_dtls_lib(&sc)?;
build_ctx(&sc)
}
pub const COAP_OPTION_AUTH_TOKEN: u16 = 65000;
pub fn authenticate_coap(
auth: &AuthMode,
auth_token_option: Option<&[u8]>,
) -> Result<AuthSubject, AuthError> {
let header_str = match auth_token_option {
Some(b) => Some(
core::str::from_utf8(b)
.map_err(|_| AuthError::MalformedCredentials("auth-token utf8".into()))?,
),
None => None,
};
zerodds_bridge_security::authenticate(auth, header_str, None, None)
}
pub fn forbid_tls_until_dtls_lib(cfg: &SecurityConfig) -> Result<(), SecurityError> {
if cfg.tls_cert.is_some() || cfg.tls_key.is_some() {
return Err(SecurityError::Tls(
zerodds_bridge_security::TlsConfigError::Rustls(
"CoAP §7.1: DTLS server not yet wired; rustls 0.23 is TCP-TLS only. \
Track: \"DTLS in next phase\"."
.into(),
),
));
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
use std::collections::HashMap;
#[test]
fn coap_bearer_in_option_accepted() {
let mut tokens = HashMap::new();
tokens.insert("tok".into(), AuthSubject::new("alice"));
let auth = AuthMode::Bearer { tokens };
let s = authenticate_coap(&auth, Some(b"Bearer tok")).unwrap();
assert_eq!(s.name, "alice");
}
#[test]
fn coap_missing_option_with_bearer_rejected() {
let auth = AuthMode::Bearer {
tokens: HashMap::new(),
};
let err = authenticate_coap(&auth, None).unwrap_err();
assert!(matches!(err, AuthError::MissingCredentials));
}
#[test]
fn coap_none_auth_yields_anonymous() {
let s = authenticate_coap(&AuthMode::None, None).unwrap();
assert_eq!(s.name, "anonymous");
}
#[test]
fn coap_dtls_phase_marker_blocks_tls_cli() {
let cfg = SecurityConfig {
tls_cert: Some("/x".into()),
tls_key: Some("/y".into()),
..Default::default()
};
let err = forbid_tls_until_dtls_lib(&cfg).unwrap_err();
assert!(matches!(err, SecurityError::Tls(_)));
}
#[test]
fn coap_no_tls_args_passes_phase_check() {
forbid_tls_until_dtls_lib(&SecurityConfig::default()).unwrap();
}
#[test]
fn coap_acl_subscribe_check() {
let mut cfg = SecurityConfig::default();
cfg.topic_acl.insert(
"rd/temp".into(),
AclEntry {
read: vec!["*".into()],
write: vec!["sensor".into()],
},
);
let ctx = build_ctx(&cfg).unwrap();
let bob = AuthSubject::new("bob");
assert!(authorize(&ctx.acl, &bob, AclOp::Read, "rd/temp"));
assert!(!authorize(&ctx.acl, &bob, AclOp::Write, "rd/temp"));
}
}