dhttp_identity/
certificate.rs1use 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}