1use std::{
2 fmt,
3 num::ParseIntError,
4 str::{self, FromStr},
5};
6
7use snafu::{ResultExt, Snafu};
8
9const DHTTP_SKI_FIELD_COUNT: usize = 3;
10const OWNER_HASH_HEX_LEN: usize = 64;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct CertificateSequence(u32);
14
15impl CertificateSequence {
16 pub const MAX: u32 = i32::MAX as u32;
17
18 pub fn get(self) -> u32 {
19 self.0
20 }
21}
22
23#[derive(Debug, Snafu)]
24#[snafu(module)]
25pub enum InvalidCertificateSequence {
26 #[snafu(display("certificate sequence must be non-negative"))]
27 Negative,
28 #[snafu(display("certificate sequence exceeds supported database range"))]
29 OutOfRange { value: u64 },
30}
31
32impl From<u8> for CertificateSequence {
33 fn from(value: u8) -> Self {
34 Self(value as u32)
35 }
36}
37
38impl From<u16> for CertificateSequence {
39 fn from(value: u16) -> Self {
40 Self(value as u32)
41 }
42}
43
44impl TryFrom<u32> for CertificateSequence {
45 type Error = InvalidCertificateSequence;
46
47 fn try_from(value: u32) -> Result<Self, Self::Error> {
48 if value > Self::MAX {
49 return invalid_certificate_sequence::OutOfRangeSnafu {
50 value: value as u64,
51 }
52 .fail();
53 }
54 Ok(Self(value))
55 }
56}
57
58impl TryFrom<i32> for CertificateSequence {
59 type Error = InvalidCertificateSequence;
60
61 fn try_from(value: i32) -> Result<Self, Self::Error> {
62 if value < 0 {
63 return invalid_certificate_sequence::NegativeSnafu.fail();
64 }
65 Self::try_from(value as u32)
66 }
67}
68
69impl TryFrom<u64> for CertificateSequence {
70 type Error = InvalidCertificateSequence;
71
72 fn try_from(value: u64) -> Result<Self, Self::Error> {
73 if value > Self::MAX as u64 {
74 return invalid_certificate_sequence::OutOfRangeSnafu { value }.fail();
75 }
76 Ok(Self(value as u32))
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
81pub enum CertificateUsage {
82 ClientOnly,
83 ClientAndServer,
84}
85
86impl CertificateUsage {
87 pub fn as_str(self) -> &'static str {
88 match self {
89 Self::ClientOnly => "client",
90 Self::ClientAndServer => "client and server",
91 }
92 }
93
94 pub fn kind_flag(self) -> &'static str {
95 match self {
96 Self::ClientOnly => "1",
97 Self::ClientAndServer => "0",
98 }
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Hash)]
103pub struct OwnerHash(String);
104
105impl OwnerHash {
106 pub fn as_str(&self) -> &str {
107 &self.0
108 }
109}
110
111#[derive(Debug, Snafu)]
112#[snafu(module)]
113pub enum InvalidOwnerHash {
114 #[snafu(display("owner hash must be 64 lowercase hexadecimal characters"))]
115 Invalid,
116}
117
118impl TryFrom<&str> for OwnerHash {
119 type Error = InvalidOwnerHash;
120
121 fn try_from(value: &str) -> Result<Self, Self::Error> {
122 if value.len() == OWNER_HASH_HEX_LEN
123 && value
124 .bytes()
125 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
126 {
127 Ok(Self(value.to_owned()))
128 } else {
129 invalid_owner_hash::InvalidSnafu.fail()
130 }
131 }
132}
133
134impl fmt::Display for OwnerHash {
135 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136 f.write_str(&self.0)
137 }
138}
139
140#[derive(Debug, Clone, PartialEq, Eq, Hash)]
141pub struct CertificateChainKey {
142 sequence: CertificateSequence,
143 usage: CertificateUsage,
144}
145
146impl CertificateChainKey {
147 pub fn new(sequence: CertificateSequence, usage: CertificateUsage) -> Self {
148 Self { sequence, usage }
149 }
150
151 pub fn sequence(&self) -> CertificateSequence {
152 self.sequence
153 }
154
155 pub fn usage(&self) -> CertificateUsage {
156 self.usage
157 }
158}
159
160impl fmt::Display for CertificateChainKey {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 write!(f, "{}:{}", self.usage.as_str(), self.sequence.get())
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Hash)]
167pub struct DhttpSubjectKeyIdentifier {
168 chain: CertificateChainKey,
169 owner_hash: OwnerHash,
170}
171
172impl DhttpSubjectKeyIdentifier {
173 pub fn new(chain: CertificateChainKey, owner_hash: OwnerHash) -> Self {
174 Self { chain, owner_hash }
175 }
176
177 pub fn try_from_subject_key_identifier_bytes(
178 bytes: &[u8],
179 ) -> Result<Self, InvalidDhttpSubjectKeyIdentifier> {
180 let value =
181 str::from_utf8(bytes).context(invalid_dhttp_subject_key_identifier::Utf8Snafu)?;
182 value.parse()
183 }
184
185 pub fn chain(&self) -> &CertificateChainKey {
186 &self.chain
187 }
188
189 pub fn owner_hash(&self) -> &OwnerHash {
190 &self.owner_hash
191 }
192}
193
194#[derive(Debug, Snafu)]
195#[snafu(module)]
196pub enum InvalidDhttpSubjectKeyIdentifier {
197 #[snafu(display("dhttp subject key identifier is not utf-8"))]
198 Utf8 { source: str::Utf8Error },
199 #[snafu(display(
200 "dhttp subject key identifier must have sequence, kind, and owner hash fields"
201 ))]
202 FieldCount,
203 #[snafu(display("dhttp subject key identifier sequence is invalid"))]
204 Sequence { source: ParseIntError },
205 #[snafu(display("dhttp subject key identifier sequence is out of range"))]
206 SequenceRange { source: InvalidCertificateSequence },
207 #[snafu(display("dhttp subject key identifier kind flag is invalid"))]
208 KindFlag,
209 #[snafu(display("dhttp subject key identifier owner hash is invalid"))]
210 OwnerHash { source: InvalidOwnerHash },
211}
212
213impl FromStr for DhttpSubjectKeyIdentifier {
214 type Err = InvalidDhttpSubjectKeyIdentifier;
215
216 fn from_str(value: &str) -> Result<Self, Self::Err> {
217 let fields = value.split(':').collect::<Vec<_>>();
218 if fields.len() != DHTTP_SKI_FIELD_COUNT {
219 return invalid_dhttp_subject_key_identifier::FieldCountSnafu.fail();
220 }
221 let sequence = fields[0];
222 let usage = fields[1];
223 let owner_hash = fields[2];
224 let sequence = sequence
225 .parse::<u64>()
226 .context(invalid_dhttp_subject_key_identifier::SequenceSnafu)?;
227 let sequence = CertificateSequence::try_from(sequence)
228 .context(invalid_dhttp_subject_key_identifier::SequenceRangeSnafu)?;
229 let usage = match usage {
230 "0" => CertificateUsage::ClientAndServer,
231 "1" => CertificateUsage::ClientOnly,
232 _ => return invalid_dhttp_subject_key_identifier::KindFlagSnafu.fail(),
233 };
234 let owner_hash = OwnerHash::try_from(owner_hash)
235 .context(invalid_dhttp_subject_key_identifier::OwnerHashSnafu)?;
236
237 Ok(Self::new(
238 CertificateChainKey::new(sequence, usage),
239 owner_hash,
240 ))
241 }
242}
243
244impl fmt::Display for DhttpSubjectKeyIdentifier {
245 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
246 write!(
247 f,
248 "{}:{}:{}",
249 self.chain.sequence().get(),
250 self.chain.usage().kind_flag(),
251 self.owner_hash
252 )
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 const OWNER_HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
261
262 #[test]
263 fn certificate_sequence_accepts_database_compatible_range() {
264 assert_eq!(CertificateSequence::from(7u8).get(), 7);
265 assert_eq!(CertificateSequence::from(u16::MAX).get(), u16::MAX as u32);
266 assert_eq!(CertificateSequence::try_from(0u32).unwrap().get(), 0);
267 assert_eq!(
268 CertificateSequence::try_from(i32::MAX as u32)
269 .unwrap()
270 .get(),
271 i32::MAX as u32
272 );
273 assert_eq!(
274 CertificateSequence::try_from(i32::MAX as u64)
275 .unwrap()
276 .get(),
277 i32::MAX as u32
278 );
279 }
280
281 #[test]
282 fn certificate_sequence_rejects_values_outside_database_range() {
283 assert!(matches!(
284 CertificateSequence::try_from(-1),
285 Err(InvalidCertificateSequence::Negative)
286 ));
287 assert!(matches!(
288 CertificateSequence::try_from(i32::MAX as u32 + 1),
289 Err(InvalidCertificateSequence::OutOfRange { .. })
290 ));
291 assert!(matches!(
292 CertificateSequence::try_from(i32::MAX as u64 + 1),
293 Err(InvalidCertificateSequence::OutOfRange { .. })
294 ));
295 }
296
297 #[test]
298 fn certificate_chain_key_displays_user_facing_label() {
299 let primary = CertificateChainKey::new(
300 CertificateSequence::try_from(0u32).unwrap(),
301 CertificateUsage::ClientAndServer,
302 );
303 let secondary = CertificateChainKey::new(
304 CertificateSequence::try_from(2u32).unwrap(),
305 CertificateUsage::ClientOnly,
306 );
307
308 assert_eq!(primary.to_string(), "client and server:0");
309 assert_eq!(secondary.to_string(), "client:2");
310 }
311
312 #[test]
313 fn certificate_usage_preserves_certserver_kind_flags() {
314 assert_eq!(CertificateUsage::ClientAndServer.kind_flag(), "0");
315 assert_eq!(CertificateUsage::ClientOnly.kind_flag(), "1");
316 }
317
318 #[test]
319 fn rejects_out_of_range_subject_key_identifier_sequence() {
320 let error = format!("{}:0:{OWNER_HASH}", i32::MAX as u64 + 1)
321 .parse::<DhttpSubjectKeyIdentifier>()
322 .unwrap_err();
323
324 assert!(matches!(
325 error,
326 InvalidDhttpSubjectKeyIdentifier::SequenceRange { .. }
327 ));
328 }
329
330 #[test]
331 fn parses_canonical_dhttp_subject_key_identifier() {
332 let ski = DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes(
333 format!("7:0:{OWNER_HASH}").as_bytes(),
334 )
335 .unwrap();
336
337 assert_eq!(ski.chain().sequence().get(), 7);
338 assert_eq!(ski.chain().usage(), CertificateUsage::ClientAndServer);
339 assert_eq!(ski.owner_hash().as_str(), OWNER_HASH);
340 assert_eq!(ski.to_string(), format!("7:0:{OWNER_HASH}"));
341 }
342
343 #[test]
344 fn rejects_non_utf8_subject_key_identifier() {
345 let error =
346 DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes(&[0xff]).unwrap_err();
347
348 assert!(matches!(
349 error,
350 InvalidDhttpSubjectKeyIdentifier::Utf8 { .. }
351 ));
352 }
353
354 #[test]
355 fn rejects_wrong_field_count() {
356 let error = "0:1".parse::<DhttpSubjectKeyIdentifier>().unwrap_err();
357
358 assert!(matches!(
359 error,
360 InvalidDhttpSubjectKeyIdentifier::FieldCount
361 ));
362 }
363
364 #[test]
365 fn rejects_invalid_sequence() {
366 let error = format!("-1:0:{OWNER_HASH}")
367 .parse::<DhttpSubjectKeyIdentifier>()
368 .unwrap_err();
369
370 assert!(matches!(
371 error,
372 InvalidDhttpSubjectKeyIdentifier::Sequence { .. }
373 ));
374 }
375
376 #[test]
377 fn rejects_invalid_kind_flag() {
378 let error = format!("0:2:{OWNER_HASH}")
379 .parse::<DhttpSubjectKeyIdentifier>()
380 .unwrap_err();
381
382 assert!(matches!(error, InvalidDhttpSubjectKeyIdentifier::KindFlag));
383 }
384
385 #[test]
386 fn rejects_uppercase_owner_hash() {
387 let error = format!("0:0:{}", OWNER_HASH.to_ascii_uppercase())
388 .parse::<DhttpSubjectKeyIdentifier>()
389 .unwrap_err();
390
391 assert!(matches!(
392 error,
393 InvalidDhttpSubjectKeyIdentifier::OwnerHash { .. }
394 ));
395 }
396
397 #[test]
398 fn rejects_short_owner_hash() {
399 let error = "0:0:abc".parse::<DhttpSubjectKeyIdentifier>().unwrap_err();
400
401 assert!(matches!(
402 error,
403 InvalidDhttpSubjectKeyIdentifier::OwnerHash { .. }
404 ));
405 }
406}