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 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}