1use std::fmt;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum HashFunc {
25 Sha1,
27 Sha224,
29 Sha256,
31 Sha384,
33 Sha512,
35}
36
37impl HashFunc {
38 #[must_use]
40 pub fn as_str(self) -> &'static str {
41 match self {
42 Self::Sha1 => "sha-1",
43 Self::Sha224 => "sha-224",
44 Self::Sha256 => "sha-256",
45 Self::Sha384 => "sha-384",
46 Self::Sha512 => "sha-512",
47 }
48 }
49
50 #[must_use]
56 pub fn parse(token: &str) -> Option<Self> {
57 [
58 Self::Sha1,
59 Self::Sha224,
60 Self::Sha256,
61 Self::Sha384,
62 Self::Sha512,
63 ]
64 .into_iter()
65 .find(|candidate| token.eq_ignore_ascii_case(candidate.as_str()))
66 }
67
68 #[must_use]
70 pub fn digest_len(self) -> usize {
71 match self {
72 Self::Sha1 => 20,
73 Self::Sha224 => 28,
74 Self::Sha256 => 32,
75 Self::Sha384 => 48,
76 Self::Sha512 => 64,
77 }
78 }
79
80 #[must_use]
82 pub fn hash(self, certificate: &[u8]) -> Vec<u8> {
83 use sha1::Sha1;
84 use sha2::{Digest as _, Sha224, Sha256, Sha384, Sha512};
85 match self {
86 Self::Sha1 => Sha1::digest(certificate).to_vec(),
87 Self::Sha224 => Sha224::digest(certificate).to_vec(),
88 Self::Sha256 => Sha256::digest(certificate).to_vec(),
89 Self::Sha384 => Sha384::digest(certificate).to_vec(),
90 Self::Sha512 => Sha512::digest(certificate).to_vec(),
91 }
92 }
93}
94
95#[derive(Clone, PartialEq, Eq)]
97pub struct Fingerprint {
98 pub func: HashFunc,
100 pub digest: Vec<u8>,
103}
104
105impl fmt::Debug for Fingerprint {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 f.debug_struct("Fingerprint")
110 .field("func", &self.func)
111 .field("digest_len", &self.digest.len())
112 .finish()
113 }
114}
115
116impl Fingerprint {
117 #[must_use]
119 pub fn of(certificate: &[u8], func: HashFunc) -> Self {
120 Self {
121 func,
122 digest: func.hash(certificate),
123 }
124 }
125
126 #[must_use]
133 pub fn parse(value: &str) -> Option<Self> {
134 let mut parts = value.trim().split_ascii_whitespace();
135 let func = HashFunc::parse(parts.next()?)?;
136 let printed = parts.next()?;
137 if parts.next().is_some() {
138 return None;
139 }
140 let mut digest = Vec::with_capacity(func.digest_len());
141 for pair in printed.split(':') {
142 if pair.len() != 2 {
143 return None;
144 }
145 digest.push(u8::from_str_radix(pair, 16).ok()?);
146 }
147 (digest.len() == func.digest_len()).then_some(Self { func, digest })
148 }
149
150 #[must_use]
155 pub fn to_value(&self) -> String {
156 use std::fmt::Write as _;
157 let printed =
158 self.digest
159 .iter()
160 .enumerate()
161 .fold(String::new(), |mut out, (index, byte)| {
162 if index > 0 {
163 out.push(':');
164 }
165 let _ = write!(out, "{byte:02X}");
166 out
167 });
168 format!("{} {printed}", self.func.as_str())
169 }
170
171 #[must_use]
177 pub fn matches(&self, certificate: &[u8]) -> bool {
178 use subtle::ConstantTimeEq as _;
179 let computed = self.func.hash(certificate);
180 computed.ct_eq(&self.digest).into()
181 }
182}
183
184#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186pub enum Setup {
187 Active,
189 Passive,
191 ActPass,
193 HoldConn,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202pub struct SetupCapabilities {
203 client: bool,
204 server: bool,
205}
206
207impl SetupCapabilities {
208 #[must_use]
210 pub const fn both() -> Self {
211 Self {
212 client: true,
213 server: true,
214 }
215 }
216
217 #[must_use]
219 pub const fn client_only() -> Self {
220 Self {
221 client: true,
222 server: false,
223 }
224 }
225
226 #[must_use]
228 pub const fn server_only() -> Self {
229 Self {
230 client: false,
231 server: true,
232 }
233 }
234
235 #[must_use]
237 pub const fn neither() -> Self {
238 Self {
239 client: false,
240 server: false,
241 }
242 }
243
244 pub fn answer_to(self, offered: Setup) -> Result<Setup, SetupRoleError> {
253 match offered {
254 Setup::ActPass | Setup::Passive if self.client => Ok(Setup::Active),
255 Setup::ActPass | Setup::Active if self.server => Ok(Setup::Passive),
256 Setup::ActPass => Err(SetupRoleError::NoAnswerRole),
257 Setup::Passive => Err(SetupRoleError::UnsupportedLocalRole(Setup::Active)),
258 Setup::Active => Err(SetupRoleError::UnsupportedLocalRole(Setup::Passive)),
259 Setup::HoldConn => Err(SetupRoleError::UnresolvedOffer(Setup::HoldConn)),
262 }
263 }
264
265 pub fn from_answer(self, answered: Option<Setup>) -> Result<Setup, SetupRoleError> {
272 match answered {
273 None => Err(SetupRoleError::MissingAnswer),
274 Some(Setup::Active) if self.server => Ok(Setup::Passive),
275 Some(Setup::Active) => Err(SetupRoleError::UnsupportedLocalRole(Setup::Passive)),
276 Some(Setup::Passive) if self.client => Ok(Setup::Active),
277 Some(Setup::Passive) => Err(SetupRoleError::UnsupportedLocalRole(Setup::Active)),
278 Some(unresolved @ (Setup::ActPass | Setup::HoldConn)) => {
279 Err(SetupRoleError::UnresolvedAnswer(unresolved))
280 }
281 }
282 }
283}
284
285impl Default for SetupCapabilities {
286 fn default() -> Self {
287 Self::both()
288 }
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
293#[non_exhaustive]
294pub enum SetupRoleError {
295 #[error("the DTLS offer did not select a usable setup role: {0:?}")]
297 UnresolvedOffer(Setup),
298 #[error("the DTLS answer supplied no setup role")]
300 MissingAnswer,
301 #[error("the DTLS answer did not resolve its setup role: {0:?}")]
303 UnresolvedAnswer(Setup),
304 #[error("the DTLS answer requires an unsupported local setup role: {0:?}")]
306 UnsupportedLocalRole(Setup),
307 #[error("no supported DTLS setup role is available for the answer")]
309 NoAnswerRole,
310}
311
312impl Setup {
313 #[must_use]
315 pub fn as_str(self) -> &'static str {
316 match self {
317 Self::Active => "active",
318 Self::Passive => "passive",
319 Self::ActPass => "actpass",
320 Self::HoldConn => "holdconn",
321 }
322 }
323
324 #[must_use]
326 pub fn parse(token: &str) -> Option<Self> {
327 [Self::Active, Self::Passive, Self::ActPass, Self::HoldConn]
328 .into_iter()
329 .find(|candidate| token.trim().eq_ignore_ascii_case(candidate.as_str()))
330 }
331
332 #[must_use]
342 pub fn answer(offered: Self) -> Self {
343 match offered {
344 Self::ActPass | Self::Passive => Self::Active,
345 Self::Active => Self::Passive,
346 Self::HoldConn => Self::HoldConn,
347 }
348 }
349
350 #[must_use]
352 pub fn is_client(self) -> bool {
353 matches!(self, Self::Active)
354 }
355}
356
357#[cfg(test)]
358#[allow(
359 clippy::unwrap_used,
360 clippy::expect_used,
361 clippy::panic,
362 clippy::indexing_slicing
363)]
364mod tests {
365 use super::*;
366
367 #[test]
369 fn a_fingerprint_round_trips_through_its_sdp_form() {
370 let certificate = b"a certificate, for the purposes of hashing something";
371 let printed = Fingerprint::of(certificate, HashFunc::Sha256).to_value();
372 let parsed = Fingerprint::parse(&printed).expect("parses");
373 assert_eq!(parsed.func, HashFunc::Sha256);
374 assert!(parsed.matches(certificate));
375 assert_eq!(parsed.to_value(), printed);
376 }
377
378 #[test]
380 fn the_printed_form_is_uppercase_hex_separated_by_colons() {
381 let fingerprint = Fingerprint {
382 func: HashFunc::Sha1,
383 digest: vec![
384 0xab, 0xcd, 0x01, 0x9f, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
385 ],
386 };
387 let value = fingerprint.to_value();
388 assert!(value.starts_with("sha-1 AB:CD:01:9F:"), "{value}");
389 assert_eq!(
390 value.matches(':').count(),
391 19,
392 "twenty octets means nineteen separators"
393 );
394 }
395
396 #[test]
399 fn a_lowercase_fingerprint_from_a_peer_is_still_read() {
400 let certificate = b"cert";
401 let upper = Fingerprint::of(certificate, HashFunc::Sha256).to_value();
402 let lower = upper.to_ascii_lowercase();
403 let parsed = Fingerprint::parse(&lower).expect("a peer's lowercase value parses");
404 assert!(parsed.matches(certificate));
405 }
406
407 #[test]
410 fn md5_and_md2_fingerprints_are_refused_rather_than_carried() {
411 assert!(HashFunc::parse("md5").is_none());
412 assert!(HashFunc::parse("md2").is_none());
413 assert!(
414 Fingerprint::parse("md5 AB:CD:EF:01:23:45:67:89:AB:CD:EF:01:23:45:67:89").is_none(),
415 "a forbidden hash must not produce a fingerprint a caller could check against"
416 );
417 }
418
419 #[test]
422 fn a_digest_of_the_wrong_length_for_its_hash_is_refused() {
423 assert!(
424 Fingerprint::parse("sha-256 AB:CD").is_none(),
425 "two octets is not a SHA-256 digest"
426 );
427 let sha1_digest = Fingerprint::of(b"cert", HashFunc::Sha1).to_value();
428 let mislabelled = sha1_digest.replace("sha-1", "sha-256");
429 assert!(
430 Fingerprint::parse(&mislabelled).is_none(),
431 "a 20-octet digest labelled sha-256 must not be accepted"
432 );
433 }
434
435 #[test]
436 fn a_malformed_fingerprint_is_refused() {
437 assert!(Fingerprint::parse("").is_none());
438 assert!(Fingerprint::parse("sha-256").is_none(), "no digest");
439 assert!(Fingerprint::parse("sha-256 ZZ:ZZ").is_none(), "not hex");
440 assert!(
441 Fingerprint::parse("sha-256 ABC:DE").is_none(),
442 "groups are two hex digits"
443 );
444 let good = Fingerprint::of(b"cert", HashFunc::Sha256).to_value();
445 assert!(
446 Fingerprint::parse(&format!("{good} extra")).is_none(),
447 "a trailing token is not part of the grammar"
448 );
449 }
450
451 #[test]
454 fn a_different_certificate_does_not_match() {
455 let fingerprint = Fingerprint::of(b"the real certificate", HashFunc::Sha256);
456 assert!(fingerprint.matches(b"the real certificate"));
457 assert!(!fingerprint.matches(b"a substituted certificate"));
458 assert!(!fingerprint.matches(b"the real certificatf"));
460 }
461
462 #[test]
465 fn every_hash_produces_the_digest_length_it_declares() {
466 for func in [
467 HashFunc::Sha1,
468 HashFunc::Sha224,
469 HashFunc::Sha256,
470 HashFunc::Sha384,
471 HashFunc::Sha512,
472 ] {
473 assert_eq!(
474 func.hash(b"cert").len(),
475 func.digest_len(),
476 "{}",
477 func.as_str()
478 );
479 }
480 }
481
482 #[test]
484 fn actpass_is_answered_active_so_the_answerer_starts_the_handshake() {
485 assert_eq!(Setup::answer(Setup::ActPass), Setup::Active);
486 assert!(
487 Setup::answer(Setup::ActPass).is_client(),
488 "the answerer becomes the DTLS client, so its `ClientHello` opens the NAT it is behind"
489 );
490 assert_eq!(Setup::answer(Setup::Passive), Setup::Active);
491 assert_eq!(Setup::answer(Setup::Active), Setup::Passive);
492 }
493
494 #[test]
497 fn holdconn_is_answered_holdconn() {
498 assert_eq!(Setup::answer(Setup::HoldConn), Setup::HoldConn);
499 assert!(!Setup::answer(Setup::HoldConn).is_client());
500 }
501
502 #[test]
503 fn setup_round_trips_and_rejects_what_is_not_a_role() {
504 for role in [
505 Setup::Active,
506 Setup::Passive,
507 Setup::ActPass,
508 Setup::HoldConn,
509 ] {
510 assert_eq!(Setup::parse(role.as_str()), Some(role));
511 assert_eq!(
512 Setup::parse(&role.as_str().to_ascii_uppercase()),
513 Some(role)
514 );
515 }
516 assert!(Setup::parse("both").is_none());
517 assert!(Setup::parse("").is_none());
518 }
519
520 #[test]
521 fn only_active_is_the_client() {
522 assert!(Setup::Active.is_client());
523 assert!(!Setup::Passive.is_client());
524 assert!(!Setup::ActPass.is_client());
527 assert!(!Setup::HoldConn.is_client());
528 }
529}