Skip to main content

synta_certificate/
ext_builder.rs

1//! DER encoders for common X.509 v3 extension values.
2//!
3//! Each function returns the raw DER bytes of an extension's `extnValue`
4//! — the content that goes inside the OCTET STRING wrapper in the
5//! `Extension` SEQUENCE.  Callers supply these bytes to a certificate
6//! builder or splice them directly into a hand-crafted TBS structure.
7//!
8//! The key-identifier functions ([`encode_subject_key_identifier`] and
9//! [`encode_authority_key_identifier`]) accept a [`KeyIdMethod`] and a
10//! [`KeyIdHasher`] implementation, making them independent of any specific
11//! crypto library.  Use `OpensslKeyIdHasher`
12//! when the `openssl` Cargo feature is enabled.
13//!
14//! ## Fluent builders
15//!
16//! For extensions whose values are sequences of entries, fluent builders
17//! accumulate entries and produce the final DER on `build()`:
18//!
19//! | Builder | Extension | OID |
20//! |---------|-----------|-----|
21//! | [`SubjectAlternativeNameBuilder`] | Subject Alternative Name | 2.5.29.17 |
22//! | [`IssuerAlternativeNameBuilder`] | Issuer Alternative Name | 2.5.29.18 |
23//! | [`AuthorityInformationAccessBuilder`] | Authority Information Access | 1.3.6.1.5.5.7.1.1 |
24//! | [`ExtendedKeyUsageBuilder`] | Extended Key Usage | 2.5.29.37 |
25//! | [`NameConstraintsBuilder`] | Name Constraints | 2.5.29.30 |
26//! | [`CRLDistributionPointsBuilder`] | CRL Distribution Points | 2.5.29.31 |
27//! | [`IssuingDistributionPointBuilder`] | Issuing Distribution Point | 2.5.29.28 |
28//! | [`CertificatePoliciesBuilder`] | Certificate Policies | 2.5.29.32 |
29
30use synta::tag::TAG_SEQUENCE;
31use synta::traits::Decode;
32use synta::{Encoder, Encoding, Tag, ToDer};
33
34use crate::crypto::{KeyIdHasher, KeyIdMethod};
35use crate::{
36    AccessDescription, AuthorityKeyIdentifier, BasicConstraints, DistributionPoint,
37    DistributionPointName, GeneralNameSpec, GeneralSubtree, IssuingDistributionPoint,
38    PolicyInformation, PolicyQualifierInfo, SubjectPublicKeyInfo, KEY_USAGE_DECIPHER_ONLY,
39};
40
41/// Encode a `BasicConstraints` extension value (OID 2.5.29.19).
42///
43/// Returns the DER bytes of the `BasicConstraints` SEQUENCE:
44///
45/// ```text
46/// BasicConstraints ::= SEQUENCE {
47///     cA                  BOOLEAN DEFAULT FALSE,
48///     pathLenConstraint   INTEGER (0..MAX) OPTIONAL }
49/// ```
50///
51/// Uses the generated [`BasicConstraints`] type's `Encode` implementation.
52/// When `ca` is `false` the `cA` field is omitted (DER DEFAULT-FALSE rule).
53///
54/// Returns `None` on DER encoding error (which cannot happen for well-formed
55/// inputs in practice), or `Some(der)` with the encoded SEQUENCE.
56///
57/// # Example
58///
59/// ```
60/// use synta_certificate::encode_basic_constraints;
61///
62/// // End-entity certificate: empty SEQUENCE (30 00)
63/// let ee = encode_basic_constraints(false, None).unwrap();
64/// assert_eq!(ee, &[0x30, 0x00]);
65///
66/// // CA with no path-length constraint: cA = TRUE (30 03 01 01 ff)
67/// let ca = encode_basic_constraints(true, None).unwrap();
68/// assert_eq!(ca, &[0x30, 0x03, 0x01, 0x01, 0xff]);
69///
70/// // CA with pathLen = 0
71/// let ca_pl0 = encode_basic_constraints(true, Some(0)).unwrap();
72/// assert_eq!(ca_pl0, &[0x30, 0x06, 0x01, 0x01, 0xff, 0x02, 0x01, 0x00]);
73/// ```
74pub fn encode_basic_constraints(ca: bool, path_length: Option<u64>) -> Option<Vec<u8>> {
75    use synta::{Boolean, Integer};
76
77    let bc = BasicConstraints {
78        // DEFAULT FALSE: omit cA field in DER when false
79        c_a: if ca { Some(Boolean(true)) } else { None },
80        path_len_constraint: path_length.map(|n| Integer::from(n as i64)),
81    };
82    bc.to_der().ok()
83}
84
85/// Encode a `KeyUsage` extension value (OID 2.5.29.15).
86///
87/// `bits` is a bitmask where bit `n` corresponds to the `KEY_USAGE_*`
88/// constants exported from this crate:
89///
90/// | Bit | Constant | Named bit |
91/// |-----|----------|-----------|
92/// | 0 | `KEY_USAGE_DIGITAL_SIGNATURE` | `digitalSignature` |
93/// | 1 | `KEY_USAGE_NON_REPUDIATION` | `contentCommitment` |
94/// | 2 | `KEY_USAGE_KEY_ENCIPHERMENT` | `keyEncipherment` |
95/// | 3 | `KEY_USAGE_DATA_ENCIPHERMENT` | `dataEncipherment` |
96/// | 4 | `KEY_USAGE_KEY_AGREEMENT` | `keyAgreement` |
97/// | 5 | `KEY_USAGE_KEY_CERT_SIGN` | `keyCertSign` |
98/// | 6 | `KEY_USAGE_C_RLSIGN` | `cRLSign` |
99/// | 7 | `KEY_USAGE_ENCIPHER_ONLY` | `encipherOnly` |
100/// | 8 | `KEY_USAGE_DECIPHER_ONLY` | `decipherOnly` |
101///
102/// The BIT STRING is encoded in minimal DER form: trailing zero-bits are
103/// trimmed by adjusting `unused_bits` accordingly.
104///
105/// Uses [`synta::BitString`] (the same underlying type as the generated
106/// `KeyUsage` alias) for encoding.
107///
108/// Returns `None` on encoding error (which cannot happen for valid bit values
109/// in practice), or `Some(der)` containing the encoded BIT STRING.
110///
111/// # Example
112///
113/// ```
114/// use synta_certificate::{
115///     encode_key_usage,
116///     KEY_USAGE_DIGITAL_SIGNATURE, KEY_USAGE_KEY_CERT_SIGN, KEY_USAGE_C_RLSIGN,
117/// };
118///
119/// // digitalSignature only → 03 02 07 80
120/// let ku = encode_key_usage(1 << KEY_USAGE_DIGITAL_SIGNATURE).unwrap();
121/// assert_eq!(ku, &[0x03, 0x02, 0x07, 0x80]);
122///
123/// // keyCertSign | cRLSign → 03 02 01 06
124/// let ku = encode_key_usage(
125///     (1 << KEY_USAGE_KEY_CERT_SIGN) | (1 << KEY_USAGE_C_RLSIGN)
126/// ).unwrap();
127/// assert_eq!(ku, &[0x03, 0x02, 0x01, 0x06]);
128/// ```
129pub fn encode_key_usage(bits: u16) -> Option<Vec<u8>> {
130    // Map named-bit positions 0–7 to the DER byte representation.
131    // Named-bit n occupies bit-position 7-(n%8) within byte n/8 (MSB-first).
132    // For the 8 bits that all fall in byte 0: reverse_bits() of the lower
133    // byte achieves the correct layout in one operation.
134    let b0 = (bits as u8).reverse_bits();
135
136    // Bit 8 (decipherOnly) falls in the MSB of a second byte.
137    let has_decipher_only = (bits >> KEY_USAGE_DECIPHER_ONLY) & 1 != 0;
138
139    let (bytes, unused_bits) = if has_decipher_only {
140        // Two bytes: b0 holds bits 0–7; 0x80 places bit 8 at byte-1 MSB.
141        (vec![b0, 0x80u8], 7u8)
142    } else if b0 == 0 {
143        // No bits set — encode as empty BIT STRING (03 01 00 in DER).
144        (vec![], 0u8)
145    } else {
146        // DER requires unused_bits == number of trailing zero bits.
147        let trailing = b0.trailing_zeros() as u8;
148        (vec![b0], trailing)
149    };
150
151    let bs = synta::BitString::new(bytes, unused_bits).ok()?;
152    bs.to_der().ok()
153}
154
155/// Encode a `SubjectKeyIdentifier` extension value (OID 2.5.29.14).
156///
157/// Decodes `spki_der` as a DER `SubjectPublicKeyInfo` and computes the key
158/// identifier according to `method`:
159///
160/// - All methods except [`Rfc7093Method4`][KeyIdMethod::Rfc7093Method4] hash
161///   the raw BIT STRING value of `subjectPublicKey` (i.e. the key bytes
162///   without the unused-bits octet).
163/// - [`Rfc7093Method4`][KeyIdMethod::Rfc7093Method4] hashes the full
164///   `SubjectPublicKeyInfo` DER encoding.
165///
166/// Returns a DER `OCTET STRING` value containing the computed identifier,
167/// or `None` if `spki_der` cannot be decoded or if the hasher fails.
168///
169/// # Example (RFC 5280 default)
170///
171/// ```rust,ignore
172/// use synta_certificate::{encode_subject_key_identifier, KeyIdMethod, OpensslKeyIdHasher};
173///
174/// let ski_der = encode_subject_key_identifier(
175///     &spki_der,
176///     KeyIdMethod::Rfc5280Sha1,
177///     &OpensslKeyIdHasher,
178/// );
179/// ```
180pub fn encode_subject_key_identifier<H: KeyIdHasher>(
181    spki_der: &[u8],
182    method: KeyIdMethod,
183    hasher: &H,
184) -> Option<Vec<u8>> {
185    let mut dec = synta::Decoder::new(spki_der, synta::Encoding::Der);
186    let spki: SubjectPublicKeyInfo<'_> = dec.decode().ok()?;
187
188    // Select input data per method (RFC 7093 §2 m4 uses the full SPKI DER).
189    let hash_input: &[u8] = if method.uses_full_spki_der() {
190        spki_der
191    } else {
192        spki.subject_public_key.as_bytes()
193    };
194
195    let raw_hash = hasher.hash(method.algorithm_oid(), hash_input).ok()?;
196    let key_id = method.apply_output_length(raw_hash);
197
198    let os = synta::OctetString::new(key_id);
199    os.to_der().ok()
200}
201
202/// Encode an `AuthorityKeyIdentifier` extension value (OID 2.5.29.35).
203///
204/// Sets only the `keyIdentifier [0]` field.  The key identifier is computed
205/// from the issuer's `SubjectPublicKeyInfo` using the same input and hash
206/// rules as [`encode_subject_key_identifier`]:
207///
208/// - Methods other than [`Rfc7093Method4`][KeyIdMethod::Rfc7093Method4] hash
209///   the BIT STRING value of `subjectPublicKey`.
210/// - [`Rfc7093Method4`][KeyIdMethod::Rfc7093Method4] hashes the full
211///   `SubjectPublicKeyInfo` DER encoding.
212///
213/// Returns the DER bytes of the `AuthorityKeyIdentifier` SEQUENCE, or `None`
214/// if `issuer_spki_der` cannot be decoded or if the hasher fails.
215///
216/// Uses the generated [`AuthorityKeyIdentifier`] type's `Encode`
217/// implementation.
218///
219/// # Example (RFC 5280 default)
220///
221/// ```rust,ignore
222/// use synta_certificate::{encode_authority_key_identifier, KeyIdMethod, OpensslKeyIdHasher};
223///
224/// let aki_der = encode_authority_key_identifier(
225///     &ca_spki_der,
226///     KeyIdMethod::Rfc5280Sha1,
227///     &OpensslKeyIdHasher,
228/// );
229/// ```
230pub fn encode_authority_key_identifier<H: KeyIdHasher>(
231    issuer_spki_der: &[u8],
232    method: KeyIdMethod,
233    hasher: &H,
234) -> Option<Vec<u8>> {
235    use synta::OctetStringRef;
236
237    let mut dec = synta::Decoder::new(issuer_spki_der, synta::Encoding::Der);
238    let spki: SubjectPublicKeyInfo<'_> = dec.decode().ok()?;
239
240    let hash_input: &[u8] = if method.uses_full_spki_der() {
241        issuer_spki_der
242    } else {
243        spki.subject_public_key.as_bytes()
244    };
245
246    let raw_hash = hasher.hash(method.algorithm_oid(), hash_input).ok()?;
247    let key_id = method.apply_output_length(raw_hash);
248
249    let aki = AuthorityKeyIdentifier {
250        key_identifier: Some(OctetStringRef::new(&key_id)),
251        authority_cert_issuer: None,
252        authority_cert_serial_number: None,
253    };
254    aki.to_der().ok()
255}
256
257// ── Internal helpers ──────────────────────────────────────────────────────────
258
259/// Wrap `content` bytes in a DER SEQUENCE TLV and return the result.
260pub(crate) fn encode_sequence(content: Vec<u8>) -> Vec<u8> {
261    let mut enc = Encoder::new(Encoding::Der);
262    let _ = enc.start_constructed_no_guard(Tag::universal_constructed(TAG_SEQUENCE));
263    enc.write_bytes(&content);
264    let _ = enc.end_constructed();
265    enc.finish().unwrap_or_default()
266}
267
268// ── SubjectAlternativeNameBuilder ─────────────────────────────────────────────
269
270/// Fluent builder for a `SubjectAltName` extension value (OID 2.5.29.17).
271///
272/// Add [`crate::GeneralName`] entries with the fluent methods, then call
273/// [`build`][Self::build] to obtain the DER bytes of the `GeneralNames`
274/// SEQUENCE — the content that goes inside the OCTET STRING wrapper in the
275/// `Extension` SEQUENCE.
276///
277/// # Example
278///
279/// ```rust,ignore
280/// use synta_certificate::SubjectAlternativeNameBuilder;
281///
282/// let san_der = SubjectAlternativeNameBuilder::new()
283///     .dns_name("www.example.com")
284///     .dns_name("example.com")
285///     .rfc822_name("admin@example.com")
286///     .ip_address(&[192, 168, 1, 1])
287///     .build()
288///     .unwrap();
289/// ```
290#[derive(Debug, Default)]
291pub struct SubjectAlternativeNameBuilder {
292    /// Pre-encoded `GeneralName` TLVs accumulated as raw DER bytes.
293    encoded: Vec<u8>,
294    /// First encoding error encountered; set by `push()`.
295    error: Option<String>,
296}
297
298impl SubjectAlternativeNameBuilder {
299    /// Create a new, empty `SubjectAlternativeNameBuilder`.
300    pub fn new() -> Self {
301        Self::default()
302    }
303
304    /// Encode a [`GeneralNameSpec`] and append its DER TLV bytes to the buffer.
305    ///
306    /// Records the first encoding error in `self.error`; subsequent pushes
307    /// are skipped once an error has been recorded.
308    pub fn push(&mut self, spec: GeneralNameSpec) {
309        if self.error.is_some() {
310            return;
311        }
312        let gn = match spec.to_general_name() {
313            Ok(gn) => gn,
314            Err(e) => {
315                self.error = Some(format!("GeneralName error: {e}"));
316                return;
317            }
318        };
319        match gn.to_der() {
320            Ok(bytes) => self.encoded.extend_from_slice(&bytes),
321            Err(e) => self.error = Some(format!("GeneralName DER encoding failed: {e}")),
322        }
323    }
324
325    /// Add a `dNSName [2]` GeneralName.
326    pub fn dns_name(mut self, name: &str) -> Self {
327        self.push(GeneralNameSpec::dns(name));
328        self
329    }
330
331    /// Add an `rfc822Name [1]` GeneralName (email address).
332    pub fn rfc822_name(mut self, email: &str) -> Self {
333        self.push(GeneralNameSpec::rfc822(email));
334        self
335    }
336
337    /// Add a `uniformResourceIdentifier [6]` GeneralName (URI).
338    pub fn uri(mut self, uri: &str) -> Self {
339        self.push(GeneralNameSpec::uri(uri));
340        self
341    }
342
343    /// Add an `iPAddress [7]` GeneralName.
344    ///
345    /// Pass 4 bytes for IPv4 or 16 bytes for IPv6.
346    pub fn ip_address(mut self, addr: &[u8]) -> Self {
347        self.push(GeneralNameSpec::ip_address(addr));
348        self
349    }
350
351    /// Add a `directoryName [4]` GeneralName (EXPLICIT wrapper).
352    ///
353    /// `name_der` is the complete DER TLV bytes of the `Name` SEQUENCE, such
354    /// as those returned by [`NameBuilder::build`][crate::NameBuilder::build].
355    pub fn directory_name(mut self, name_der: &[u8]) -> Self {
356        self.push(GeneralNameSpec::directory_name(name_der));
357        self
358    }
359
360    /// Add a `registeredID [8]` GeneralName.
361    pub fn registered_id(mut self, oid: synta::ObjectIdentifier) -> Self {
362        self.push(GeneralNameSpec::registered_id(oid));
363        self
364    }
365
366    /// Add an `otherName [0]` GeneralName.
367    ///
368    /// `other_name_der` is the complete DER TLV bytes of the `OtherName` SEQUENCE:
369    /// `SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }`.
370    ///
371    /// The bytes are decoded and re-encoded as a `GeneralName::OtherName` TLV.
372    /// On decoding failure the error is recorded and subsequent `push` calls are skipped.
373    pub fn other_name(mut self, other_name_der: &[u8]) -> Self {
374        if self.error.is_some() {
375            return self;
376        }
377        let mut dec = synta::Decoder::new(other_name_der, synta::Encoding::Der);
378        match crate::OtherName::decode(&mut dec) {
379            Ok(on) => {
380                let gn = crate::GeneralName::OtherName(on);
381                match gn.to_der() {
382                    Ok(bytes) => self.encoded.extend_from_slice(&bytes),
383                    Err(e) => self.error = Some(format!("OtherName DER re-encoding failed: {e}")),
384                }
385            }
386            Err(e) => {
387                self.error = Some(format!("OtherName DER decode failed: {e}"));
388            }
389        }
390        self
391    }
392
393    /// Build the DER-encoded `GeneralNames` SEQUENCE.
394    ///
395    /// Returns an empty `SEQUENCE` (`30 00`) if no entries have been added.
396    /// Returns `Err` if any entry could not be DER-encoded.
397    pub fn build(self) -> Result<Vec<u8>, String> {
398        if let Some(e) = self.error {
399            return Err(e);
400        }
401        Ok(encode_sequence(self.encoded))
402    }
403}
404
405// ── AuthorityInformationAccessBuilder ─────────────────────────────────────────
406
407/// Fluent builder for an `AuthorityInfoAccessSyntax` extension value
408/// (OID 1.3.6.1.5.5.7.1.1, Authority Information Access, RFC 5280 §4.2.2.1).
409///
410/// Add OCSP and/or CA Issuers URIs with the fluent methods, then call
411/// [`build`][Self::build] to obtain the DER bytes of the
412/// `AuthorityInfoAccessSyntax` SEQUENCE — the content that goes inside the
413/// OCTET STRING wrapper in the `Extension` SEQUENCE.
414///
415/// # Example
416///
417/// ```rust,ignore
418/// use synta_certificate::AuthorityInformationAccessBuilder;
419///
420/// let aia_der = AuthorityInformationAccessBuilder::new()
421///     .ocsp("http://ocsp.example.com")
422///     .ca_issuers("http://ca.example.com/ca.crt")
423///     .build()
424///     .unwrap();
425/// ```
426#[derive(Debug, Default)]
427pub struct AuthorityInformationAccessBuilder {
428    /// Pre-encoded `AccessDescription` TLVs accumulated as raw DER bytes.
429    encoded: Vec<u8>,
430    /// First encoding error encountered; set by `push()` or `push_uri()`.
431    error: Option<String>,
432}
433
434impl AuthorityInformationAccessBuilder {
435    /// Create a new, empty `AuthorityInformationAccessBuilder`.
436    pub fn new() -> Self {
437        Self::default()
438    }
439
440    /// Encode an `AccessDescription` and append its TLV bytes.
441    ///
442    /// Records the first encoding error in `self.error`.
443    fn push(&mut self, desc: AccessDescription<'_>) {
444        if self.error.is_some() {
445            return;
446        }
447        match desc.to_der() {
448            Ok(bytes) => self.encoded.extend_from_slice(&bytes),
449            Err(e) => self.error = Some(format!("AccessDescription DER encoding failed: {e}")),
450        }
451    }
452
453    fn push_uri(&mut self, method_oid: &[u32], uri: &str) {
454        if self.error.is_some() {
455            return;
456        }
457        let access_method = match synta::ObjectIdentifier::new(method_oid) {
458            Ok(oid) => oid,
459            Err(e) => {
460                self.error = Some(format!("invalid access method OID: {e}"));
461                return;
462            }
463        };
464        let spec = GeneralNameSpec::uri(uri);
465        let access_location = match spec.to_general_name() {
466            Ok(gn) => gn,
467            Err(e) => {
468                self.error = Some(format!("URI GeneralName error: {e}"));
469                return;
470            }
471        };
472        self.push(AccessDescription {
473            access_method,
474            access_location,
475        });
476    }
477
478    /// Add an OCSP responder URI (`id-ad-ocsp`, 1.3.6.1.5.5.7.48.1).
479    pub fn ocsp(mut self, uri: &str) -> Self {
480        self.push_uri(crate::ID_AD_OCSP, uri);
481        self
482    }
483
484    /// Add a CA Issuers URI (`id-ad-caIssuers`, 1.3.6.1.5.5.7.48.2).
485    pub fn ca_issuers(mut self, uri: &str) -> Self {
486        self.push_uri(crate::ID_AD_CA_ISSUERS, uri);
487        self
488    }
489
490    /// Build the DER-encoded `AuthorityInfoAccessSyntax` SEQUENCE.
491    ///
492    /// Returns an empty `SEQUENCE` (`30 00`) if no entries have been added.
493    /// Returns `Err` if any entry could not be DER-encoded.
494    pub fn build(self) -> Result<Vec<u8>, String> {
495        if let Some(e) = self.error {
496            return Err(e);
497        }
498        Ok(encode_sequence(self.encoded))
499    }
500}
501
502// ── ExtendedKeyUsageBuilder ───────────────────────────────────────────────────
503
504/// Fluent builder for an `ExtKeyUsageSyntax` extension value
505/// (OID 2.5.29.37, Extended Key Usage, RFC 5280 §4.2.1.12).
506///
507/// Add well-known key purpose OIDs with the convenience methods or custom OIDs
508/// via [`add_oid`][Self::add_oid], then call [`build`][Self::build] to obtain
509/// the DER bytes of the `ExtKeyUsageSyntax` SEQUENCE — the content that goes
510/// inside the OCTET STRING wrapper in the `Extension` SEQUENCE.
511///
512/// # Example
513///
514/// ```rust,ignore
515/// use synta_certificate::ExtendedKeyUsageBuilder;
516///
517/// // Typical TLS server certificate EKU
518/// let eku_der = ExtendedKeyUsageBuilder::new()
519///     .server_auth()
520///     .client_auth()
521///     .build()
522///     .unwrap();
523/// ```
524#[derive(Debug, Default)]
525pub struct ExtendedKeyUsageBuilder {
526    /// Pre-encoded `KeyPurposeId` (OID) TLVs accumulated as raw DER bytes.
527    encoded: Vec<u8>,
528    /// First encoding error encountered; set by `add_oid()`.
529    error: Option<String>,
530}
531
532impl ExtendedKeyUsageBuilder {
533    /// Create a new, empty `ExtendedKeyUsageBuilder`.
534    pub fn new() -> Self {
535        Self::default()
536    }
537
538    /// Add a key purpose OID by component slice.
539    ///
540    /// Use this for custom EKU OIDs not covered by the convenience methods.
541    /// The well-known constants are in [`crate::oids`].
542    ///
543    /// # Example
544    ///
545    /// ```rust,ignore
546    /// use synta_certificate::{ExtendedKeyUsageBuilder, oids};
547    ///
548    /// let eku_der = ExtendedKeyUsageBuilder::new()
549    ///     .add_oid(oids::KP_SERVER_AUTH)
550    ///     .build()
551    ///     .unwrap();
552    /// ```
553    pub fn add_oid(mut self, comps: &[u32]) -> Self {
554        if self.error.is_some() {
555            return self;
556        }
557        let oid = match synta::ObjectIdentifier::new(comps) {
558            Ok(oid) => oid,
559            Err(e) => {
560                self.error = Some(format!("invalid EKU OID: {e}"));
561                return self;
562            }
563        };
564        match oid.to_der() {
565            Ok(bytes) => self.encoded.extend_from_slice(&bytes),
566            Err(e) => self.error = Some(format!("OID DER encoding failed: {e}")),
567        }
568        self
569    }
570
571    /// Add `id-kp-serverAuth` (1.3.6.1.5.5.7.3.1).
572    pub fn server_auth(self) -> Self {
573        self.add_oid(crate::ID_KP_SERVER_AUTH)
574    }
575
576    /// Add `id-kp-clientAuth` (1.3.6.1.5.5.7.3.2).
577    pub fn client_auth(self) -> Self {
578        self.add_oid(crate::ID_KP_CLIENT_AUTH)
579    }
580
581    /// Add `id-kp-codeSigning` (1.3.6.1.5.5.7.3.3).
582    pub fn code_signing(self) -> Self {
583        self.add_oid(crate::ID_KP_CODE_SIGNING)
584    }
585
586    /// Add `id-kp-emailProtection` (1.3.6.1.5.5.7.3.4).
587    pub fn email_protection(self) -> Self {
588        self.add_oid(crate::ID_KP_EMAIL_PROTECTION)
589    }
590
591    /// Add `id-kp-timeStamping` (1.3.6.1.5.5.7.3.8).
592    pub fn time_stamping(self) -> Self {
593        self.add_oid(crate::ID_KP_TIME_STAMPING)
594    }
595
596    /// Add `id-kp-OCSPSigning` (1.3.6.1.5.5.7.3.9).
597    pub fn ocsp_signing(self) -> Self {
598        self.add_oid(crate::ID_KP_OCSPSIGNING)
599    }
600
601    /// Build the DER-encoded `ExtKeyUsageSyntax` SEQUENCE.
602    ///
603    /// Returns an empty `SEQUENCE` (`30 00`) if no OIDs have been added.
604    /// Returns `Err` if any OID could not be constructed or DER-encoded.
605    pub fn build(self) -> Result<Vec<u8>, String> {
606        if let Some(e) = self.error {
607            return Err(e);
608        }
609        Ok(encode_sequence(self.encoded))
610    }
611}
612
613// ── IssuerAlternativeNameBuilder ──────────────────────────────────────────────
614
615/// Fluent builder for an `IssuerAltName` extension value (OID 2.5.29.18).
616///
617/// The encoding is identical to [`SubjectAlternativeNameBuilder`] — a
618/// `GeneralNames` SEQUENCE — but the OID differs (2.5.29.18 vs 2.5.29.17).
619/// Add entries with the fluent methods, then call [`build`][Self::build].
620///
621/// # Example
622///
623/// ```rust,ignore
624/// use synta_certificate::IssuerAlternativeNameBuilder;
625///
626/// let ian_der = IssuerAlternativeNameBuilder::new()
627///     .rfc822_name("ca@example.com")
628///     .uri("http://www.example.com")
629///     .build()
630///     .unwrap();
631/// ```
632#[derive(Debug, Default)]
633pub struct IssuerAlternativeNameBuilder {
634    /// Pre-encoded `GeneralName` TLVs accumulated as raw DER bytes.
635    encoded: Vec<u8>,
636    /// First encoding error encountered; set by `push()`.
637    error: Option<String>,
638}
639
640impl IssuerAlternativeNameBuilder {
641    /// Create a new, empty `IssuerAlternativeNameBuilder`.
642    pub fn new() -> Self {
643        Self::default()
644    }
645
646    /// Encode a [`GeneralNameSpec`] and append its DER TLV bytes to the buffer.
647    ///
648    /// Records the first encoding error in `self.error`.
649    pub fn push(&mut self, spec: GeneralNameSpec) {
650        if self.error.is_some() {
651            return;
652        }
653        let gn = match spec.to_general_name() {
654            Ok(gn) => gn,
655            Err(e) => {
656                self.error = Some(format!("GeneralName error: {e}"));
657                return;
658            }
659        };
660        match gn.to_der() {
661            Ok(bytes) => self.encoded.extend_from_slice(&bytes),
662            Err(e) => {
663                self.error = Some(format!("GeneralName DER encoding failed: {e}"));
664            }
665        }
666    }
667
668    /// Add a DNS name entry.
669    pub fn dns_name(mut self, name: &str) -> Self {
670        self.push(GeneralNameSpec::dns(name));
671        self
672    }
673
674    /// Add an RFC 822 (email) name entry.
675    pub fn rfc822_name(mut self, email: &str) -> Self {
676        self.push(GeneralNameSpec::rfc822(email));
677        self
678    }
679
680    /// Add a URI entry.
681    pub fn uri(mut self, uri: &str) -> Self {
682        self.push(GeneralNameSpec::uri(uri));
683        self
684    }
685
686    /// Add an IP address entry (4 bytes for IPv4, 16 bytes for IPv6).
687    pub fn ip_address(mut self, addr: &[u8]) -> Self {
688        self.push(GeneralNameSpec::ip_address(addr));
689        self
690    }
691
692    /// Add a directory name entry (DER-encoded `Name` SEQUENCE bytes).
693    pub fn directory_name(mut self, name_der: &[u8]) -> Self {
694        self.push(GeneralNameSpec::directory_name(name_der));
695        self
696    }
697
698    /// Add a registered OID entry.
699    pub fn registered_id(mut self, oid: synta::ObjectIdentifier) -> Self {
700        self.push(GeneralNameSpec::registered_id(oid));
701        self
702    }
703
704    /// Build the DER-encoded `IssuerAltName` value (a `GeneralNames` SEQUENCE).
705    ///
706    /// Returns `Err` if any entry could not be DER-encoded.
707    pub fn build(self) -> Result<Vec<u8>, String> {
708        if let Some(e) = self.error {
709            return Err(e);
710        }
711        Ok(encode_sequence(self.encoded))
712    }
713}
714
715// ── NameConstraintsBuilder ────────────────────────────────────────────────────
716
717/// Fluent builder for a `NameConstraints` extension value (OID 2.5.29.30,
718/// RFC 5280 §4.2.1.10).
719///
720/// The `NameConstraints` extension restricts the namespace within which all
721/// subject names in certificates issued by a CA must fall.  Names in the
722/// `permittedSubtrees` field are allowed; names in the `excludedSubtrees`
723/// field are forbidden.  When present, this extension MUST be marked critical.
724///
725/// ```text
726/// NameConstraints ::= SEQUENCE {
727///     permittedSubtrees   [0] GeneralSubtrees OPTIONAL,
728///     excludedSubtrees    [1] GeneralSubtrees OPTIONAL }
729///
730/// GeneralSubtrees ::= SEQUENCE SIZE (1..MAX) OF GeneralSubtree
731///
732/// GeneralSubtree ::= SEQUENCE {
733///     base                GeneralName,
734///     minimum  [0]        BaseDistance DEFAULT 0,
735///     maximum  [1]        BaseDistance OPTIONAL }
736/// ```
737///
738/// Add permitted and excluded subtrees using the typed methods, then call
739/// [`build`][Self::build] to obtain the DER bytes of the outer `NameConstraints`
740/// SEQUENCE — the content that goes inside the OCTET STRING wrapper in the
741/// `Extension` SEQUENCE.
742///
743/// The permitted and excluded lists are encoded as
744/// `[0] IMPLICIT GeneralSubtrees` and `[1] IMPLICIT GeneralSubtrees`
745/// respectively.  Each [`GeneralSubtree`] is encoded with `minimum` and
746/// `maximum` absent (i.e. the DEFAULT values `0` and absent, per RFC 5280).
747///
748/// **DNS name conventions (RFC 5280 §4.2.1.10):** a constraint of `".example.com"`
749/// applies to any subdomain of `example.com` (e.g. `www.example.com`), while
750/// `"example.com"` constrains only that exact host name.
751///
752/// **IP address constraints:** supply `addr ++ mask` concatenated — 8 bytes for
753/// IPv4 (4-byte address followed by 4-byte mask, e.g. `[192,168,0,0,255,255,0,0]`)
754/// or 32 bytes for IPv6 (16-byte address followed by 16-byte mask).
755///
756/// # Example
757///
758/// ```rust,ignore
759/// use synta_certificate::NameConstraintsBuilder;
760///
761/// // Permit the example.com DNS subtree and a specific IP range;
762/// // exclude a known-bad subdomain.
763/// let nc_der = NameConstraintsBuilder::new()
764///     .permit_dns(".example.com")
765///     .permit_ip(&[10, 0, 0, 0, 255, 0, 0, 0])   // 10.0.0.0/8
766///     .exclude_dns(".evil.com")
767///     .build()
768///     .unwrap();
769/// ```
770#[derive(Debug, Default)]
771pub struct NameConstraintsBuilder {
772    /// Pre-encoded `GeneralSubtree` TLVs for permitted subtrees.
773    permitted_bytes: Vec<u8>,
774    /// Pre-encoded `GeneralSubtree` TLVs for excluded subtrees.
775    excluded_bytes: Vec<u8>,
776    /// First encoding error encountered.
777    error: Option<String>,
778}
779
780impl NameConstraintsBuilder {
781    /// Create a new, empty `NameConstraintsBuilder`.
782    pub fn new() -> Self {
783        Self::default()
784    }
785
786    /// Encode a `GeneralSubtree { base: gn, minimum: None, maximum: None }` and
787    /// append to the given buffer.  Records the first error in `self.error`.
788    fn push_subtree(&mut self, spec: GeneralNameSpec, buf: &mut Vec<u8>) {
789        if self.error.is_some() {
790            return;
791        }
792        let gn = match spec.to_general_name() {
793            Ok(gn) => gn,
794            Err(e) => {
795                self.error = Some(format!("GeneralName error: {e}"));
796                return;
797            }
798        };
799        let subtree = GeneralSubtree {
800            base: gn,
801            minimum: None,
802            maximum: None,
803        };
804        match subtree.to_der() {
805            Ok(bytes) => buf.extend_from_slice(&bytes),
806            Err(e) => {
807                self.error = Some(format!("GeneralSubtree DER encoding failed: {e}"));
808            }
809        }
810    }
811
812    /// Add a DNS name constraint to the permitted subtrees.
813    ///
814    /// Use a leading dot (e.g. `".example.com"`) to constrain the entire
815    /// subtree rooted at `example.com`.  A value without a leading dot
816    /// (e.g. `"host.example.com"`) constrains only that exact host name.
817    pub fn permit_dns(mut self, dns: &str) -> Self {
818        let mut buf = std::mem::take(&mut self.permitted_bytes);
819        self.push_subtree(GeneralNameSpec::dns(dns), &mut buf);
820        self.permitted_bytes = buf;
821        self
822    }
823
824    /// Add an RFC 822 (email) domain constraint to the permitted subtrees.
825    ///
826    /// Pass a bare domain (e.g. `"example.com"`) to permit all addresses in
827    /// that domain, or a full address (e.g. `"user@example.com"`) to permit
828    /// only that specific address.
829    pub fn permit_rfc822(mut self, email: &str) -> Self {
830        let mut buf = std::mem::take(&mut self.permitted_bytes);
831        self.push_subtree(GeneralNameSpec::rfc822(email), &mut buf);
832        self.permitted_bytes = buf;
833        self
834    }
835
836    /// Add a URI scheme constraint to the permitted subtrees.
837    ///
838    /// The URI value is encoded as a `uniformResourceIdentifier [6]`
839    /// `GeneralName`.  Per RFC 5280 §4.2.1.10, a URI constraint specifies
840    /// a host name (optionally with a leading dot for subtree matching),
841    /// not a full URI.  Example: `"example.com"` or `".example.com"`.
842    pub fn permit_uri(mut self, uri: &str) -> Self {
843        let mut buf = std::mem::take(&mut self.permitted_bytes);
844        self.push_subtree(GeneralNameSpec::uri(uri), &mut buf);
845        self.permitted_bytes = buf;
846        self
847    }
848
849    /// Add an IP address range constraint to the permitted subtrees.
850    ///
851    /// `addr_and_mask` must be the address bytes immediately followed by the
852    /// mask bytes — 8 bytes for IPv4 (e.g. `[192,168,0,0, 255,255,0,0]` for
853    /// `192.168.0.0/16`) or 32 bytes for IPv6.  The encoding follows
854    /// RFC 5280 §4.2.1.10 (a `SEQUENCE { iPAddress [7] OCTET STRING }`
855    /// where the OCTET STRING contains address||mask).
856    pub fn permit_ip(mut self, addr_and_mask: &[u8]) -> Self {
857        let mut buf = std::mem::take(&mut self.permitted_bytes);
858        self.push_subtree(GeneralNameSpec::ip_address(addr_and_mask), &mut buf);
859        self.permitted_bytes = buf;
860        self
861    }
862
863    /// Add a directory name constraint to the permitted subtrees.
864    ///
865    /// `name_der` is the complete DER TLV bytes of an X.509 `Name` SEQUENCE,
866    /// such as those returned by `NameBuilder::build()`.  The constraint
867    /// is encoded as a `directoryName [4]` `GeneralName` inside a
868    /// `GeneralSubtree`.
869    pub fn permit_directory_name(mut self, name_der: &[u8]) -> Self {
870        let mut buf = std::mem::take(&mut self.permitted_bytes);
871        self.push_subtree(GeneralNameSpec::directory_name(name_der), &mut buf);
872        self.permitted_bytes = buf;
873        self
874    }
875
876    /// Add a DNS name constraint to the excluded subtrees.
877    ///
878    /// Use a leading dot (e.g. `".evil.com"`) to exclude the entire subtree
879    /// rooted at `evil.com`.  A value without a leading dot excludes only
880    /// that exact host name.
881    pub fn exclude_dns(mut self, dns: &str) -> Self {
882        let mut buf = std::mem::take(&mut self.excluded_bytes);
883        self.push_subtree(GeneralNameSpec::dns(dns), &mut buf);
884        self.excluded_bytes = buf;
885        self
886    }
887
888    /// Add an RFC 822 (email) domain constraint to the excluded subtrees.
889    ///
890    /// Pass a bare domain (e.g. `"evil.com"`) to exclude all addresses in
891    /// that domain, or a full address (e.g. `"user@evil.com"`) to exclude
892    /// only that specific address.
893    pub fn exclude_rfc822(mut self, email: &str) -> Self {
894        let mut buf = std::mem::take(&mut self.excluded_bytes);
895        self.push_subtree(GeneralNameSpec::rfc822(email), &mut buf);
896        self.excluded_bytes = buf;
897        self
898    }
899
900    /// Add a URI scheme constraint to the excluded subtrees.
901    ///
902    /// The value is encoded as a `uniformResourceIdentifier [6]` `GeneralName`.
903    /// Per RFC 5280 §4.2.1.10, the constraint value is a host name or
904    /// dot-prefixed domain, not a full URI.
905    pub fn exclude_uri(mut self, uri: &str) -> Self {
906        let mut buf = std::mem::take(&mut self.excluded_bytes);
907        self.push_subtree(GeneralNameSpec::uri(uri), &mut buf);
908        self.excluded_bytes = buf;
909        self
910    }
911
912    /// Add an IP address range constraint to the excluded subtrees.
913    ///
914    /// `addr_and_mask` must be the address bytes immediately followed by the
915    /// mask bytes — 8 bytes for IPv4 (e.g. `[10,0,0,0, 255,0,0,0]` for
916    /// `10.0.0.0/8`) or 32 bytes for IPv6.  Follows the same encoding as
917    /// [`permit_ip`][Self::permit_ip].
918    pub fn exclude_ip(mut self, addr_and_mask: &[u8]) -> Self {
919        let mut buf = std::mem::take(&mut self.excluded_bytes);
920        self.push_subtree(GeneralNameSpec::ip_address(addr_and_mask), &mut buf);
921        self.excluded_bytes = buf;
922        self
923    }
924
925    /// Add a directory name constraint to the excluded subtrees.
926    ///
927    /// `name_der` is the complete DER TLV bytes of an X.509 `Name` SEQUENCE.
928    /// The constraint is encoded as a `directoryName [4]` `GeneralName`
929    /// inside a `GeneralSubtree`.  See also [`permit_directory_name`][Self::permit_directory_name].
930    pub fn exclude_directory_name(mut self, name_der: &[u8]) -> Self {
931        let mut buf = std::mem::take(&mut self.excluded_bytes);
932        self.push_subtree(GeneralNameSpec::directory_name(name_der), &mut buf);
933        self.excluded_bytes = buf;
934        self
935    }
936
937    /// Build the DER-encoded `NameConstraints` SEQUENCE.
938    ///
939    /// Returns the DER bytes of the outer `NameConstraints` SEQUENCE
940    /// (tag `30`), ready to be placed inside the `extnValue` OCTET STRING
941    /// of an `Extension`.  If neither permitted nor excluded subtrees have
942    /// been added, the result is an empty SEQUENCE (`30 00`).
943    ///
944    /// The permitted subtrees, if any, are wrapped in `[0] IMPLICIT` (tag
945    /// `A0`); excluded subtrees in `[1] IMPLICIT` (tag `A1`), following
946    /// RFC 5280 §4.2.1.10.
947    ///
948    /// Returns `Err(String)` if any entry failed DER encoding; the error
949    /// message describes the first failure encountered.
950    pub fn build(self) -> Result<Vec<u8>, String> {
951        if let Some(e) = self.error {
952            return Err(e);
953        }
954
955        // Wrap pre-encoded `GeneralSubtree` TLVs in an IMPLICIT CONSTRUCTED
956        // context tag.  The SEQUENCE OF tag (0x30) is replaced by [N] keeping
957        // the CONSTRUCTED bit: A0 for [0], A1 for [1].
958        let wrap_implicit = |tag_byte: u8, content: Vec<u8>| -> Vec<u8> {
959            let mut out = Vec::with_capacity(content.len() + 4);
960            out.push(tag_byte);
961            let len = content.len();
962            if len < 128 {
963                out.push(len as u8);
964            } else if len < 256 {
965                out.push(0x81);
966                out.push(len as u8);
967            } else if len < 65536 {
968                out.push(0x82);
969                out.push((len >> 8) as u8);
970                out.push((len & 0xFF) as u8);
971            } else {
972                out.push(0x83);
973                out.push((len >> 16) as u8);
974                out.push((len >> 8) as u8);
975                out.push((len & 0xFF) as u8);
976            }
977            out.extend_from_slice(&content);
978            out
979        };
980
981        let mut content = Vec::new();
982        if !self.permitted_bytes.is_empty() {
983            content.extend_from_slice(&wrap_implicit(0xA0, self.permitted_bytes));
984        }
985        if !self.excluded_bytes.is_empty() {
986            content.extend_from_slice(&wrap_implicit(0xA1, self.excluded_bytes));
987        }
988        Ok(encode_sequence(content))
989    }
990}
991
992// ── CRLDistributionPointsBuilder ──────────────────────────────────────────────
993
994/// Fluent builder for a `CRLDistributionPoints` extension value (OID 2.5.29.31).
995///
996/// Add distribution point entries with the fluent methods, then call
997/// [`build`][Self::build] to obtain the DER bytes of the
998/// `CRLDistributionPoints` SEQUENCE OF — the content that goes inside the
999/// OCTET STRING wrapper in the `Extension` SEQUENCE.
1000///
1001/// Each entry is encoded as a [`DistributionPoint`] SEQUENCE using the
1002/// generated type's `Encode` implementation.
1003///
1004/// # Example
1005///
1006/// ```rust,ignore
1007/// use synta_certificate::CRLDistributionPointsBuilder;
1008///
1009/// let cdp_der = CRLDistributionPointsBuilder::new()
1010///     .full_name_uri("http://crl.example.com/ca.crl")
1011///     .build()
1012///     .unwrap();
1013/// ```
1014#[derive(Debug, Default)]
1015pub struct CRLDistributionPointsBuilder {
1016    /// Pre-encoded `DistributionPoint` TLVs accumulated as raw DER bytes.
1017    encoded: Vec<u8>,
1018    /// First encoding error encountered.
1019    error: Option<String>,
1020}
1021
1022impl CRLDistributionPointsBuilder {
1023    /// Create a new, empty `CRLDistributionPointsBuilder`.
1024    pub fn new() -> Self {
1025        Self::default()
1026    }
1027
1028    /// Encode a `DistributionPoint` with a `fullName` containing a single
1029    /// `GeneralName` and append the TLV bytes to the buffer.
1030    fn push_full_name(&mut self, spec: GeneralNameSpec) {
1031        if self.error.is_some() {
1032            return;
1033        }
1034        let gn = match spec.to_general_name() {
1035            Ok(gn) => gn,
1036            Err(e) => {
1037                self.error = Some(format!("GeneralName error: {e}"));
1038                return;
1039            }
1040        };
1041        let dp = DistributionPoint {
1042            distribution_point: Some(DistributionPointName::FullName(vec![gn])),
1043            reasons: None,
1044            c_rlissuer: None,
1045        };
1046        match dp.to_der() {
1047            Ok(bytes) => self.encoded.extend_from_slice(&bytes),
1048            Err(e) => {
1049                self.error = Some(format!("DistributionPoint DER encoding failed: {e}"));
1050            }
1051        }
1052    }
1053
1054    /// Add a distribution point with a URI as the full name.
1055    pub fn full_name_uri(mut self, uri: &str) -> Self {
1056        self.push_full_name(GeneralNameSpec::uri(uri));
1057        self
1058    }
1059
1060    /// Add a distribution point with a DNS name as the full name.
1061    pub fn full_name_dns(mut self, dns: &str) -> Self {
1062        self.push_full_name(GeneralNameSpec::dns(dns));
1063        self
1064    }
1065
1066    /// Add a distribution point with a directory name as the full name
1067    /// (DER-encoded `Name` SEQUENCE bytes).
1068    pub fn full_name_directory(mut self, name_der: &[u8]) -> Self {
1069        self.push_full_name(GeneralNameSpec::directory_name(name_der));
1070        self
1071    }
1072
1073    /// Build the DER-encoded `CRLDistributionPoints` SEQUENCE OF.
1074    ///
1075    /// Returns an empty `SEQUENCE` (`30 00`) if no entries have been added.
1076    /// Returns `Err` if any entry could not be DER-encoded.
1077    pub fn build(self) -> Result<Vec<u8>, String> {
1078        if let Some(e) = self.error {
1079            return Err(e);
1080        }
1081        Ok(encode_sequence(self.encoded))
1082    }
1083}
1084
1085// ── IssuingDistributionPointBuilder ───────────────────────────────────────────
1086
1087/// Fluent builder for an `IssuingDistributionPoint` extension value
1088/// (OID 2.5.29.28, RFC 5280 Section 5.2.5).
1089///
1090/// Set the optional distribution point name and/or the boolean flags, then
1091/// call [`build`][Self::build] to obtain the DER bytes of the
1092/// `IssuingDistributionPoint` SEQUENCE — the content that goes inside the
1093/// OCTET STRING wrapper in the `Extension` SEQUENCE.
1094///
1095/// All boolean flags default to `false` (omitted in DER per DEFAULT FALSE
1096/// semantics).  The distribution point is optional.
1097///
1098/// Uses the generated [`IssuingDistributionPoint`] type with
1099/// IMPLICIT-tagged BOOLEAN fields (as specified in RFC 5280).
1100///
1101/// # Example
1102///
1103/// ```rust,ignore
1104/// use synta_certificate::IssuingDistributionPointBuilder;
1105///
1106/// let idp_der = IssuingDistributionPointBuilder::new()
1107///     .full_name_uri("http://crl.example.com/ca.crl")
1108///     .only_contains_user_certs(true)
1109///     .build()
1110///     .unwrap();
1111/// ```
1112#[derive(Debug, Default)]
1113pub struct IssuingDistributionPointBuilder {
1114    /// Pre-encoded `DistributionPointName` DER bytes, if set.
1115    distribution_point: Option<Vec<u8>>,
1116    /// `onlyContainsUserCerts` boolean flag.
1117    only_contains_user_certs: bool,
1118    /// `onlyContainsCACerts` boolean flag.
1119    only_contains_cacerts: bool,
1120    /// `indirectCRL` boolean flag.
1121    indirect_crl: bool,
1122    /// `onlyContainsAttributeCerts` boolean flag.
1123    only_contains_attribute_certs: bool,
1124    /// First encoding error encountered.
1125    error: Option<String>,
1126}
1127
1128impl IssuingDistributionPointBuilder {
1129    /// Create a new `IssuingDistributionPointBuilder` with all fields absent/false.
1130    pub fn new() -> Self {
1131        Self::default()
1132    }
1133
1134    /// Set the distribution point to a URI full name.
1135    pub fn full_name_uri(mut self, uri: &str) -> Self {
1136        if self.error.is_some() {
1137            return self;
1138        }
1139        let spec = GeneralNameSpec::uri(uri);
1140        let gn = match spec.to_general_name() {
1141            Ok(gn) => gn,
1142            Err(e) => {
1143                self.error = Some(format!("GeneralName error: {e}"));
1144                return self;
1145            }
1146        };
1147        let dpn = DistributionPointName::FullName(vec![gn]);
1148        match dpn.to_der() {
1149            Ok(bytes) => self.distribution_point = Some(bytes),
1150            Err(e) => {
1151                self.error = Some(format!("DistributionPointName DER encoding failed: {e}"));
1152            }
1153        }
1154        self
1155    }
1156
1157    /// Set the distribution point to a DNS name full name.
1158    pub fn full_name_dns(mut self, dns: &str) -> Self {
1159        if self.error.is_some() {
1160            return self;
1161        }
1162        let spec = GeneralNameSpec::dns(dns);
1163        let gn = match spec.to_general_name() {
1164            Ok(gn) => gn,
1165            Err(e) => {
1166                self.error = Some(format!("GeneralName error: {e}"));
1167                return self;
1168            }
1169        };
1170        let dpn = DistributionPointName::FullName(vec![gn]);
1171        match dpn.to_der() {
1172            Ok(bytes) => self.distribution_point = Some(bytes),
1173            Err(e) => {
1174                self.error = Some(format!("DistributionPointName DER encoding failed: {e}"));
1175            }
1176        }
1177        self
1178    }
1179
1180    /// Set `onlyContainsUserCerts`.
1181    pub fn only_contains_user_certs(mut self, val: bool) -> Self {
1182        self.only_contains_user_certs = val;
1183        self
1184    }
1185
1186    /// Set `onlyContainsCACerts`.
1187    pub fn only_contains_cacerts(mut self, val: bool) -> Self {
1188        self.only_contains_cacerts = val;
1189        self
1190    }
1191
1192    /// Set `indirectCRL`.
1193    pub fn indirect_crl(mut self, val: bool) -> Self {
1194        self.indirect_crl = val;
1195        self
1196    }
1197
1198    /// Set `onlyContainsAttributeCerts`.
1199    pub fn only_contains_attribute_certs(mut self, val: bool) -> Self {
1200        self.only_contains_attribute_certs = val;
1201        self
1202    }
1203
1204    /// Build the DER-encoded `IssuingDistributionPoint` SEQUENCE.
1205    ///
1206    /// Returns `Err` if the distribution point could not be re-decoded or
1207    /// DER encoding of the final structure fails.
1208    pub fn build(self) -> Result<Vec<u8>, String> {
1209        if let Some(e) = self.error {
1210            return Err(e);
1211        }
1212
1213        // Decode the pre-encoded DistributionPointName bytes (if any) back into
1214        // the generated type so we can pass it to IssuingDistributionPoint.
1215        // Both decode and encode happen before `dp_bytes` is dropped.
1216        let dp_bytes = self.distribution_point;
1217        let distribution_point = if let Some(ref bytes) = dp_bytes {
1218            let dp_name = synta::Decoder::new(bytes, synta::Encoding::Der)
1219                .decode::<DistributionPointName<'_>>()
1220                .map_err(|e| format!("DistributionPointName re-decode failed: {e}"))?;
1221            Some(dp_name)
1222        } else {
1223            None
1224        };
1225
1226        let idp = IssuingDistributionPoint {
1227            distribution_point,
1228            only_contains_user_certs: self
1229                .only_contains_user_certs
1230                .then_some(synta::Boolean(true)),
1231            only_contains_cacerts: self.only_contains_cacerts.then_some(synta::Boolean(true)),
1232            only_some_reasons: None,
1233            indirect_crl: self.indirect_crl.then_some(synta::Boolean(true)),
1234            only_contains_attribute_certs: self
1235                .only_contains_attribute_certs
1236                .then_some(synta::Boolean(true)),
1237        };
1238
1239        idp.to_der()
1240            .map_err(|e| format!("IssuingDistributionPoint DER encoding failed: {e}"))
1241    }
1242}
1243
1244// ── CertificatePoliciesBuilder ────────────────────────────────────────────────
1245
1246/// Fluent builder for a `CertificatePolicies` extension value (OID 2.5.29.32).
1247///
1248/// Add policy entries (with or without a CPS URI qualifier) using the fluent
1249/// methods, then call [`build`][Self::build] to obtain the DER bytes of the
1250/// `CertificatePolicies` SEQUENCE OF — the content that goes inside the
1251/// OCTET STRING wrapper in the `Extension` SEQUENCE.
1252///
1253/// Each [`PolicyInformation`] is encoded immediately using the generated
1254/// type's `Encode` implementation.
1255///
1256/// # Example
1257///
1258/// ```rust,ignore
1259/// use synta_certificate::{CertificatePoliciesBuilder, oids};
1260///
1261/// let cp_der = CertificatePoliciesBuilder::new()
1262///     .add_policy(oids::PKIX_CE_ANY_POLICY)
1263///     .add_policy_cps(&[2, 23, 140, 1, 2, 1], "https://example.com/cps")
1264///     .build()
1265///     .unwrap();
1266/// ```
1267#[derive(Debug, Default)]
1268pub struct CertificatePoliciesBuilder {
1269    /// Pre-encoded `PolicyInformation` TLVs accumulated as raw DER bytes.
1270    encoded: Vec<u8>,
1271    /// First encoding error encountered.
1272    error: Option<String>,
1273}
1274
1275impl CertificatePoliciesBuilder {
1276    /// Create a new, empty `CertificatePoliciesBuilder`.
1277    pub fn new() -> Self {
1278        Self::default()
1279    }
1280
1281    /// Add a policy identified by `policy_oid` with no qualifiers.
1282    pub fn add_policy(mut self, policy_oid: &[u32]) -> Self {
1283        if self.error.is_some() {
1284            return self;
1285        }
1286        let policy_identifier = match synta::ObjectIdentifier::new(policy_oid) {
1287            Ok(oid) => oid,
1288            Err(e) => {
1289                self.error = Some(format!("invalid policy OID: {e}"));
1290                return self;
1291            }
1292        };
1293        let pi = PolicyInformation {
1294            policy_identifier,
1295            policy_qualifiers: None,
1296        };
1297        match pi.to_der() {
1298            Ok(bytes) => self.encoded.extend_from_slice(&bytes),
1299            Err(e) => self.error = Some(format!("PolicyInformation DER encoding failed: {e}")),
1300        }
1301        self
1302    }
1303
1304    /// Add a policy with a CPS URI qualifier (`id-qt-cps`, 1.3.6.1.5.5.7.2.1).
1305    ///
1306    /// `cps_uri` must be ASCII; returns an error via [`build`][Self::build]
1307    /// if it contains non-ASCII characters.
1308    pub fn add_policy_cps(mut self, policy_oid: &[u32], cps_uri: &str) -> Self {
1309        if self.error.is_some() {
1310            return self;
1311        }
1312        let policy_identifier = match synta::ObjectIdentifier::new(policy_oid) {
1313            Ok(oid) => oid,
1314            Err(e) => {
1315                self.error = Some(format!("invalid policy OID: {e}"));
1316                return self;
1317            }
1318        };
1319        // id-qt-cps = 1.3.6.1.5.5.7.2.1
1320        let cps_oid = synta::ObjectIdentifier::new(&[1, 3, 6, 1, 5, 5, 7, 2, 1])
1321            .expect("id-qt-cps is a valid OID");
1322        let uri_ref = match synta::IA5StringRef::new(cps_uri) {
1323            Ok(r) => r,
1324            Err(e) => {
1325                self.error = Some(format!("CPS URI contains non-ASCII characters: {e}"));
1326                return self;
1327            }
1328        };
1329        // Construct PolicyQualifierInfo within this scope so that the IA5StringRef
1330        // lifetime (bound to cps_uri) stays live until after encode().
1331        let pqi = PolicyQualifierInfo {
1332            policy_qualifier_id: cps_oid,
1333            qualifier: synta::Element::IA5String(uri_ref),
1334        };
1335        let pi = PolicyInformation {
1336            policy_identifier,
1337            policy_qualifiers: Some(vec![pqi]),
1338        };
1339        match pi.to_der() {
1340            Ok(bytes) => self.encoded.extend_from_slice(&bytes),
1341            Err(e) => self.error = Some(format!("PolicyInformation DER encoding failed: {e}")),
1342        }
1343        self
1344    }
1345
1346    /// Build the DER-encoded `CertificatePolicies` SEQUENCE OF.
1347    ///
1348    /// Returns an empty `SEQUENCE` (`30 00`) if no policies have been added.
1349    /// Returns `Err` if any entry could not be DER-encoded.
1350    pub fn build(self) -> Result<Vec<u8>, String> {
1351        if let Some(e) = self.error {
1352            return Err(e);
1353        }
1354        Ok(encode_sequence(self.encoded))
1355    }
1356}
1357
1358#[cfg(test)]
1359mod tests {
1360    use super::*;
1361
1362    /// RFC 5280 §4.2.1.13 requires two IMPLICIT context tags for a fullName CDP:
1363    ///
1364    ///   30 ??          -- CRLDistributionPoints SEQUENCE OF
1365    ///     30 ??        -- DistributionPoint SEQUENCE
1366    ///       A0 ??      -- distributionPoint [0] EXPLICIT (targets CHOICE → forced EXPLICIT)
1367    ///         A0 ??    -- fullName [0] IMPLICIT (replaces GeneralNames SEQUENCE 0x30 tag)
1368    ///           86 ??  -- [6] uniformResourceIdentifier
1369    ///
1370    /// OpenSSL 3.x rejects certificates where the inner A0 is a plain 0x30 SEQUENCE.
1371    #[test]
1372    fn cdp_full_name_uri_correct_implicit_tagging() {
1373        let uri = "http://crl.example.com/ca.crl";
1374        let cdp = CRLDistributionPointsBuilder::new()
1375            .full_name_uri(uri)
1376            .build()
1377            .expect("CDP build must succeed");
1378
1379        // Byte layout:
1380        //   0: 0x30 (CRLDistributionPoints SEQUENCE)
1381        //   2: 0x30 (DistributionPoint SEQUENCE)
1382        //   4: 0xA0 (distributionPoint [0] EXPLICIT — outer A0)
1383        //   6: 0xA0 (fullName [0] IMPLICIT — inner A0, NOT 0x30 SEQUENCE)
1384        //   8: 0x86 ([6] uniformResourceIdentifier)
1385        assert_eq!(cdp[0], 0x30, "outer SEQUENCE tag");
1386        assert_eq!(cdp[2], 0x30, "DistributionPoint SEQUENCE tag");
1387        assert_eq!(cdp[4], 0xA0, "distributionPoint [0] must be A0 (EXPLICIT)");
1388        assert_eq!(
1389            cdp[6], 0xA0,
1390            "fullName [0] IMPLICIT must be A0 (not 0x30 SEQUENCE), got 0x{:02x}",
1391            cdp[6]
1392        );
1393        assert_eq!(cdp[8], 0x86, "[6] uniformResourceIdentifier tag");
1394        let uri_len = cdp[9] as usize;
1395        assert_eq!(&cdp[10..10 + uri_len], uri.as_bytes());
1396    }
1397
1398    /// Round-trip: build a CDP, decode it with the generated DistributionPoint type,
1399    /// and verify the URI survives.
1400    #[cfg(feature = "derive")]
1401    #[test]
1402    fn cdp_full_name_uri_roundtrip() {
1403        use crate::{DistributionPoint, DistributionPointName, GeneralName};
1404
1405        let uri = "http://crl.example.com/ca.crl";
1406        let cdp_der = CRLDistributionPointsBuilder::new()
1407            .full_name_uri(uri)
1408            .build()
1409            .expect("CDP build must succeed");
1410
1411        // Decode the outer SEQUENCE OF (skip 2-byte wrapper)
1412        let inner = &cdp_der[2..];
1413        let mut dec = synta::Decoder::new(inner, synta::Encoding::Der);
1414        let dp: DistributionPoint<'_> = dec.decode().expect("DistributionPoint must decode");
1415
1416        let names = match dp
1417            .distribution_point
1418            .expect("distributionPoint must be present")
1419        {
1420            DistributionPointName::FullName(names) => names,
1421            other => panic!("expected FullName, got {:?}", other),
1422        };
1423        assert_eq!(names.len(), 1);
1424        match &names[0] {
1425            GeneralName::UniformResourceIdentifier(s) => {
1426                assert_eq!(s.as_str(), uri);
1427            }
1428            other => panic!("expected URI GeneralName, got {:?}", other),
1429        }
1430    }
1431}