use std::{
fmt,
num::ParseIntError,
str::{self, FromStr},
};
use snafu::{ResultExt, Snafu};
const DHTTP_SKI_FIELD_COUNT: usize = 3;
const OWNER_HASH_HEX_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CertificateSequence(u32);
impl CertificateSequence {
pub fn get(self) -> u32 {
self.0
}
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum InvalidCertificateSequence {
#[snafu(display("certificate sequence must be non-negative"))]
Negative,
}
impl TryFrom<i32> for CertificateSequence {
type Error = InvalidCertificateSequence;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if value < 0 {
return invalid_certificate_sequence::NegativeSnafu.fail();
}
Ok(Self(value as u32))
}
}
impl From<u32> for CertificateSequence {
fn from(value: u32) -> Self {
Self(value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum CertificateChainKind {
Primary,
Secondary,
}
impl CertificateChainKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Primary => "primary",
Self::Secondary => "secondary",
}
}
pub fn kind_flag(self) -> &'static str {
match self {
Self::Primary => "0",
Self::Secondary => "1",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct OwnerHash(String);
impl OwnerHash {
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum InvalidOwnerHash {
#[snafu(display("owner hash must be 64 lowercase hexadecimal characters"))]
Invalid,
}
impl TryFrom<&str> for OwnerHash {
type Error = InvalidOwnerHash;
fn try_from(value: &str) -> Result<Self, Self::Error> {
if value.len() == OWNER_HASH_HEX_LEN
&& value
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
Ok(Self(value.to_owned()))
} else {
invalid_owner_hash::InvalidSnafu.fail()
}
}
}
impl fmt::Display for OwnerHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct CertificateChainKey {
sequence: CertificateSequence,
kind: CertificateChainKind,
}
impl CertificateChainKey {
pub fn new(sequence: CertificateSequence, kind: CertificateChainKind) -> Self {
Self { sequence, kind }
}
pub fn sequence(&self) -> CertificateSequence {
self.sequence
}
pub fn kind(&self) -> CertificateChainKind {
self.kind
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DhttpSubjectKeyIdentifier {
chain: CertificateChainKey,
owner_hash: OwnerHash,
}
impl DhttpSubjectKeyIdentifier {
pub fn new(chain: CertificateChainKey, owner_hash: OwnerHash) -> Self {
Self { chain, owner_hash }
}
pub fn try_from_subject_key_identifier_bytes(
bytes: &[u8],
) -> Result<Self, InvalidDhttpSubjectKeyIdentifier> {
let value =
str::from_utf8(bytes).context(invalid_dhttp_subject_key_identifier::Utf8Snafu)?;
value.parse()
}
pub fn chain(&self) -> &CertificateChainKey {
&self.chain
}
pub fn owner_hash(&self) -> &OwnerHash {
&self.owner_hash
}
}
#[derive(Debug, Snafu)]
#[snafu(module)]
pub enum InvalidDhttpSubjectKeyIdentifier {
#[snafu(display("dhttp subject key identifier is not utf-8"))]
Utf8 { source: str::Utf8Error },
#[snafu(display(
"dhttp subject key identifier must have sequence, kind, and owner hash fields"
))]
FieldCount,
#[snafu(display("dhttp subject key identifier sequence is invalid"))]
Sequence { source: ParseIntError },
#[snafu(display("dhttp subject key identifier kind flag is invalid"))]
KindFlag,
#[snafu(display("dhttp subject key identifier owner hash is invalid"))]
OwnerHash { source: InvalidOwnerHash },
}
impl FromStr for DhttpSubjectKeyIdentifier {
type Err = InvalidDhttpSubjectKeyIdentifier;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let fields = value.split(':').collect::<Vec<_>>();
if fields.len() != DHTTP_SKI_FIELD_COUNT {
return invalid_dhttp_subject_key_identifier::FieldCountSnafu.fail();
}
let sequence = fields[0];
let kind = fields[1];
let owner_hash = fields[2];
let sequence = sequence
.parse::<u32>()
.context(invalid_dhttp_subject_key_identifier::SequenceSnafu)?;
let kind = match kind {
"0" => CertificateChainKind::Primary,
"1" => CertificateChainKind::Secondary,
_ => return invalid_dhttp_subject_key_identifier::KindFlagSnafu.fail(),
};
let owner_hash = OwnerHash::try_from(owner_hash)
.context(invalid_dhttp_subject_key_identifier::OwnerHashSnafu)?;
Ok(Self::new(
CertificateChainKey::new(CertificateSequence::from(sequence), kind),
owner_hash,
))
}
}
impl fmt::Display for DhttpSubjectKeyIdentifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}:{}",
self.chain.sequence().get(),
self.chain.kind().kind_flag(),
self.owner_hash
)
}
}
#[cfg(test)]
mod tests {
use super::*;
const OWNER_HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
#[test]
fn parses_canonical_dhttp_subject_key_identifier() {
let ski = DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes(
format!("7:1:{OWNER_HASH}").as_bytes(),
)
.unwrap();
assert_eq!(ski.chain().sequence().get(), 7);
assert_eq!(ski.chain().kind(), CertificateChainKind::Secondary);
assert_eq!(ski.owner_hash().as_str(), OWNER_HASH);
assert_eq!(ski.to_string(), format!("7:1:{OWNER_HASH}"));
}
#[test]
fn rejects_non_utf8_subject_key_identifier() {
let error =
DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes(&[0xff]).unwrap_err();
assert!(matches!(
error,
InvalidDhttpSubjectKeyIdentifier::Utf8 { .. }
));
}
#[test]
fn rejects_wrong_field_count() {
let error = "0:1".parse::<DhttpSubjectKeyIdentifier>().unwrap_err();
assert!(matches!(
error,
InvalidDhttpSubjectKeyIdentifier::FieldCount
));
}
#[test]
fn rejects_invalid_sequence() {
let error = format!("-1:0:{OWNER_HASH}")
.parse::<DhttpSubjectKeyIdentifier>()
.unwrap_err();
assert!(matches!(
error,
InvalidDhttpSubjectKeyIdentifier::Sequence { .. }
));
}
#[test]
fn rejects_invalid_kind_flag() {
let error = format!("0:2:{OWNER_HASH}")
.parse::<DhttpSubjectKeyIdentifier>()
.unwrap_err();
assert!(matches!(error, InvalidDhttpSubjectKeyIdentifier::KindFlag));
}
#[test]
fn rejects_uppercase_owner_hash() {
let error = format!("0:0:{}", OWNER_HASH.to_ascii_uppercase())
.parse::<DhttpSubjectKeyIdentifier>()
.unwrap_err();
assert!(matches!(
error,
InvalidDhttpSubjectKeyIdentifier::OwnerHash { .. }
));
}
#[test]
fn rejects_short_owner_hash() {
let error = "0:0:abc".parse::<DhttpSubjectKeyIdentifier>().unwrap_err();
assert!(matches!(
error,
InvalidDhttpSubjectKeyIdentifier::OwnerHash { .. }
));
}
}