1use std::net::IpAddr;
17use std::time::SystemTime;
18
19use crate::x509::{Certificate, digest::Algorithm};
20
21#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
23pub enum NameField {
24 Subject,
26 Dns,
28 Uri,
30 Ip,
32 Email,
34}
35
36#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
38pub enum Failure {
39 Mismatch(NameField),
41 InvalidClientIp,
43 MultipleSan(NameField),
45 MalformedSanIp,
47 NotYetValid,
49 Expired,
51 ThumbprintMismatch,
53}
54
55pub fn name_matches(cert: &Certificate, field: NameField, name: &str) -> Result<(), Failure> {
61 let eq = |v: &str| v.to_lowercase() == name.to_lowercase();
62 let san = cert.san();
63 let matched = match field {
64 NameField::Subject => cert.subject_cn().is_some_and(eq),
65 NameField::Dns => san.dns.iter().any(|v| eq(v)),
66 NameField::Uri => san.uri.iter().any(|v| eq(v)),
67 NameField::Email => san.email.iter().any(|v| eq(v)),
68 NameField::Ip => {
69 let ip: IpAddr = name.parse().map_err(|_| Failure::InvalidClientIp)?;
70 san.ip.iter().any(|v| v.addr() == Some(ip))
71 }
72 };
73 if matched {
74 Ok(())
75 } else {
76 Err(Failure::Mismatch(field))
77 }
78}
79
80pub fn field(cert: &Certificate, field: NameField) -> Result<Option<String>, Failure> {
88 fn single<T>(v: &[T], field: NameField) -> Result<Option<&T>, Failure> {
89 match v {
90 [] => Ok(None),
91 [v] => Ok(Some(v)),
92 _ => Err(Failure::MultipleSan(field)),
93 }
94 }
95 let san = cert.san();
96 Ok(match field {
97 NameField::Subject => cert.subject_cn().map(str::to_lowercase),
98 NameField::Dns => single(&san.dns, field)?.map(|v| v.to_lowercase()),
99 NameField::Uri => single(&san.uri, field)?.map(|v| v.to_lowercase()),
100 NameField::Email => single(&san.email, field)?.map(|v| v.to_lowercase()),
101 NameField::Ip => match single(&san.ip, field)? {
102 Some(ip) => Some(ip.addr().ok_or(Failure::MalformedSanIp)?.to_string()),
103 None => None,
104 },
105 })
106}
107
108pub fn validity(cert: &Certificate, now: SystemTime) -> Result<SystemTime, Failure> {
114 if now < cert.not_before() {
115 Err(Failure::NotYetValid)
116 } else if now > cert.not_after() {
117 Err(Failure::Expired)
118 } else {
119 Ok(cert.not_after())
120 }
121}
122
123pub fn thumbprint_matches<'a>(
125 cert: &Certificate,
126 thumbprints: impl IntoIterator<Item = &'a str>,
127) -> bool {
128 thumbprints.into_iter().any(|t| {
129 let alg = Algorithm::for_hex_thumbprint(t);
130 hex::decode(t).is_ok_and(|t| t.as_slice() == cert.digest(alg))
131 })
132}
133
134pub fn verify_thumbprint<'a>(
139 cert: &Certificate,
140 thumbprints: impl IntoIterator<Item = &'a str>,
141 now: SystemTime,
142) -> Result<SystemTime, Failure> {
143 let expires = validity(cert, now)?;
144 if thumbprint_matches(cert, thumbprints) {
145 Ok(expires)
146 } else {
147 Err(Failure::ThumbprintMismatch)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use std::time::Duration;
154
155 use super::*;
156 use crate::x509::testgen::{self, San};
157
158 fn cert(san: &San<'_>) -> Certificate {
159 testgen::cert("Client1", false, None, &testgen::key(), san, (-1, 1))
160 }
161
162 #[test]
167 fn names() {
168 let c = cert(&San::default());
169 assert_eq!(name_matches(&c, NameField::Subject, "CLIENT1"), Ok(()));
170 assert_eq!(
171 name_matches(&c, NameField::Dns, "client1"),
172 Err(Failure::Mismatch(NameField::Dns))
173 );
174 assert_eq!(
175 name_matches(&c, NameField::Ip, "x"),
176 Err(Failure::InvalidClientIp)
177 );
178 assert_eq!(field(&c, NameField::Subject), Ok(Some("client1".into())));
179 assert_eq!(field(&c, NameField::Email), Ok(None));
180
181 let c = cert(&San {
182 dns: &["A.b", "c.d"],
183 email: &["X@y"],
184 ip: &["::1".parse().unwrap()],
185 ..San::default()
186 });
187 assert_eq!(name_matches(&c, NameField::Dns, "c.D"), Ok(()));
188 assert_eq!(name_matches(&c, NameField::Ip, "::0:1"), Ok(()));
189 assert_eq!(
190 field(&c, NameField::Dns),
191 Err(Failure::MultipleSan(NameField::Dns))
192 );
193 assert_eq!(field(&c, NameField::Ip), Ok(Some("::1".into())));
194 assert_eq!(field(&c, NameField::Email), Ok(Some("x@y".into())));
195 }
196
197 #[test]
202 fn thumbprints_and_validity() {
203 let c = cert(&San::default());
204 let now = SystemTime::now();
205 let sha1 = c.thumbprint(Algorithm::Sha1).to_uppercase();
206 let sha256 = c.thumbprint(Algorithm::Sha256);
207 assert_eq!(
208 verify_thumbprint(&c, [sha1.as_str()], now),
209 Ok(c.not_after())
210 );
211 assert_eq!(
212 verify_thumbprint(&c, ["zz", sha256.as_str()], now),
213 Ok(c.not_after())
214 );
215 assert_eq!(
216 verify_thumbprint(&c, [&sha256[2..]], now),
217 Err(Failure::ThumbprintMismatch)
218 );
219 let day = Duration::from_hours(48);
220 assert_eq!(
221 verify_thumbprint(&c, [sha1.as_str()], now - day),
222 Err(Failure::NotYetValid)
223 );
224 assert_eq!(
225 verify_thumbprint(&c, [sha1.as_str()], now + day),
226 Err(Failure::Expired)
227 );
228 }
229}