From 3012bf9c1448848c02b408bb4db3d4ce0f923f49 Mon Sep 17 00:00:00 2001 From: eareimu Date: Tue, 16 Jun 2026 21:56:57 +0800 Subject: [PATCH 1/2] feat: constrain certificate chain sequence --- identity/src/certificate.rs | 119 ++++++++++++++++++++++++++++++++++-- 1 file changed, 113 insertions(+), 6 deletions(-) diff --git a/identity/src/certificate.rs b/identity/src/certificate.rs index 76b7b01..42358a4 100644 --- a/identity/src/certificate.rs +++ b/identity/src/certificate.rs @@ -13,6 +13,8 @@ const OWNER_HASH_HEX_LEN: usize = 64; pub struct CertificateSequence(u32); impl CertificateSequence { + pub const MAX: u32 = i32::MAX as u32; + pub fn get(self) -> u32 { self.0 } @@ -23,6 +25,34 @@ impl CertificateSequence { pub enum InvalidCertificateSequence { #[snafu(display("certificate sequence must be non-negative"))] Negative, + #[snafu(display("certificate sequence exceeds supported database range"))] + OutOfRange { value: u64 }, +} + +impl From for CertificateSequence { + fn from(value: u8) -> Self { + Self(value as u32) + } +} + +impl From for CertificateSequence { + fn from(value: u16) -> Self { + Self(value as u32) + } +} + +impl TryFrom for CertificateSequence { + type Error = InvalidCertificateSequence; + + fn try_from(value: u32) -> Result { + if value > Self::MAX { + return invalid_certificate_sequence::OutOfRangeSnafu { + value: value as u64, + } + .fail(); + } + Ok(Self(value)) + } } impl TryFrom for CertificateSequence { @@ -32,13 +62,18 @@ impl TryFrom for CertificateSequence { if value < 0 { return invalid_certificate_sequence::NegativeSnafu.fail(); } - Ok(Self(value as u32)) + Self::try_from(value as u32) } } -impl From for CertificateSequence { - fn from(value: u32) -> Self { - Self(value) +impl TryFrom for CertificateSequence { + type Error = InvalidCertificateSequence; + + fn try_from(value: u64) -> Result { + if value > Self::MAX as u64 { + return invalid_certificate_sequence::OutOfRangeSnafu { value }.fail(); + } + Ok(Self(value as u32)) } } @@ -122,6 +157,12 @@ impl CertificateChainKey { } } +impl fmt::Display for CertificateChainKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}:{}", self.kind.as_str(), self.sequence.get()) + } +} + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct DhttpSubjectKeyIdentifier { chain: CertificateChainKey, @@ -161,6 +202,8 @@ pub enum InvalidDhttpSubjectKeyIdentifier { FieldCount, #[snafu(display("dhttp subject key identifier sequence is invalid"))] Sequence { source: ParseIntError }, + #[snafu(display("dhttp subject key identifier sequence is out of range"))] + SequenceRange { source: InvalidCertificateSequence }, #[snafu(display("dhttp subject key identifier kind flag is invalid"))] KindFlag, #[snafu(display("dhttp subject key identifier owner hash is invalid"))] @@ -179,8 +222,10 @@ impl FromStr for DhttpSubjectKeyIdentifier { let kind = fields[1]; let owner_hash = fields[2]; let sequence = sequence - .parse::() + .parse::() .context(invalid_dhttp_subject_key_identifier::SequenceSnafu)?; + let sequence = CertificateSequence::try_from(sequence) + .context(invalid_dhttp_subject_key_identifier::SequenceRangeSnafu)?; let kind = match kind { "0" => CertificateChainKind::Primary, "1" => CertificateChainKind::Secondary, @@ -190,7 +235,7 @@ impl FromStr for DhttpSubjectKeyIdentifier { .context(invalid_dhttp_subject_key_identifier::OwnerHashSnafu)?; Ok(Self::new( - CertificateChainKey::new(CertificateSequence::from(sequence), kind), + CertificateChainKey::new(sequence, kind), owner_hash, )) } @@ -214,6 +259,68 @@ mod tests { const OWNER_HASH: &str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + #[test] + fn certificate_sequence_accepts_database_compatible_range() { + assert_eq!(CertificateSequence::from(7u8).get(), 7); + assert_eq!(CertificateSequence::from(u16::MAX).get(), u16::MAX as u32); + assert_eq!(CertificateSequence::try_from(0u32).unwrap().get(), 0); + assert_eq!( + CertificateSequence::try_from(i32::MAX as u32) + .unwrap() + .get(), + i32::MAX as u32 + ); + assert_eq!( + CertificateSequence::try_from(i32::MAX as u64) + .unwrap() + .get(), + i32::MAX as u32 + ); + } + + #[test] + fn certificate_sequence_rejects_values_outside_database_range() { + assert!(matches!( + CertificateSequence::try_from(-1), + Err(InvalidCertificateSequence::Negative) + )); + assert!(matches!( + CertificateSequence::try_from(i32::MAX as u32 + 1), + Err(InvalidCertificateSequence::OutOfRange { .. }) + )); + assert!(matches!( + CertificateSequence::try_from(i32::MAX as u64 + 1), + Err(InvalidCertificateSequence::OutOfRange { .. }) + )); + } + + #[test] + fn certificate_chain_key_displays_user_facing_label() { + let primary = CertificateChainKey::new( + CertificateSequence::try_from(0u32).unwrap(), + CertificateChainKind::Primary, + ); + let secondary = CertificateChainKey::new( + CertificateSequence::try_from(2u32).unwrap(), + CertificateChainKind::Secondary, + ); + + assert_eq!(primary.to_string(), "primary:0"); + assert_eq!(secondary.to_string(), "secondary:2"); + } + + #[test] + fn rejects_out_of_range_subject_key_identifier_sequence() { + let error = format!("{}:0:{OWNER_HASH}", i32::MAX as u64 + 1) + .parse::() + .unwrap_err(); + + assert!(matches!( + error, + InvalidDhttpSubjectKeyIdentifier::SequenceRange { .. } + )); + } + #[test] fn parses_canonical_dhttp_subject_key_identifier() { let ski = DhttpSubjectKeyIdentifier::try_from_subject_key_identifier_bytes( From bb8a909534e3878ebec93f87448446d2ddb17921 Mon Sep 17 00:00:00 2001 From: eareimu Date: Wed, 17 Jun 2026 01:45:07 +0800 Subject: [PATCH 2/2] release: stage dhttp-identity v0.2.0 --- identity/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/identity/Cargo.toml b/identity/Cargo.toml index 6c7f46c..44f607b 100644 --- a/identity/Cargo.toml +++ b/identity/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "dhttp-identity" description = "Identity primitives for DHttp" -version.workspace = true +version = "0.2.0" edition.workspace = true license.workspace = true repository.workspace = true