use crate::ext_builder::encode_sequence;
use synta::tag::TAG_SET;
use synta::traits::Encode;
use crate::ess_types::{
AllOrFirstTier, ESSCertID, ESSPrivacyMark, ReceiptsFrom, SecurityClassification,
SecurityPolicyIdentifier, ALL_OR_FIRST_TIER_ALL_RECEIPTS,
ALL_OR_FIRST_TIER_FIRST_TIER_RECIPIENTS,
};
use crate::GeneralNameSpec;
#[derive(Debug, Default)]
pub struct SigningCertificateBuilder {
certs_encoded: Vec<u8>,
policies_encoded: Vec<u8>,
error: Option<String>,
}
impl SigningCertificateBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn add_cert_id(mut self, cert_hash: &[u8], issuer_serial_der: Option<&[u8]>) -> Self {
if self.error.is_some() {
return self;
}
let hash_ref = synta::OctetStringRef::new(cert_hash);
let issuer_serial_bytes = issuer_serial_der.map(|b| b.to_vec());
let cert_id_der = {
let issuer_serial_decoded: Option<crate::ess_types::IssuerSerial<'_>> =
if let Some(ref bytes) = issuer_serial_bytes {
match synta::Decoder::new(bytes, synta::Encoding::Der)
.decode::<crate::ess_types::IssuerSerial<'_>>()
{
Ok(is) => Some(is),
Err(e) => {
self.error = Some(format!("IssuerSerial decode failed: {e}"));
return self;
}
}
} else {
None
};
let cert_id = ESSCertID {
cert_hash: hash_ref,
issuer_serial: issuer_serial_decoded,
};
let mut enc = synta::Encoder::new(synta::Encoding::Der);
match cert_id.encode(&mut enc).and_then(|()| enc.finish()) {
Ok(b) => b,
Err(e) => {
self.error = Some(format!("ESSCertID DER encoding failed: {e}"));
return self;
}
}
};
self.certs_encoded.extend_from_slice(&cert_id_der);
self
}
pub fn add_policy(mut self, policy_oid: &[u32]) -> Self {
if self.error.is_some() {
return self;
}
let oid = match synta::ObjectIdentifier::new(policy_oid) {
Ok(o) => o,
Err(e) => {
self.error = Some(format!("invalid policy OID: {e}"));
return self;
}
};
let pi = crate::PolicyInformation {
policy_identifier: oid,
policy_qualifiers: None,
};
let mut enc = synta::Encoder::new(synta::Encoding::Der);
match pi.encode(&mut enc).and_then(|()| enc.finish()) {
Ok(bytes) => self.policies_encoded.extend_from_slice(&bytes),
Err(e) => {
self.error = Some(format!("PolicyInformation DER encoding failed: {e}"));
}
}
self
}
pub fn build(self) -> Result<Vec<u8>, String> {
if let Some(e) = self.error {
return Err(e);
}
if self.certs_encoded.is_empty() {
return Err("at least one certificate ID is required".to_string());
}
let mut content = Vec::new();
content.extend_from_slice(&encode_sequence(self.certs_encoded));
if !self.policies_encoded.is_empty() {
content.extend_from_slice(&encode_sequence(self.policies_encoded));
}
Ok(encode_sequence(content))
}
}
#[derive(Debug, Default)]
pub struct ReceiptRequestBuilder {
signed_content_id: Option<Vec<u8>>,
receipts_from: Option<Vec<u8>>,
receipts_to_encoded: Vec<u8>,
error: Option<String>,
}
impl ReceiptRequestBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn signed_content_identifier(mut self, id: &[u8]) -> Self {
if self.error.is_some() {
return self;
}
self.signed_content_id = Some(id.to_vec());
self
}
pub fn receipts_from_all(self) -> Self {
if self.error.is_some() {
return self;
}
self.encode_receipts_from(ReceiptsFrom::AllOrFirstTier(AllOrFirstTier::from(
ALL_OR_FIRST_TIER_ALL_RECEIPTS,
)))
}
pub fn receipts_from_first_tier(self) -> Self {
if self.error.is_some() {
return self;
}
self.encode_receipts_from(ReceiptsFrom::AllOrFirstTier(AllOrFirstTier::from(
ALL_OR_FIRST_TIER_FIRST_TIER_RECIPIENTS,
)))
}
fn encode_receipts_from(mut self, rf: ReceiptsFrom<'_>) -> Self {
let mut enc = synta::Encoder::new(synta::Encoding::Der);
match rf.encode(&mut enc).and_then(|()| enc.finish()) {
Ok(bytes) => self.receipts_from = Some(bytes),
Err(e) => self.error = Some(format!("ReceiptsFrom DER encoding failed: {e}")),
}
self
}
pub fn add_receipt_to_email(mut self, email: &str) -> Self {
if self.error.is_some() {
return self;
}
let spec = GeneralNameSpec::rfc822(email);
let gn = match spec.to_general_name() {
Ok(gn) => gn,
Err(e) => {
self.error = Some(format!("GeneralName error: {e}"));
return self;
}
};
let mut enc = synta::Encoder::new(synta::Encoding::Der);
match gn.encode(&mut enc).and_then(|()| enc.finish()) {
Ok(gn_bytes) => {
let gn_seq = encode_sequence(gn_bytes);
self.receipts_to_encoded.extend_from_slice(&gn_seq);
}
Err(e) => {
self.error = Some(format!("GeneralName DER encoding failed: {e}"));
}
}
self
}
pub fn add_receipt_to_raw(mut self, general_names_der: &[u8]) -> Self {
if self.error.is_some() {
return self;
}
self.receipts_to_encoded
.extend_from_slice(general_names_der);
self
}
pub fn build(self) -> Result<Vec<u8>, String> {
if let Some(e) = self.error {
return Err(e);
}
let sci_bytes = self
.signed_content_id
.ok_or_else(|| "signed_content_identifier is required".to_string())?;
let rf_bytes = self.receipts_from.ok_or_else(|| {
"receipts_from (call receipts_from_all or receipts_from_first_tier) is required"
.to_string()
})?;
if self.receipts_to_encoded.is_empty() {
return Err(
"at least one receiptsTo entry is required (use add_receipt_to_email or add_receipt_to_raw)"
.to_string(),
);
}
let sci_os = synta::OctetString::new(sci_bytes);
let mut sci_enc = synta::Encoder::new(synta::Encoding::Der);
sci_os
.encode(&mut sci_enc)
.map_err(|e| format!("signedContentIdentifier encode failed: {e}"))?;
let sci_der = sci_enc
.finish()
.map_err(|e| format!("signedContentIdentifier finish failed: {e}"))?;
let receipts_to_der = encode_sequence(self.receipts_to_encoded);
let mut content = Vec::new();
content.extend_from_slice(&sci_der);
content.extend_from_slice(&rf_bytes);
content.extend_from_slice(&receipts_to_der);
Ok(encode_sequence(content))
}
}
#[derive(Debug, Default)]
pub struct ESSSecurityLabelBuilder {
policy_oid: Option<Vec<u32>>,
classification: Option<u16>,
privacy_mark: Option<String>,
privacy_mark_is_printable: bool,
categories_encoded: Vec<u8>,
error: Option<String>,
}
impl ESSSecurityLabelBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn security_policy(mut self, policy_oid: &[u32]) -> Self {
if self.error.is_some() {
return self;
}
self.policy_oid = Some(policy_oid.to_vec());
self
}
pub fn classification(mut self, value: u16) -> Self {
if self.error.is_some() {
return self;
}
if value > 32767 {
self.error = Some(format!(
"security classification value {} exceeds RFC 2634 maximum of 32767",
value
));
return self;
}
self.classification = Some(value);
self
}
pub fn privacy_mark_utf8(mut self, mark: &str) -> Self {
if self.error.is_some() {
return self;
}
self.privacy_mark = Some(mark.to_string());
self.privacy_mark_is_printable = false;
self
}
pub fn privacy_mark_printable(mut self, mark: &str) -> Self {
if self.error.is_some() {
return self;
}
let printable_chars = mark
.chars()
.all(|c| c.is_ascii_alphanumeric() || " '()+,-./:=?".contains(c));
if !printable_chars {
self.error = Some(format!(
"privacy mark '{}' contains characters not allowed in PrintableString",
mark
));
return self;
}
self.privacy_mark = Some(mark.to_string());
self.privacy_mark_is_printable = true;
self
}
pub fn add_security_category_raw(mut self, security_category_der: &[u8]) -> Self {
if self.error.is_some() {
return self;
}
self.categories_encoded
.extend_from_slice(security_category_der);
self
}
pub fn build(self) -> Result<Vec<u8>, String> {
if let Some(e) = self.error {
return Err(e);
}
let policy_comps = self
.policy_oid
.ok_or_else(|| "security_policy (OID) is required".to_string())?;
let policy_oid: SecurityPolicyIdentifier = synta::ObjectIdentifier::new(&policy_comps)
.map_err(|e| format!("invalid security policy OID: {e}"))?;
let security_classification = self
.classification
.map(SecurityClassification::new_unchecked);
let privacy_mark_str = self.privacy_mark;
let privacy_mark_is_printable = self.privacy_mark_is_printable;
let privacy_mark: Option<ESSPrivacyMark<'_>> = if let Some(ref s) = privacy_mark_str {
if privacy_mark_is_printable {
match synta::PrintableStringRef::new(s) {
Ok(ps) => Some(ESSPrivacyMark::PString(ps)),
Err(e) => {
return Err(format!("privacyMark PrintableString encoding failed: {e}"))
}
}
} else {
Some(ESSPrivacyMark::Utf8String(synta::Utf8StringRef::new(s)))
}
} else {
None
};
let mut pol_enc = synta::Encoder::new(synta::Encoding::Der);
use synta::traits::Encode;
policy_oid
.encode(&mut pol_enc)
.map_err(|e| format!("securityPolicyIdentifier encode failed: {e}"))?;
let pol_der = pol_enc
.finish()
.map_err(|e| format!("securityPolicyIdentifier finish failed: {e}"))?;
let class_der: Option<Vec<u8>> = if let Some(c) = security_classification {
let mut enc = synta::Encoder::new(synta::Encoding::Der);
c.encode(&mut enc)
.map_err(|e| format!("securityClassification encode failed: {e}"))?;
Some(
enc.finish()
.map_err(|e| format!("securityClassification finish failed: {e}"))?,
)
} else {
None
};
let pm_der: Option<Vec<u8>> = if let Some(pm) = privacy_mark {
let mut enc = synta::Encoder::new(synta::Encoding::Der);
pm.encode(&mut enc)
.map_err(|e| format!("privacyMark encode failed: {e}"))?;
Some(
enc.finish()
.map_err(|e| format!("privacyMark finish failed: {e}"))?,
)
} else {
None
};
let mut set_content = Vec::new();
set_content.extend_from_slice(&pol_der);
if let Some(ref c) = class_der {
set_content.extend_from_slice(c);
}
if let Some(ref pm) = pm_der {
set_content.extend_from_slice(pm);
}
if !self.categories_encoded.is_empty() {
let cat_tag = synta::Tag::universal_constructed(TAG_SET);
let mut cat_enc = synta::Encoder::new(synta::Encoding::Der);
cat_enc
.start_constructed_no_guard(cat_tag)
.map_err(|e| format!("SecurityCategories SET start failed: {e}"))?;
cat_enc.write_bytes(&self.categories_encoded);
cat_enc
.end_constructed()
.map_err(|e| format!("SecurityCategories SET end failed: {e}"))?;
let cat_der = cat_enc
.finish()
.map_err(|e| format!("SecurityCategories SET finish failed: {e}"))?;
set_content.extend_from_slice(&cat_der);
}
let set_tag = synta::Tag::universal_constructed(TAG_SET);
let mut enc = synta::Encoder::new(synta::Encoding::Der);
enc.start_constructed_no_guard(set_tag)
.map_err(|e| format!("ESSSecurityLabel SET start failed: {e}"))?;
enc.write_bytes(&set_content);
enc.end_constructed()
.map_err(|e| format!("ESSSecurityLabel SET end failed: {e}"))?;
enc.finish().map_err(|e| format!("DER finish failed: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signing_certificate_minimal() {
let cert_hash = [0xabu8; 20]; let der = SigningCertificateBuilder::new()
.add_cert_id(&cert_hash, None)
.build()
.expect("build should succeed");
let decoded =
crate::ess_types::SigningCertificate::from_der(&der).expect("round-trip decode failed");
assert_eq!(decoded.certs.len(), 1);
assert!(decoded.policies.is_none());
}
#[test]
fn signing_certificate_requires_at_least_one_cert() {
let err = SigningCertificateBuilder::new().build();
assert!(err.is_err());
}
#[test]
fn receipt_request_all_receipts() {
let der = ReceiptRequestBuilder::new()
.signed_content_identifier(b"\x01\x02\x03\x04")
.receipts_from_all()
.add_receipt_to_email("receipts@example.com")
.build()
.expect("build should succeed");
let decoded =
crate::ess_types::ReceiptRequest::from_der(&der).expect("round-trip decode failed");
assert!(!decoded.receipts_to.is_empty());
}
#[test]
fn receipt_request_missing_sci_returns_error() {
let err = ReceiptRequestBuilder::new().receipts_from_all().build();
assert!(err.is_err());
assert!(err.unwrap_err().contains("signed_content_identifier"));
}
#[test]
fn receipt_request_missing_from_returns_error() {
let err = ReceiptRequestBuilder::new()
.signed_content_identifier(b"\x01\x02")
.build();
assert!(err.is_err());
assert!(err.unwrap_err().contains("receipts_from"));
}
#[test]
fn ess_security_label_minimal() {
let policy = &[1u32, 3, 6, 1, 5, 5, 7, 13, 1];
let der = ESSSecurityLabelBuilder::new()
.security_policy(policy)
.build()
.expect("build should succeed");
let decoded =
crate::ess_types::ESSSecurityLabel::from_der(&der).expect("round-trip decode failed");
assert!(decoded.security_classification.is_none());
assert!(decoded.privacy_mark.is_none());
}
#[test]
fn ess_security_label_with_classification() {
let policy = &[1u32, 3, 6, 1, 5, 5, 7, 13, 1];
let der = ESSSecurityLabelBuilder::new()
.security_policy(policy)
.classification(SecurityClassification::SECRET)
.privacy_mark_utf8("SECRET")
.build()
.expect("build with classification should succeed");
let decoded =
crate::ess_types::ESSSecurityLabel::from_der(&der).expect("round-trip decode failed");
assert_eq!(
decoded.security_classification.map(|c| c.get()),
Some(SecurityClassification::SECRET)
);
}
#[test]
fn receipt_request_empty_receipts_to_returns_error() {
let err = ReceiptRequestBuilder::new()
.signed_content_identifier(b"\x01\x02")
.receipts_from_all()
.build();
assert!(err.is_err());
assert!(err.unwrap_err().contains("receiptsTo"));
}
#[test]
fn ess_security_label_requires_policy() {
let err = ESSSecurityLabelBuilder::new().build();
assert!(err.is_err());
assert!(err.unwrap_err().contains("security_policy"));
}
#[test]
fn ess_security_label_classification_out_of_range() {
let policy = &[1u32, 3, 6, 1, 5, 5, 7, 13, 1];
let err = ESSSecurityLabelBuilder::new()
.security_policy(policy)
.classification(32768)
.build();
assert!(err.is_err());
assert!(err.unwrap_err().contains("32767"));
}
#[test]
fn ess_security_label_privacy_mark_printable_encodes_correctly() {
let policy = &[1u32, 3, 6, 1, 5, 5, 7, 13, 1];
let der = ESSSecurityLabelBuilder::new()
.security_policy(policy)
.privacy_mark_printable("SECRET")
.build()
.expect("build with PrintableString mark should succeed");
let decoded =
crate::ess_types::ESSSecurityLabel::from_der(&der).expect("round-trip decode failed");
assert!(
matches!(
decoded.privacy_mark,
Some(crate::ess_types::ESSPrivacyMark::PString(_))
),
"expected PString variant, got {:?}",
decoded.privacy_mark
);
}
}