use synta::tag::TAG_SEQUENCE;
use synta::{Boolean, Enumerated, Integer, ObjectIdentifier, OctetStringRef, RawDer, Tag, ToDer};
use crate::time_utils::parse_time;
use crate::{Extension, Time};
struct RevokedEntry {
serial: Integer,
revocation_date: Time,
extensions_der: Vec<u8>,
}
impl RevokedEntry {
fn encode(&self) -> Result<Vec<u8>, String> {
let mut enc = synta::Encoder::new(synta::Encoding::Der);
enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE))
.map_err(|e| format!("RevokedCertificate encode error: {e}"))?;
enc.encode(&self.serial)
.map_err(|e| format!("serial encode error: {e}"))?;
enc.encode(&self.revocation_date)
.map_err(|e| format!("revocationDate encode error: {e}"))?;
if !self.extensions_der.is_empty() {
enc.write_bytes(&self.extensions_der);
}
enc.end_constructed()
.map_err(|e| format!("RevokedCertificate end error: {e}"))?;
enc.finish()
.map_err(|e| format!("RevokedCertificate finish error: {e}"))
}
}
pub struct CertificateListBuilder {
issuer: Option<Vec<u8>>,
this_update: Option<Time>,
next_update: Option<Time>,
revoked: Vec<RevokedEntry>,
crl_extensions: Vec<(ObjectIdentifier, bool, Vec<u8>)>,
signature_algorithm_der: Option<Vec<u8>>,
error: Option<String>,
}
impl Default for CertificateListBuilder {
fn default() -> Self {
Self::new()
}
}
impl CertificateListBuilder {
pub fn new() -> Self {
Self {
issuer: None,
this_update: None,
next_update: None,
revoked: Vec::new(),
crl_extensions: Vec::new(),
signature_algorithm_der: None,
error: None,
}
}
pub fn issuer(mut self, name_der: &[u8]) -> Self {
if self.error.is_none() {
self.issuer = Some(name_der.to_vec());
}
self
}
pub fn this_update(mut self, time: &str) -> Self {
if self.error.is_none() {
match parse_time(time) {
Ok(t) => self.this_update = Some(t),
Err(e) => self.error = Some(e),
}
}
self
}
pub fn next_update(mut self, time: &str) -> Self {
if self.error.is_none() {
match parse_time(time) {
Ok(t) => self.next_update = Some(t),
Err(e) => self.error = Some(e),
}
}
self
}
pub fn revoke(mut self, serial: &[u8], revocation_date: &str, reason: Option<u8>) -> Self {
if self.error.is_some() {
return self;
}
let rev_date = match parse_time(revocation_date) {
Ok(t) => t,
Err(e) => {
self.error = Some(e);
return self;
}
};
let extensions_der = match reason.map(encode_reason_code_extension) {
Some(Ok(der)) => der,
Some(Err(e)) => {
self.error = Some(e);
return self;
}
None => Vec::new(),
};
self.revoked.push(RevokedEntry {
serial: Integer::from_unsigned_bytes(serial),
revocation_date: rev_date,
extensions_der,
});
self
}
pub fn add_crl_extension(
mut self,
oid_components: &[u32],
critical: bool,
value_der: &[u8],
) -> Self {
if self.error.is_some() {
return self;
}
match ObjectIdentifier::new(oid_components) {
Ok(oid) => self
.crl_extensions
.push((oid, critical, value_der.to_vec())),
Err(e) => self.error = Some(format!("invalid extension OID: {e}")),
}
self
}
pub fn signature_algorithm(mut self, alg_der: &[u8]) -> Self {
if self.error.is_none() {
self.signature_algorithm_der = Some(alg_der.to_vec());
}
self
}
pub fn build(self) -> Result<Vec<u8>, String> {
if let Some(e) = self.error {
return Err(e);
}
let issuer = self.issuer.ok_or("issuer not set")?;
let this_update = self.this_update.ok_or("this_update not set")?;
let sig_alg_der = self
.signature_algorithm_der
.ok_or("signature_algorithm not set")?;
let has_crl_extensions = !self.crl_extensions.is_empty();
let mut enc = synta::Encoder::new(synta::Encoding::Der);
enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE))
.map_err(|e| format!("TBSCertList start error: {e}"))?;
if has_crl_extensions {
enc.encode(&Integer::from_i64(1))
.map_err(|e| format!("version encode error: {e}"))?;
}
enc.write_bytes(&sig_alg_der);
enc.encode(&RawDer(&issuer))
.map_err(|e| format!("issuer encode error: {e}"))?;
enc.encode(&this_update)
.map_err(|e| format!("thisUpdate encode error: {e}"))?;
if let Some(next) = self.next_update {
enc.encode(&next)
.map_err(|e| format!("nextUpdate encode error: {e}"))?;
}
if !self.revoked.is_empty() {
enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE))
.map_err(|e| format!("revokedCertificates start error: {e}"))?;
for entry in &self.revoked {
let entry_der = entry.encode()?;
enc.write_bytes(&entry_der);
}
enc.end_constructed()
.map_err(|e| format!("revokedCertificates end error: {e}"))?;
}
if has_crl_extensions {
enc.start_constructed_no_guard(Tag::context_specific_constructed(0))
.map_err(|e| format!("crlExtensions [0] start error: {e}"))?;
enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE))
.map_err(|e| format!("crlExtensions SEQUENCE start error: {e}"))?;
for (oid, critical, value_bytes) in &self.crl_extensions {
let ext = Extension {
extn_id: oid.clone(),
critical: if *critical {
Some(Boolean::new(true))
} else {
None
},
extn_value: OctetStringRef::new(value_bytes),
};
enc.encode(&ext)
.map_err(|e| format!("crlExtension encode error: {e}"))?;
}
enc.end_constructed()
.map_err(|e| format!("crlExtensions SEQUENCE end error: {e}"))?;
enc.end_constructed()
.map_err(|e| format!("crlExtensions [0] end error: {e}"))?;
}
enc.end_constructed()
.map_err(|e| format!("TBSCertList end error: {e}"))?;
enc.finish()
.map_err(|e| format!("TBSCertList finish error: {e}"))
}
pub fn assemble(
tbs_der: &[u8],
sig_alg_der: &[u8],
signature: &[u8],
) -> Result<Vec<u8>, String> {
use synta::BitStringRef;
let mut enc = synta::Encoder::with_capacity(
synta::Encoding::Der,
tbs_der.len() + sig_alg_der.len() + signature.len() + 8,
);
enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE))
.map_err(|e| format!("CertificateList start error: {e}"))?;
enc.write_bytes(tbs_der);
enc.write_bytes(sig_alg_der);
let sig_bstr = BitStringRef::new(signature, 0)
.map_err(|e| format!("signature BIT STRING error: {e}"))?;
enc.encode(&sig_bstr)
.map_err(|e| format!("signature encode error: {e}"))?;
enc.end_constructed()
.map_err(|e| format!("CertificateList end error: {e}"))?;
enc.finish()
.map_err(|e| format!("CertificateList finish error: {e}"))
}
}
fn encode_reason_code_extension(reason: u8) -> Result<Vec<u8>, String> {
let enum_der = Enumerated::from_i32(reason as i32)
.to_der()
.map_err(|e| format!("reasonCode ENUMERATED encode error: {e}"))?;
let oid = ObjectIdentifier::new(crate::oids::CRL_REASON)
.map_err(|e| format!("CRL_REASON OID error: {e}"))?;
let ext = Extension {
extn_id: oid,
critical: None,
extn_value: OctetStringRef::new(&enum_der),
};
let mut enc = synta::Encoder::new(synta::Encoding::Der);
enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE))
.map_err(|e| format!("crlEntryExtensions SEQUENCE start error: {e}"))?;
enc.encode(&ext)
.map_err(|e| format!("reasonCode Extension encode error: {e}"))?;
enc.end_constructed()
.map_err(|e| format!("crlEntryExtensions SEQUENCE end error: {e}"))?;
enc.finish()
.map_err(|e| format!("crlEntryExtensions finish error: {e}"))
}
#[cfg(test)]
mod tests {
use super::*;
fn test_issuer_der() -> &'static [u8] {
&[
0x30, 0x0f, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x04, b'T', b'e', b's', b't', ]
}
fn test_alg_der() -> &'static [u8] {
&[
0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05,
0x00,
]
}
#[test]
fn build_and_roundtrip_minimal() {
let tbs = CertificateListBuilder::new()
.issuer(test_issuer_der())
.this_update("20240101120000Z")
.next_update("20250101120000Z")
.signature_algorithm(test_alg_der())
.build()
.expect("build should succeed");
let mut dec = synta::Decoder::new(&tbs, synta::Encoding::Der);
let _: crate::crl::TBSCertList<'_> = dec.decode().expect("round-trip decode failed");
}
#[test]
fn build_with_revoked_entry_and_reason() {
let serial = &[0x01u8];
let tbs = CertificateListBuilder::new()
.issuer(test_issuer_der())
.this_update("20240101120000Z")
.signature_algorithm(test_alg_der())
.revoke(serial, "20231201000000Z", Some(1)) .build()
.expect("build with revoked entry should succeed");
let mut dec = synta::Decoder::new(&tbs, synta::Encoding::Der);
let tbs_list: crate::crl::TBSCertList<'_> = dec.decode().expect("round-trip decode failed");
let revoked = tbs_list
.revoked_certificates
.expect("should have revoked entries");
assert_eq!(revoked.len(), 1);
}
#[test]
fn missing_fields_returns_error() {
let err = CertificateListBuilder::new().build();
assert!(err.is_err(), "expected error for missing fields");
let msg = err.unwrap_err();
assert!(
msg.contains("issuer")
|| msg.contains("this_update")
|| msg.contains("signature_algorithm"),
"unexpected error: {msg}"
);
}
#[test]
fn invalid_time_format_propagates() {
let result = CertificateListBuilder::new()
.issuer(test_issuer_der())
.this_update("not-a-time")
.signature_algorithm(test_alg_der())
.build();
assert!(result.is_err());
assert!(result.unwrap_err().contains("not-a-time"));
}
}