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 CertificateChainKind {
82 Primary,
83 Secondary,
84}
85
86impl CertificateChainKind {
87 pub fn as_str(self) -> &'static str {
88 match self {
89 Self::Primary => "primary",
90 Self::Secondary => "secondary",
91 }
92 }
93
94 pub fn kind_flag(self) -> &'static str {
95 match self {
96 Self::Primary => "0",
97 Self::Secondary => "1",
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 kind: CertificateChainKind,
144}
145
146impl CertificateChainKey {
147 pub fn new(sequence: CertificateSequence, kind: CertificateChainKind) -> Self {
148 Self { sequence, kind }
149 }
150
151 pub fn sequence(&self) -> CertificateSequence {
152 self.sequence
153 }
154
155 pub fn kind(&self) -> CertificateChainKind {
156 self.kind
157 }
158}
159
160impl fmt::Display for CertificateChainKey {
161 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
162 write!(f, "{}:{}", self.kind.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 kind = 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 kind = match kind {
230 "0" => CertificateChainKind::Primary,
231 "1" => CertificateChainKind::Secondary,
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, kind),
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.kind().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 CertificateChainKind::Primary,
302 );
303 let secondary = CertificateChainKey::new(
304 CertificateSequence::try_from(2u32).unwrap(),
305 CertificateChainKind::Secondary,
306 );
307
308 assert_eq!(primary.to_string(), "primary:0");
309 assert_eq!(secondary.to_string(), "secondary:2");
310 }
311
312 #[test]
313 fn rejects_out_of_range_subject_key_identifier_sequence() {
314 let error = format!("{}:0:{OWNER_HASH}", i32::MAX as u64 + 1)
315 .parse::<DhttpSubjectKeyIdentifier>()
316 .unwrap_err();
317
318 assert!(matches!(
319 error,
320 InvalidDhttpSubjectKeyIdentifier::SequenceRange { .. }
321 ));
322 }
323
324 #[test]
325 fn parses_canonical_dhttp_subject_key_identifier() {
326 let ski = DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes(
327 format!("7:1:{OWNER_HASH}").as_bytes(),
328 )
329 .unwrap();
330
331 assert_eq!(ski.chain().sequence().get(), 7);
332 assert_eq!(ski.chain().kind(), CertificateChainKind::Secondary);
333 assert_eq!(ski.owner_hash().as_str(), OWNER_HASH);
334 assert_eq!(ski.to_string(), format!("7:1:{OWNER_HASH}"));
335 }
336
337 #[test]
338 fn rejects_non_utf8_subject_key_identifier() {
339 let error =
340 DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes(&[0xff]).unwrap_err();
341
342 assert!(matches!(
343 error,
344 InvalidDhttpSubjectKeyIdentifier::Utf8 { .. }
345 ));
346 }
347
348 #[test]
349 fn rejects_wrong_field_count() {
350 let error = "0:1".parse::<DhttpSubjectKeyIdentifier>().unwrap_err();
351
352 assert!(matches!(
353 error,
354 InvalidDhttpSubjectKeyIdentifier::FieldCount
355 ));
356 }
357
358 #[test]
359 fn rejects_invalid_sequence() {
360 let error = format!("-1:0:{OWNER_HASH}")
361 .parse::<DhttpSubjectKeyIdentifier>()
362 .unwrap_err();
363
364 assert!(matches!(
365 error,
366 InvalidDhttpSubjectKeyIdentifier::Sequence { .. }
367 ));
368 }
369
370 #[test]
371 fn rejects_invalid_kind_flag() {
372 let error = format!("0:2:{OWNER_HASH}")
373 .parse::<DhttpSubjectKeyIdentifier>()
374 .unwrap_err();
375
376 assert!(matches!(error, InvalidDhttpSubjectKeyIdentifier::KindFlag));
377 }
378
379 #[test]
380 fn rejects_uppercase_owner_hash() {
381 let error = format!("0:0:{}", OWNER_HASH.to_ascii_uppercase())
382 .parse::<DhttpSubjectKeyIdentifier>()
383 .unwrap_err();
384
385 assert!(matches!(
386 error,
387 InvalidDhttpSubjectKeyIdentifier::OwnerHash { .. }
388 ));
389 }
390
391 #[test]
392 fn rejects_short_owner_hash() {
393 let error = "0:0:abc".parse::<DhttpSubjectKeyIdentifier>().unwrap_err();
394
395 assert!(matches!(
396 error,
397 InvalidDhttpSubjectKeyIdentifier::OwnerHash { .. }
398 ));
399 }
400}