1use crate::tls::TlsInfo;
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub enum Denied {
20 ProfileMismatch(&'static str),
22 BadCredentials,
24 InvalidCertificate(String),
26}
27
28pub struct Inputs<'a> {
29 pub profile: Option<i16>,
30 pub tls: Option<&'a TlsInfo>,
31 pub basic: Option<bool>,
33 pub has_password: bool,
34 pub require_password: bool,
35 pub identity: &'a str,
36 pub serial_number: Option<&'a str>,
37 pub cso_name: Option<&'a str>,
38}
39
40pub fn check(i: &Inputs<'_>) -> Result<(), Denied> {
41 let basic_ok = || match i.basic {
42 Some(true) => Ok(()),
43 _ => Err(Denied::BadCredentials),
44 };
45 match i.profile {
46 None | Some(0) => {
47 if i.has_password || i.require_password {
48 basic_ok()
49 } else {
50 Ok(())
51 }
52 }
53 Some(1) => {
54 if i.tls.is_some() {
55 return Err(Denied::ProfileMismatch(
56 "station is configured for Security Profile 1 (unsecured transport)",
57 ));
58 }
59 basic_ok()
60 }
61 Some(2) => {
62 if i.tls.is_none() {
63 return Err(Denied::ProfileMismatch(
64 "station is configured for Security Profile 2 (TLS required)",
65 ));
66 }
67 basic_ok()
68 }
69 Some(3) => {
70 let Some(tls) = i.tls else {
71 return Err(Denied::ProfileMismatch(
72 "station is configured for Security Profile 3 (TLS with client certificate required)",
73 ));
74 };
75 let Some(cert) = &tls.client_cert else {
76 return Err(Denied::InvalidCertificate(
77 "no client certificate presented".into(),
78 ));
79 };
80 if !cert.verified {
81 return Err(Denied::InvalidCertificate(
82 cert.error
83 .clone()
84 .unwrap_or_else(|| "certificate not verified".into()),
85 ));
86 }
87 if let Some(cso) = i.cso_name
89 && !cert
90 .organization
91 .as_deref()
92 .is_some_and(|o| o.contains(cso))
93 {
94 return Err(Denied::InvalidCertificate(format!(
95 "organizationName {:?} does not contain the CSO name",
96 cert.organization
97 )));
98 }
99 let cn = cert.common_name.as_deref().unwrap_or("");
102 let matches = (!cn.is_empty() && cn.contains(i.identity))
103 || i.serial_number
104 .is_some_and(|s| !s.is_empty() && cn.contains(s));
105 if !matches {
106 return Err(Denied::InvalidCertificate(format!(
107 "commonName {cn:?} does not match the charging station"
108 )));
109 }
110 Ok(())
111 }
112 Some(_) => Err(Denied::ProfileMismatch(
113 "invalid security profile configured",
114 )),
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121 use crate::tls::ClientCert;
122
123 fn tls(cert: Option<(&str, &str, bool)>) -> TlsInfo {
124 TlsInfo {
125 version: "TLSv1.3".into(),
126 cipher: "x".into(),
127 client_cert: cert.map(|(o, cn, verified)| ClientCert {
128 common_name: Some(cn.into()),
129 organization: Some(o.into()),
130 verified,
131 error: (!verified).then(|| "unable to get local issuer certificate".into()),
132 serial_hex: "01".into(),
133 }),
134 }
135 }
136
137 fn inputs<'a>(profile: Option<i16>, t: Option<&'a TlsInfo>, basic: Option<bool>) -> Inputs<'a> {
138 Inputs {
139 profile,
140 tls: t,
141 basic,
142 has_password: true,
143 require_password: false,
144 identity: "CS1",
145 serial_number: Some("SN-9"),
146 cso_name: Some("TestCSO"),
147 }
148 }
149
150 #[test]
151 fn profiles() {
152 let plain = None;
153 let t = tls(None);
154 let good = tls(Some(("TestCSO", "CS1", true)));
155 let by_serial = tls(Some(("TestCSO", "SN-9", true)));
156 let other_org = tls(Some(("OtherCSO", "CS1", true)));
157 let untrusted = tls(Some(("TestCSO", "CS1", false)));
158 let wrong_cn = tls(Some(("TestCSO", "CS2", true)));
159
160 assert_eq!(check(&inputs(Some(1), plain, Some(true))), Ok(()));
161 assert_eq!(
162 check(&inputs(Some(1), plain, None)),
163 Err(Denied::BadCredentials)
164 );
165 assert!(matches!(
166 check(&inputs(Some(1), Some(&t), Some(true))),
167 Err(Denied::ProfileMismatch(_))
168 ));
169
170 assert_eq!(check(&inputs(Some(2), Some(&t), Some(true))), Ok(()));
171 assert_eq!(
172 check(&inputs(Some(2), Some(&t), Some(false))),
173 Err(Denied::BadCredentials)
174 );
175 assert!(matches!(
176 check(&inputs(Some(2), plain, Some(true))),
177 Err(Denied::ProfileMismatch(_))
178 ));
179
180 assert_eq!(check(&inputs(Some(3), Some(&good), None)), Ok(()));
181 assert_eq!(check(&inputs(Some(3), Some(&by_serial), None)), Ok(()));
182 for bad in [&t, &other_org, &untrusted, &wrong_cn] {
183 assert!(matches!(
184 check(&inputs(Some(3), Some(bad), None)),
185 Err(Denied::InvalidCertificate(_))
186 ));
187 }
188 assert!(matches!(
189 check(&inputs(Some(3), plain, None)),
190 Err(Denied::ProfileMismatch(_))
191 ));
192
193 let mut open = inputs(None, plain, None);
194 open.has_password = false;
195 assert_eq!(check(&open), Ok(()));
196 open.require_password = true;
197 assert_eq!(check(&open), Err(Denied::BadCredentials));
198 }
199}