use crate::tls::TlsInfo;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Denied {
ProfileMismatch(&'static str),
BadCredentials,
InvalidCertificate(String),
}
pub struct Inputs<'a> {
pub profile: Option<i16>,
pub tls: Option<&'a TlsInfo>,
pub basic: Option<bool>,
pub has_password: bool,
pub require_password: bool,
pub identity: &'a str,
pub serial_number: Option<&'a str>,
pub cso_name: Option<&'a str>,
}
pub fn check(i: &Inputs<'_>) -> Result<(), Denied> {
let basic_ok = || match i.basic {
Some(true) => Ok(()),
_ => Err(Denied::BadCredentials),
};
match i.profile {
None | Some(0) => {
if i.has_password || i.require_password {
basic_ok()
} else {
Ok(())
}
}
Some(1) => {
if i.tls.is_some() {
return Err(Denied::ProfileMismatch(
"station is configured for Security Profile 1 (unsecured transport)",
));
}
basic_ok()
}
Some(2) => {
if i.tls.is_none() {
return Err(Denied::ProfileMismatch(
"station is configured for Security Profile 2 (TLS required)",
));
}
basic_ok()
}
Some(3) => {
let Some(tls) = i.tls else {
return Err(Denied::ProfileMismatch(
"station is configured for Security Profile 3 (TLS with client certificate required)",
));
};
let Some(cert) = &tls.client_cert else {
return Err(Denied::InvalidCertificate(
"no client certificate presented".into(),
));
};
if !cert.verified {
return Err(Denied::InvalidCertificate(
cert.error
.clone()
.unwrap_or_else(|| "certificate not verified".into()),
));
}
if let Some(cso) = i.cso_name
&& !cert
.organization
.as_deref()
.is_some_and(|o| o.contains(cso))
{
return Err(Denied::InvalidCertificate(format!(
"organizationName {:?} does not contain the CSO name",
cert.organization
)));
}
let cn = cert.common_name.as_deref().unwrap_or("");
let matches = (!cn.is_empty() && cn.contains(i.identity))
|| i.serial_number
.is_some_and(|s| !s.is_empty() && cn.contains(s));
if !matches {
return Err(Denied::InvalidCertificate(format!(
"commonName {cn:?} does not match the charging station"
)));
}
Ok(())
}
Some(_) => Err(Denied::ProfileMismatch(
"invalid security profile configured",
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tls::ClientCert;
fn tls(cert: Option<(&str, &str, bool)>) -> TlsInfo {
TlsInfo {
version: "TLSv1.3".into(),
cipher: "x".into(),
client_cert: cert.map(|(o, cn, verified)| ClientCert {
common_name: Some(cn.into()),
organization: Some(o.into()),
verified,
error: (!verified).then(|| "unable to get local issuer certificate".into()),
serial_hex: "01".into(),
}),
}
}
fn inputs<'a>(profile: Option<i16>, t: Option<&'a TlsInfo>, basic: Option<bool>) -> Inputs<'a> {
Inputs {
profile,
tls: t,
basic,
has_password: true,
require_password: false,
identity: "CS1",
serial_number: Some("SN-9"),
cso_name: Some("TestCSO"),
}
}
#[test]
fn profiles() {
let plain = None;
let t = tls(None);
let good = tls(Some(("TestCSO", "CS1", true)));
let by_serial = tls(Some(("TestCSO", "SN-9", true)));
let other_org = tls(Some(("OtherCSO", "CS1", true)));
let untrusted = tls(Some(("TestCSO", "CS1", false)));
let wrong_cn = tls(Some(("TestCSO", "CS2", true)));
assert_eq!(check(&inputs(Some(1), plain, Some(true))), Ok(()));
assert_eq!(
check(&inputs(Some(1), plain, None)),
Err(Denied::BadCredentials)
);
assert!(matches!(
check(&inputs(Some(1), Some(&t), Some(true))),
Err(Denied::ProfileMismatch(_))
));
assert_eq!(check(&inputs(Some(2), Some(&t), Some(true))), Ok(()));
assert_eq!(
check(&inputs(Some(2), Some(&t), Some(false))),
Err(Denied::BadCredentials)
);
assert!(matches!(
check(&inputs(Some(2), plain, Some(true))),
Err(Denied::ProfileMismatch(_))
));
assert_eq!(check(&inputs(Some(3), Some(&good), None)), Ok(()));
assert_eq!(check(&inputs(Some(3), Some(&by_serial), None)), Ok(()));
for bad in [&t, &other_org, &untrusted, &wrong_cn] {
assert!(matches!(
check(&inputs(Some(3), Some(bad), None)),
Err(Denied::InvalidCertificate(_))
));
}
assert!(matches!(
check(&inputs(Some(3), plain, None)),
Err(Denied::ProfileMismatch(_))
));
let mut open = inputs(None, plain, None);
open.has_password = false;
assert_eq!(check(&open), Ok(()));
open.require_password = true;
assert_eq!(check(&open), Err(Denied::BadCredentials));
}
}