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}