Skip to main content

dhttp_identity/
certificate.rs

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}