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 fn get(self) -> u32 {
17        self.0
18    }
19}
20
21#[derive(Debug, Snafu)]
22#[snafu(module)]
23pub enum InvalidCertificateSequence {
24    #[snafu(display("certificate sequence must be non-negative"))]
25    Negative,
26}
27
28impl TryFrom<i32> for CertificateSequence {
29    type Error = InvalidCertificateSequence;
30
31    fn try_from(value: i32) -> Result<Self, Self::Error> {
32        if value < 0 {
33            return invalid_certificate_sequence::NegativeSnafu.fail();
34        }
35        Ok(Self(value as u32))
36    }
37}
38
39impl From<u32> for CertificateSequence {
40    fn from(value: u32) -> Self {
41        Self(value)
42    }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
46pub enum CertificateChainKind {
47    Primary,
48    Secondary,
49}
50
51impl CertificateChainKind {
52    pub fn as_str(self) -> &'static str {
53        match self {
54            Self::Primary => "primary",
55            Self::Secondary => "secondary",
56        }
57    }
58
59    pub fn kind_flag(self) -> &'static str {
60        match self {
61            Self::Primary => "0",
62            Self::Secondary => "1",
63        }
64    }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
68pub struct OwnerHash(String);
69
70impl OwnerHash {
71    pub fn as_str(&self) -> &str {
72        &self.0
73    }
74}
75
76#[derive(Debug, Snafu)]
77#[snafu(module)]
78pub enum InvalidOwnerHash {
79    #[snafu(display("owner hash must be 64 lowercase hexadecimal characters"))]
80    Invalid,
81}
82
83impl TryFrom<&str> for OwnerHash {
84    type Error = InvalidOwnerHash;
85
86    fn try_from(value: &str) -> Result<Self, Self::Error> {
87        if value.len() == OWNER_HASH_HEX_LEN
88            && value
89                .bytes()
90                .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
91        {
92            Ok(Self(value.to_owned()))
93        } else {
94            invalid_owner_hash::InvalidSnafu.fail()
95        }
96    }
97}
98
99impl fmt::Display for OwnerHash {
100    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101        f.write_str(&self.0)
102    }
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Hash)]
106pub struct CertificateChainKey {
107    sequence: CertificateSequence,
108    kind: CertificateChainKind,
109}
110
111impl CertificateChainKey {
112    pub fn new(sequence: CertificateSequence, kind: CertificateChainKind) -> Self {
113        Self { sequence, kind }
114    }
115
116    pub fn sequence(&self) -> CertificateSequence {
117        self.sequence
118    }
119
120    pub fn kind(&self) -> CertificateChainKind {
121        self.kind
122    }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Hash)]
126pub struct DhttpSubjectKeyIdentifier {
127    chain: CertificateChainKey,
128    owner_hash: OwnerHash,
129}
130
131impl DhttpSubjectKeyIdentifier {
132    pub fn new(chain: CertificateChainKey, owner_hash: OwnerHash) -> Self {
133        Self { chain, owner_hash }
134    }
135
136    pub fn try_from_subject_key_identifier_bytes(
137        bytes: &[u8],
138    ) -> Result<Self, InvalidDhttpSubjectKeyIdentifier> {
139        let value =
140            str::from_utf8(bytes).context(invalid_dhttp_subject_key_identifier::Utf8Snafu)?;
141        value.parse()
142    }
143
144    pub fn chain(&self) -> &CertificateChainKey {
145        &self.chain
146    }
147
148    pub fn owner_hash(&self) -> &OwnerHash {
149        &self.owner_hash
150    }
151}
152
153#[derive(Debug, Snafu)]
154#[snafu(module)]
155pub enum InvalidDhttpSubjectKeyIdentifier {
156    #[snafu(display("dhttp subject key identifier is not utf-8"))]
157    Utf8 { source: str::Utf8Error },
158    #[snafu(display(
159        "dhttp subject key identifier must have sequence, kind, and owner hash fields"
160    ))]
161    FieldCount,
162    #[snafu(display("dhttp subject key identifier sequence is invalid"))]
163    Sequence { source: ParseIntError },
164    #[snafu(display("dhttp subject key identifier kind flag is invalid"))]
165    KindFlag,
166    #[snafu(display("dhttp subject key identifier owner hash is invalid"))]
167    OwnerHash { source: InvalidOwnerHash },
168}
169
170impl FromStr for DhttpSubjectKeyIdentifier {
171    type Err = InvalidDhttpSubjectKeyIdentifier;
172
173    fn from_str(value: &str) -> Result<Self, Self::Err> {
174        let fields = value.split(':').collect::<Vec<_>>();
175        if fields.len() != DHTTP_SKI_FIELD_COUNT {
176            return invalid_dhttp_subject_key_identifier::FieldCountSnafu.fail();
177        }
178        let sequence = fields[0];
179        let kind = fields[1];
180        let owner_hash = fields[2];
181        let sequence = sequence
182            .parse::<u32>()
183            .context(invalid_dhttp_subject_key_identifier::SequenceSnafu)?;
184        let kind = match kind {
185            "0" => CertificateChainKind::Primary,
186            "1" => CertificateChainKind::Secondary,
187            _ => return invalid_dhttp_subject_key_identifier::KindFlagSnafu.fail(),
188        };
189        let owner_hash = OwnerHash::try_from(owner_hash)
190            .context(invalid_dhttp_subject_key_identifier::OwnerHashSnafu)?;
191
192        Ok(Self::new(
193            CertificateChainKey::new(CertificateSequence::from(sequence), kind),
194            owner_hash,
195        ))
196    }
197}
198
199impl fmt::Display for DhttpSubjectKeyIdentifier {
200    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201        write!(
202            f,
203            "{}:{}:{}",
204            self.chain.sequence().get(),
205            self.chain.kind().kind_flag(),
206            self.owner_hash
207        )
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    const OWNER_HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
216
217    #[test]
218    fn parses_canonical_dhttp_subject_key_identifier() {
219        let ski = DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes(
220            format!("7:1:{OWNER_HASH}").as_bytes(),
221        )
222        .unwrap();
223
224        assert_eq!(ski.chain().sequence().get(), 7);
225        assert_eq!(ski.chain().kind(), CertificateChainKind::Secondary);
226        assert_eq!(ski.owner_hash().as_str(), OWNER_HASH);
227        assert_eq!(ski.to_string(), format!("7:1:{OWNER_HASH}"));
228    }
229
230    #[test]
231    fn rejects_non_utf8_subject_key_identifier() {
232        let error =
233            DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes(&[0xff]).unwrap_err();
234
235        assert!(matches!(
236            error,
237            InvalidDhttpSubjectKeyIdentifier::Utf8 { .. }
238        ));
239    }
240
241    #[test]
242    fn rejects_wrong_field_count() {
243        let error = "0:1".parse::<DhttpSubjectKeyIdentifier>().unwrap_err();
244
245        assert!(matches!(
246            error,
247            InvalidDhttpSubjectKeyIdentifier::FieldCount
248        ));
249    }
250
251    #[test]
252    fn rejects_invalid_sequence() {
253        let error = format!("-1:0:{OWNER_HASH}")
254            .parse::<DhttpSubjectKeyIdentifier>()
255            .unwrap_err();
256
257        assert!(matches!(
258            error,
259            InvalidDhttpSubjectKeyIdentifier::Sequence { .. }
260        ));
261    }
262
263    #[test]
264    fn rejects_invalid_kind_flag() {
265        let error = format!("0:2:{OWNER_HASH}")
266            .parse::<DhttpSubjectKeyIdentifier>()
267            .unwrap_err();
268
269        assert!(matches!(error, InvalidDhttpSubjectKeyIdentifier::KindFlag));
270    }
271
272    #[test]
273    fn rejects_uppercase_owner_hash() {
274        let error = format!("0:0:{}", OWNER_HASH.to_ascii_uppercase())
275            .parse::<DhttpSubjectKeyIdentifier>()
276            .unwrap_err();
277
278        assert!(matches!(
279            error,
280            InvalidDhttpSubjectKeyIdentifier::OwnerHash { .. }
281        ));
282    }
283
284    #[test]
285    fn rejects_short_owner_hash() {
286        let error = "0:0:abc".parse::<DhttpSubjectKeyIdentifier>().unwrap_err();
287
288        assert!(matches!(
289            error,
290            InvalidDhttpSubjectKeyIdentifier::OwnerHash { .. }
291        ));
292    }
293}