use synta::{
BitStringRef, Decoder, Encoding, GeneralizedTime, Integer, Null, ObjectIdentifier,
OctetStringRef,
};
use crate::ocsp::{
BasicOCSPResponse, CertID, CertStatus, OCSPResponse, OCSPResponseStatus, ResponseBytes,
ResponseData, RevokedInfo, SingleResponse, ID_PKIX_OCSP_BASIC,
};
use crate::time_utils::parse_generalized_time;
use crate::{AlgorithmIdentifier, Name};
pub struct SingleResponseSpec<'a> {
pub hash_algorithm_der: &'a [u8],
pub issuer_name_hash: &'a [u8],
pub issuer_key_hash: &'a [u8],
pub serial: &'a [u8],
pub status: u8,
pub this_update: &'a str,
pub next_update: Option<&'a str>,
}
struct PendingSingleResponse {
hash_algorithm_der: Vec<u8>,
issuer_name_hash: Vec<u8>,
issuer_key_hash: Vec<u8>,
serial: Vec<u8>,
status: u8,
this_update: GeneralizedTime,
next_update: Option<GeneralizedTime>,
}
struct ResponseByteStorage {
hash_algorithm_der: Vec<u8>,
issuer_name_hash: Vec<u8>,
issuer_key_hash: Vec<u8>,
serial: Vec<u8>,
}
pub struct OCSPResponseBuilder {
responder_name_der: Option<Vec<u8>>,
responder_key_hash: Option<Vec<u8>>,
produced_at: Option<GeneralizedTime>,
responses: Vec<PendingSingleResponse>,
error: Option<String>,
}
impl Default for OCSPResponseBuilder {
fn default() -> Self {
Self::new()
}
}
impl OCSPResponseBuilder {
pub fn new() -> Self {
Self {
responder_name_der: None,
responder_key_hash: None,
produced_at: None,
responses: Vec::new(),
error: None,
}
}
pub fn responder_name(mut self, name_der: &[u8]) -> Self {
if self.error.is_some() {
return self;
}
match Decoder::new(name_der, Encoding::Der).decode::<Name<'_>>() {
Ok(_) => self.responder_name_der = Some(name_der.to_vec()),
Err(e) => self.error = Some(format!("invalid responder Name DER: {e}")),
}
self
}
pub fn responder_key_hash(mut self, key_hash: &[u8]) -> Self {
if self.error.is_none() {
self.responder_key_hash = Some(key_hash.to_vec());
}
self
}
pub fn produced_at(mut self, time: &str) -> Self {
if self.error.is_none() {
match parse_generalized_time(time) {
Ok(t) => self.produced_at = Some(t),
Err(e) => self.error = Some(e),
}
}
self
}
pub fn add_response(mut self, spec: SingleResponseSpec<'_>) -> Self {
if self.error.is_some() {
return self;
}
if let Err(e) =
Decoder::new(spec.hash_algorithm_der, Encoding::Der).decode::<AlgorithmIdentifier<'_>>()
{
self.error = Some(format!("invalid hash_algorithm_der: {e}"));
return self;
}
let this = match parse_generalized_time(spec.this_update) {
Ok(t) => t,
Err(e) => {
self.error = Some(e);
return self;
}
};
let next = if let Some(s) = spec.next_update {
match parse_generalized_time(s) {
Ok(t) => Some(t),
Err(e) => {
self.error = Some(e);
return self;
}
}
} else {
None
};
if spec.status > 2 {
self.error = Some(format!(
"invalid status {}: must be 0, 1, or 2",
spec.status
));
return self;
}
self.responses.push(PendingSingleResponse {
hash_algorithm_der: spec.hash_algorithm_der.to_vec(),
issuer_name_hash: spec.issuer_name_hash.to_vec(),
issuer_key_hash: spec.issuer_key_hash.to_vec(),
serial: spec.serial.to_vec(),
status: spec.status,
this_update: this,
next_update: next,
});
self
}
pub fn build_tbs(self) -> Result<Vec<u8>, String> {
if let Some(e) = self.error {
return Err(e);
}
let produced_at = self.produced_at.ok_or("produced_at not set")?;
let mut response_storage: Vec<ResponseByteStorage> =
Vec::with_capacity(self.responses.len());
for r in &self.responses {
response_storage.push(ResponseByteStorage {
hash_algorithm_der: r.hash_algorithm_der.clone(),
issuer_name_hash: r.issuer_name_hash.clone(),
issuer_key_hash: r.issuer_key_hash.clone(),
serial: r.serial.clone(),
});
}
let mut single_responses: Vec<SingleResponse<'_>> =
Vec::with_capacity(self.responses.len());
for (r, storage) in self.responses.iter().zip(response_storage.iter()) {
let hash_algorithm: AlgorithmIdentifier<'_> =
Decoder::new(&storage.hash_algorithm_der, Encoding::Der)
.decode()
.map_err(|e| format!("re-decode hash_algorithm_der failed: {e}"))?;
let cert_id = CertID {
hash_algorithm,
issuer_name_hash: OctetStringRef::new(&storage.issuer_name_hash),
issuer_key_hash: OctetStringRef::new(&storage.issuer_key_hash),
serial_number: Integer::from_unsigned_bytes(&storage.serial),
};
let cert_status = match r.status {
0 => CertStatus::Good(Null),
2 => CertStatus::Unknown(Null),
_ => {
CertStatus::Revoked(RevokedInfo {
revocation_time: r.this_update.clone(),
revocation_reason: None,
})
}
};
single_responses.push(SingleResponse {
cert_id,
cert_status,
this_update: r.this_update.clone(),
next_update: r.next_update.clone(),
single_extensions: None,
});
}
let responder_id = if let Some(name_der) = &self.responder_name_der {
let name: Name<'_> = Decoder::new(name_der, Encoding::Der)
.decode()
.map_err(|e| format!("re-decode responder Name failed: {e}"))?;
crate::ocsp::ResponderID::ByName(name)
} else if let Some(key_hash) = &self.responder_key_hash {
crate::ocsp::ResponderID::ByKey(OctetStringRef::new(key_hash))
} else {
return Err("responder not set: call responder_name() or responder_key_hash()".into());
};
let response_data = ResponseData {
version: None,
responder_id,
produced_at,
responses: single_responses,
response_extensions: None,
};
response_data
.to_der()
.map_err(|e| format!("ResponseData encode error: {e}"))
}
pub fn assemble(
tbs_der: &[u8],
sig_alg_der: &[u8],
signature: &[u8],
) -> Result<Vec<u8>, String> {
let tbs: ResponseData<'_> = Decoder::new(tbs_der, Encoding::Der)
.decode()
.map_err(|e| format!("assemble: tbs_der decode error: {e}"))?;
let sig_algorithm: AlgorithmIdentifier<'_> = Decoder::new(sig_alg_der, Encoding::Der)
.decode()
.map_err(|e| format!("assemble: sig_alg_der decode error: {e}"))?;
let sig_bstr = BitStringRef::new(signature, 0)
.map_err(|e| format!("assemble: signature BIT STRING error: {e}"))?;
let basic = BasicOCSPResponse {
tbs_response_data: tbs,
signature_algorithm: sig_algorithm,
signature: sig_bstr,
certs: None,
};
let basic_der = basic
.to_der()
.map_err(|e| format!("BasicOCSPResponse encode error: {e}"))?;
let response_type = ObjectIdentifier::new(ID_PKIX_OCSP_BASIC)
.map_err(|e| format!("id-pkix-ocsp-basic OID error: {e}"))?;
let response_octet = OctetStringRef::new(&basic_der);
let response_bytes = ResponseBytes {
response_type,
response: response_octet,
};
let ocsp_response = OCSPResponse {
response_status: OCSPResponseStatus::Successful,
response_bytes: Some(response_bytes),
};
ocsp_response
.to_der()
.map_err(|e| format!("OCSPResponse encode error: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sha1_alg_der() -> &'static [u8] {
&[
0x30, 0x09, 0x06, 0x05, 0x2b, 0x0e, 0x03, 0x02, 0x1a, 0x05, 0x00,
]
}
fn sha256_rsa_alg_der() -> &'static [u8] {
&[
0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b, 0x05,
0x00,
]
}
fn test_name_der() -> &'static [u8] {
&[
0x30, 0x0f, 0x31, 0x0d, 0x30, 0x0b, 0x06, 0x03, 0x55, 0x04, 0x03, 0x0c, 0x04, b'T', b'e', b's', b't', ]
}
#[test]
fn build_and_assemble_minimal() {
let key_hash = [0x01u8; 20]; let name_hash = [0x02u8; 20];
let serial = [0x01u8];
let fake_sig = [0xdeu8; 32];
let tbs = OCSPResponseBuilder::new()
.responder_key_hash(&key_hash)
.produced_at("20240101120000Z")
.add_response(SingleResponseSpec {
hash_algorithm_der: sha1_alg_der(),
issuer_name_hash: &name_hash,
issuer_key_hash: &key_hash,
serial: &serial,
status: 0, this_update: "20240101120000Z",
next_update: Some("20240201120000Z"),
})
.build_tbs()
.expect("build_tbs should succeed");
assert!(!tbs.is_empty(), "TBS DER must not be empty");
let ocsp_der = OCSPResponseBuilder::assemble(&tbs, sha256_rsa_alg_der(), &fake_sig)
.expect("assemble should succeed");
let mut dec = Decoder::new(&ocsp_der, Encoding::Der);
let resp: OCSPResponse<'_> = dec.decode().expect("OCSPResponse round-trip decode failed");
assert_eq!(resp.response_status, OCSPResponseStatus::Successful);
let rb = resp.response_bytes.expect("responseBytes must be present");
assert_eq!(rb.response_type.components(), ID_PKIX_OCSP_BASIC);
let basic_der = rb.response.as_bytes();
let mut basic_dec = Decoder::new(basic_der, Encoding::Der);
let basic: BasicOCSPResponse<'_> =
basic_dec.decode().expect("BasicOCSPResponse decode failed");
let rd = &basic.tbs_response_data;
assert_eq!(rd.responses.len(), 1);
assert!(
matches!(rd.responses[0].cert_status, CertStatus::Good(_)),
"status must be Good"
);
}
#[test]
fn build_tbs_responder_name() {
let name_hash = [0xaau8; 20];
let key_hash = [0xbbu8; 20];
let serial = [0x02u8];
let tbs = OCSPResponseBuilder::new()
.responder_name(test_name_der())
.produced_at("20240601000000Z")
.add_response(SingleResponseSpec {
hash_algorithm_der: sha1_alg_der(),
issuer_name_hash: &name_hash,
issuer_key_hash: &key_hash,
serial: &serial,
status: 0,
this_update: "20240601000000Z",
next_update: None,
})
.build_tbs()
.expect("build_tbs with byName should succeed");
let mut dec = Decoder::new(&tbs, Encoding::Der);
let rd: ResponseData<'_> = dec.decode().expect("ResponseData decode failed");
assert!(
matches!(rd.responder_id, crate::ocsp::ResponderID::ByName(_)),
"expected byName responder"
);
assert_eq!(rd.responses.len(), 1);
}
#[test]
fn missing_responder_returns_error() {
let result = OCSPResponseBuilder::new()
.produced_at("20240101120000Z")
.build_tbs();
assert!(result.is_err(), "expected error for missing responder");
let msg = result.unwrap_err();
assert!(
msg.contains("responder"),
"error message must mention 'responder', got: {msg}"
);
}
#[test]
fn invalid_time_format_propagates() {
let result = OCSPResponseBuilder::new()
.responder_key_hash(&[0x01u8; 20])
.produced_at("not-a-time")
.build_tbs();
assert!(result.is_err());
assert!(result.unwrap_err().contains("not-a-time"));
}
}