Skip to main content

kipuka/routes/
csrattrs.rs

1//! `GET /.well-known/est/csrattrs` — CSR Attributes Request.
2//!
3//! RFC 7030 §4.5: EST clients request the CSR attributes that the
4//! server expects in enrollment requests.  The response tells the
5//! client which algorithms, extensions, and subject fields to include
6//! in its PKCS#10 CSR.
7//!
8//! ## CSR Attributes Template (RFC 9908)
9//!
10//! When `csr_template` is configured, the response also includes a
11//! `CertificationRequestInfoTemplate` attribute (OID
12//! `1.2.840.113549.1.9.16.2.61`) that partially pre-fills a
13//! CertificationRequestInfo, guiding the client on subject DN,
14//! key algorithm, and required extensions.  The template coexists
15//! with the OID-list mode for backward compatibility.
16//!
17//! No authentication required per RFC 7030 §4.5.
18//!
19//! Per-label attribute variation is supported (RHELBU-3536 R31):
20//! different EST labels can advertise different required attributes
21//! based on their enrollment profile.
22
23use std::sync::Arc;
24
25use axum::extract::State;
26use axum::http::{HeaderValue, StatusCode, header};
27use axum::response::{IntoResponse, Response};
28
29use crate::auth::OptionalAuth;
30use crate::config::{CsrTemplate, CsrTemplateRdn};
31use crate::error::KipukaError;
32use crate::routes::LabelExtractor;
33use crate::routes::est::{content_types, encode_est_base64};
34use crate::state::AppState;
35
36/// Well-known OIDs for CSR attributes.
37pub mod oids {
38    /// challengePassword (1.2.840.113549.1.9.7) — used for POP linking.
39    pub const CHALLENGE_PASSWORD: &str = "1.2.840.113549.1.9.7";
40
41    /// extensionRequest (1.2.840.113549.1.9.14) — certificate extension request.
42    pub const EXTENSION_REQUEST: &str = "1.2.840.113549.1.9.14";
43
44    /// ecPublicKey (1.2.840.10045.2.1) — EC key algorithm.
45    pub const EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
46
47    /// rsaEncryption (1.2.840.113549.1.1.1) — RSA key algorithm.
48    pub const RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
49
50    /// id-ecPublicKey secp256r1 (1.2.840.10045.3.1.7) — P-256 curve.
51    pub const SECP256R1: &str = "1.2.840.10045.3.1.7";
52
53    /// id-ecPublicKey secp384r1 (1.3.132.0.34) — P-384 curve.
54    pub const SECP384R1: &str = "1.3.132.0.34";
55
56    /// keyUsage (2.5.29.15).
57    pub const KEY_USAGE: &str = "2.5.29.15";
58
59    /// extKeyUsage (2.5.29.37).
60    pub const EXT_KEY_USAGE: &str = "2.5.29.37";
61
62    /// subjectAltName (2.5.29.17).
63    pub const SUBJECT_ALT_NAME: &str = "2.5.29.17";
64
65    /// id-aa-certificationRequestInfoTemplate (1.2.840.113549.1.9.16.2.61)
66    /// — RFC 9908 §3.4, IANA Table 2.
67    pub const CSR_TEMPLATE: &str = "1.2.840.113549.1.9.16.2.61";
68
69    /// OID components for id-aa-certificationRequestInfoTemplate.
70    pub const CSR_TEMPLATE_COMPONENTS: &[u32] = &[1, 2, 840, 113549, 1, 9, 16, 2, 61];
71
72    /// id-aa-extensionReqTemplate (1.2.840.113549.1.9.16.2.62)
73    /// — RFC 9908 §3.4, IANA Table 2.
74    pub const EXTENSION_REQ_TEMPLATE: &str = "1.2.840.113549.1.9.16.2.62";
75
76    /// OID components for id-aa-extensionReqTemplate.
77    pub const EXTENSION_REQ_TEMPLATE_COMPONENTS: &[u32] = &[1, 2, 840, 113549, 1, 9, 16, 2, 62];
78
79    // ── X.500 attribute type OIDs used in CSR templates ──────────────
80
81    /// commonName (2.5.4.3).
82    pub const COMMON_NAME: &str = "2.5.4.3";
83
84    /// organizationName (2.5.4.10).
85    pub const ORGANIZATION_NAME: &str = "2.5.4.10";
86
87    /// countryName (2.5.4.6).
88    pub const COUNTRY_NAME: &str = "2.5.4.6";
89
90    /// stateOrProvinceName (2.5.4.8).
91    pub const STATE_OR_PROVINCE: &str = "2.5.4.8";
92
93    /// localityName (2.5.4.7).
94    pub const LOCALITY_NAME: &str = "2.5.4.7";
95
96    /// organizationalUnitName (2.5.4.11).
97    pub const ORG_UNIT_NAME: &str = "2.5.4.11";
98}
99
100/// `GET /.well-known/est/csrattrs`
101///
102/// Returns the CSR attributes that the server expects in enrollment
103/// requests for the resolved label.
104///
105/// # Response
106///
107/// | Header         | Value                 |
108/// |----------------|-----------------------|
109/// | Status         | `200 OK` or `204 No Content` |
110/// | Content-Type   | `application/csrattrs` |
111///
112/// The body is a base64-encoded DER sequence of `AttrOrOID` values
113/// (RFC 7030 §4.5.2):
114///
115/// ```asn1
116/// CsrAttrs ::= SEQUENCE SIZE (1..MAX) OF AttrOrOID
117/// AttrOrOID ::= CHOICE {
118///     oid OBJECT IDENTIFIER,
119///     attribute Attribute
120/// }
121/// ```
122///
123/// # Authentication
124///
125/// No authentication required per RFC 7030 §4.5.
126///
127/// # Errors
128///
129/// - `404 Not Found` — unknown EST label
130/// - `500 Internal Server Error` — attribute encoding failure
131pub async fn get_csrattrs(
132    _auth: OptionalAuth,
133    label: LabelExtractor,
134    State(state): State<Arc<AppState>>,
135) -> Result<Response, KipukaError> {
136    let ca_id = label.ca_id();
137
138    tracing::debug!(
139        ca_id = %ca_id,
140        label = %label.label,
141        "serving CSR attributes"
142    );
143
144    // Determine the attribute set: per-label overrides global.
145    let attributes = if !label.csr_attributes.is_empty() {
146        &label.csr_attributes
147    } else {
148        &state.config.est.csr_attributes
149    };
150
151    // Determine the CSR template: per-label overrides global.
152    let template = label
153        .csr_template
154        .as_ref()
155        .or(state.config.est.csr_template.as_ref());
156
157    // If no attributes and no template are configured, return 204 No Content
158    // per RFC 7030 §4.5.1.
159    let has_template = template.is_some_and(|t| t.has_content());
160    if attributes.is_empty() && !has_template {
161        return Ok(StatusCode::NO_CONTENT.into_response());
162    }
163
164    // Encode the attributes as a DER SEQUENCE of AttrOrOID values.
165    // When a template is configured, the template Attribute is appended
166    // after the OID list within the same CsrAttrs SEQUENCE.
167    let csrattrs_der = encode_csr_attrs_with_template(attributes, template)?;
168
169    let body = encode_est_base64(&csrattrs_der);
170
171    let mut resp = (StatusCode::OK, body).into_response();
172    resp.headers_mut().insert(
173        header::CONTENT_TYPE,
174        HeaderValue::from_static(content_types::CSR_ATTRS),
175    );
176    resp.headers_mut().insert(
177        header::HeaderName::from_static("content-transfer-encoding"),
178        HeaderValue::from_static(content_types::TRANSFER_ENCODING_BASE64),
179    );
180
181    Ok(resp)
182}
183
184/// Encode CSR attribute OID strings into DER format.
185///
186/// Produces the RFC 7030 §4.5.2 `CsrAttrs` structure:
187///
188/// ```asn1
189/// CsrAttrs ::= SEQUENCE SIZE (1..MAX) OF AttrOrOID
190/// AttrOrOID ::= CHOICE {
191///     oid       OBJECT IDENTIFIER,
192///     attribute Attribute {{ ... }}
193/// }
194/// ```
195///
196/// Each configured OID string is resolved against `synta_certificate::oids`
197/// well-known constants where possible, falling back to dotted-decimal
198/// parsing for custom OIDs.
199pub(crate) fn encode_csr_attrs(oid_strings: &[String]) -> Result<Vec<u8>, KipukaError> {
200    use synta::{Encoder, Encoding, ObjectIdentifier, Tag, tag};
201
202    if oid_strings.is_empty() {
203        return Ok(Vec::new());
204    }
205
206    let seq_tag = Tag::universal_constructed(tag::TAG_SEQUENCE);
207    let mut enc = Encoder::new(Encoding::Der);
208
209    // CsrAttrs SEQUENCE
210    enc.start_constructed_no_guard(seq_tag)
211        .map_err(|e| KipukaError::Internal(format!("CsrAttrs SEQUENCE: {e}")))?;
212
213    for oid_str in oid_strings {
214        // Resolve the OID string to components. Try well-known OIDs first,
215        // then fall back to dotted-decimal parsing.
216        let components = resolve_oid_components(oid_str)?;
217        let oid = ObjectIdentifier::new(&components)
218            .map_err(|e| KipukaError::Internal(format!("invalid OID '{oid_str}': {e}")))?;
219
220        enc.encode(&oid)
221            .map_err(|e| KipukaError::Internal(format!("OID encode '{oid_str}': {e}")))?;
222    }
223
224    // Close CsrAttrs SEQUENCE
225    enc.end_constructed()
226        .map_err(|e| KipukaError::Internal(format!("CsrAttrs end: {e}")))?;
227
228    let der_bytes = enc
229        .finish()
230        .map_err(|e| KipukaError::Internal(format!("CsrAttrs DER finish: {e}")))?;
231
232    tracing::debug!(
233        num_oids = oid_strings.len(),
234        der_len = der_bytes.len(),
235        "encoded CSR attributes"
236    );
237
238    Ok(der_bytes)
239}
240
241/// Resolve an OID string to its component integers.
242///
243/// First checks known OID constants from `synta_certificate::oids` (avoids
244/// hardcoding), then falls back to parsing the dotted-decimal string.
245fn resolve_oid_components(oid_str: &str) -> Result<Vec<u32>, KipukaError> {
246    // Map well-known OID strings to synta-certificate constants.
247    let components: &[u32] = match oid_str {
248        oids::CHALLENGE_PASSWORD => synta_certificate::oids::PKCS9_CHALLENGE_PASSWORD,
249        oids::EXTENSION_REQUEST => synta_certificate::oids::PKCS9_EXTENSION_REQUEST,
250        oids::EC_PUBLIC_KEY => synta_certificate::oids::EC_PUBLIC_KEY,
251        oids::RSA_ENCRYPTION => synta_certificate::oids::RSA_ENCRYPTION,
252        oids::KEY_USAGE => synta_certificate::oids::KEY_USAGE,
253        oids::EXT_KEY_USAGE => synta_certificate::oids::EXTENDED_KEY_USAGE,
254        oids::SUBJECT_ALT_NAME => synta_certificate::oids::SUBJECT_ALT_NAME,
255        oids::SECP256R1 => synta_certificate::oids::EC_CURVE_P256,
256        oids::SECP384R1 => synta_certificate::oids::EC_CURVE_P384,
257        _ => {
258            // Fall back to parsing dotted-decimal OID string.
259            let parts: Result<Vec<u32>, _> = oid_str.split('.').map(|s| s.parse::<u32>()).collect();
260            return parts.map_err(|e| {
261                KipukaError::Internal(format!("invalid OID string '{oid_str}': {e}"))
262            });
263        }
264    };
265    Ok(components.to_vec())
266}
267
268/// Encode CSR attribute OIDs and an optional RFC 9908 template into DER.
269///
270/// When `template` is `None`, this is equivalent to [`encode_csr_attrs`].
271/// When a template is present, the template `Attribute` is appended to
272/// the `CsrAttrs` SEQUENCE after the OID list entries.  Clients that
273/// understand the template use it exclusively; legacy clients ignore the
274/// unrecognised Attribute and process only the OID list.
275pub(crate) fn encode_csr_attrs_with_template(
276    oid_strings: &[String],
277    template: Option<&CsrTemplate>,
278) -> Result<Vec<u8>, KipukaError> {
279    use synta::{Encoder, Encoding, ObjectIdentifier, Tag, tag};
280
281    // Filter to a template reference only when it has content.
282    let template = template.filter(|t| t.has_content());
283
284    if oid_strings.is_empty() && template.is_none() {
285        return Ok(Vec::new());
286    }
287
288    // When there is no template, use the simpler OID-only encoder.
289    let Some(template) = template else {
290        return encode_csr_attrs(oid_strings);
291    };
292
293    let seq_tag = Tag::universal_constructed(tag::TAG_SEQUENCE);
294    let mut enc = Encoder::new(Encoding::Der);
295
296    // CsrAttrs SEQUENCE
297    enc.start_constructed_no_guard(seq_tag)
298        .map_err(|e| KipukaError::Internal(format!("CsrAttrs SEQUENCE: {e}")))?;
299
300    // Encode OID list entries (backward-compatible with legacy clients).
301    for oid_str in oid_strings {
302        let components = resolve_oid_components(oid_str)?;
303        let oid = ObjectIdentifier::new(&components)
304            .map_err(|e| KipukaError::Internal(format!("invalid OID '{oid_str}': {e}")))?;
305        enc.encode(&oid)
306            .map_err(|e| KipukaError::Internal(format!("OID encode '{oid_str}': {e}")))?;
307    }
308
309    // Append the RFC 9908 template Attribute.
310    let attr_der = encode_template_attribute(template)?;
311    enc.write_bytes(&attr_der);
312
313    enc.end_constructed()
314        .map_err(|e| KipukaError::Internal(format!("CsrAttrs end: {e}")))?;
315
316    let der_bytes = enc
317        .finish()
318        .map_err(|e| KipukaError::Internal(format!("CsrAttrs DER finish: {e}")))?;
319
320    tracing::debug!(
321        num_oids = oid_strings.len(),
322        has_template = true,
323        der_len = der_bytes.len(),
324        "encoded CSR attributes with template"
325    );
326
327    Ok(der_bytes)
328}
329
330/// Encode the RFC 9908 template as an ASN.1 `Attribute`:
331///
332/// ```asn1
333/// Attribute ::= SEQUENCE {
334///     type   OID (id-aa-certificationRequestInfoTemplate),
335///     values SET OF CertificationRequestInfoTemplate
336/// }
337/// ```
338fn encode_template_attribute(template: &CsrTemplate) -> Result<Vec<u8>, KipukaError> {
339    use synta::{Encoder, Encoding, ObjectIdentifier, Tag, tag};
340
341    let mut enc = Encoder::new(Encoding::Der);
342    let seq_tag = Tag::universal_constructed(tag::TAG_SEQUENCE);
343    let set_tag = Tag::universal_constructed(tag::TAG_SET);
344
345    // Attribute SEQUENCE
346    enc.start_constructed_no_guard(seq_tag)
347        .map_err(|e| KipukaError::Internal(format!("Attribute SEQUENCE: {e}")))?;
348
349    // attrType OID
350    let oid = ObjectIdentifier::new(oids::CSR_TEMPLATE_COMPONENTS)
351        .map_err(|e| KipukaError::Internal(format!("template OID: {e}")))?;
352    enc.encode(&oid)
353        .map_err(|e| KipukaError::Internal(format!("template OID encode: {e}")))?;
354
355    // attrValues SET OF CertificationRequestInfoTemplate
356    enc.start_constructed_no_guard(set_tag)
357        .map_err(|e| KipukaError::Internal(format!("attrValues SET: {e}")))?;
358
359    let cri_der = encode_cri_template(template)?;
360    enc.write_bytes(&cri_der);
361
362    enc.end_constructed()
363        .map_err(|e| KipukaError::Internal(format!("attrValues SET end: {e}")))?;
364
365    // Close Attribute SEQUENCE
366    enc.end_constructed()
367        .map_err(|e| KipukaError::Internal(format!("Attribute end: {e}")))?;
368
369    enc.finish()
370        .map_err(|e| KipukaError::Internal(format!("Attribute DER finish: {e}")))
371}
372
373/// Encode the inner `CertificationRequestInfoTemplate` SEQUENCE:
374///
375/// ```asn1
376/// CertificationRequestInfoTemplate ::= SEQUENCE {
377///     version       INTEGER (0),
378///     subject       NameTemplate OPTIONAL,
379///     subjectPKInfo [0] SubjectPublicKeyInfoTemplate OPTIONAL,
380///     attributes    [1] Attributes{{ CRIAttributes }}
381/// }
382/// ```
383///
384/// Note: `attributes` is **not** OPTIONAL per RFC 9908 — it is always
385/// present.  When no extension templates are configured, an empty
386/// `[1] {}` (DER `A1 00`) is emitted.
387fn encode_cri_template(template: &CsrTemplate) -> Result<Vec<u8>, KipukaError> {
388    use synta::{Encoder, Encoding, Integer, Tag, tag};
389
390    let mut enc = Encoder::new(Encoding::Der);
391    let seq_tag = Tag::universal_constructed(tag::TAG_SEQUENCE);
392
393    enc.start_constructed_no_guard(seq_tag)
394        .map_err(|e| KipukaError::Internal(format!("CRI template SEQUENCE: {e}")))?;
395
396    // version INTEGER (0)
397    let version = Integer::from_i64(0);
398    enc.encode(&version)
399        .map_err(|e| KipukaError::Internal(format!("CRI version: {e}")))?;
400
401    // subject NameTemplate OPTIONAL
402    if !template.subject.is_empty() {
403        let name_der = encode_subject_template(&template.subject)?;
404        enc.write_bytes(&name_der);
405    }
406
407    // subjectPKInfo [0] IMPLICIT SubjectPublicKeyInfoTemplate OPTIONAL
408    if let Some(ref key_alg) = template.key_algorithm {
409        let spki_der = encode_spki_template(key_alg)?;
410        let ctx0 = Tag::context_specific_constructed(0);
411        enc.start_constructed_no_guard(ctx0)
412            .map_err(|e| KipukaError::Internal(format!("[0] SPKI tag: {e}")))?;
413        enc.write_bytes(&spki_der);
414        enc.end_constructed()
415            .map_err(|e| KipukaError::Internal(format!("[0] SPKI end: {e}")))?;
416    }
417
418    // attributes [1] IMPLICIT SET OF Attribute — mandatory per RFC 9908.
419    let ctx1 = Tag::context_specific_constructed(1);
420    if !template.required_extensions.is_empty() {
421        let ext_der = encode_extension_template(&template.required_extensions)?;
422        enc.start_constructed_no_guard(ctx1)
423            .map_err(|e| KipukaError::Internal(format!("[1] attrs tag: {e}")))?;
424        enc.write_bytes(&ext_der);
425        enc.end_constructed()
426            .map_err(|e| KipukaError::Internal(format!("[1] attrs end: {e}")))?;
427    } else {
428        // Empty SET — still required by RFC 9908 (DER: A1 00).
429        enc.start_constructed_no_guard(ctx1)
430            .map_err(|e| KipukaError::Internal(format!("[1] empty attrs tag: {e}")))?;
431        enc.end_constructed()
432            .map_err(|e| KipukaError::Internal(format!("[1] empty attrs end: {e}")))?;
433    }
434
435    enc.end_constructed()
436        .map_err(|e| KipukaError::Internal(format!("CRI template end: {e}")))?;
437
438    enc.finish()
439        .map_err(|e| KipukaError::Internal(format!("CRI template DER finish: {e}")))
440}
441
442/// Encode a NameTemplate (RFC 9908 §3.4) as a DER `Name` SEQUENCE.
443///
444/// Unlike a standard X.509 Name, `SingleAttributeTemplate` allows the
445/// `value` field to be absent — signalling that the client must supply
446/// a value for that RDN type.
447///
448/// ```asn1
449/// SingleAttributeTemplate ::= SEQUENCE {
450///     type  OID,
451///     value ANY OPTIONAL    -- absent = client fills in
452/// }
453/// ```
454fn encode_subject_template(rdns: &[CsrTemplateRdn]) -> Result<Vec<u8>, KipukaError> {
455    use synta::types::string::Utf8StringRef;
456    use synta::{Encoder, Encoding, ObjectIdentifier, Tag, tag};
457
458    let seq_tag = Tag::universal_constructed(tag::TAG_SEQUENCE);
459    let set_tag = Tag::universal_constructed(tag::TAG_SET);
460
461    let mut enc = Encoder::new(Encoding::Der);
462
463    // Name ::= SEQUENCE OF RelativeDistinguishedName
464    enc.start_constructed_no_guard(seq_tag)
465        .map_err(|e| KipukaError::Internal(format!("Name SEQUENCE: {e}")))?;
466
467    for rdn in rdns {
468        let components = resolve_oid_components(&rdn.oid)?;
469        let oid = ObjectIdentifier::new(&components)
470            .map_err(|e| KipukaError::Internal(format!("RDN OID '{}': {e}", rdn.oid)))?;
471
472        // RDN ::= SET OF SingleAttributeTemplate
473        enc.start_constructed_no_guard(set_tag)
474            .map_err(|e| KipukaError::Internal(format!("RDN SET: {e}")))?;
475
476        // SingleAttributeTemplate SEQUENCE
477        enc.start_constructed_no_guard(seq_tag)
478            .map_err(|e| KipukaError::Internal(format!("AttrTypeAndValue SEQUENCE: {e}")))?;
479
480        enc.encode(&oid)
481            .map_err(|e| KipukaError::Internal(format!("RDN OID encode: {e}")))?;
482
483        // Value is optional per RFC 9908 — omit when None.
484        if let Some(value) = &rdn.value {
485            let val = Utf8StringRef::new(value.as_str());
486            enc.encode(&val)
487                .map_err(|e| KipukaError::Internal(format!("RDN value encode: {e}")))?;
488        }
489
490        enc.end_constructed()
491            .map_err(|e| KipukaError::Internal(format!("AttrTypeAndValue end: {e}")))?;
492
493        enc.end_constructed()
494            .map_err(|e| KipukaError::Internal(format!("RDN SET end: {e}")))?;
495    }
496
497    enc.end_constructed()
498        .map_err(|e| KipukaError::Internal(format!("Name end: {e}")))?;
499
500    enc.finish()
501        .map_err(|e| KipukaError::Internal(format!("Name DER finish: {e}")))
502}
503
504/// Encode a `SubjectPublicKeyInfoTemplate` (RFC 9908 §3.4) body.
505///
506/// The encoded bytes are the *content* of the `[0] IMPLICIT` wrapper;
507/// the caller adds the context tag.
508///
509/// ```asn1
510/// SubjectPublicKeyInfoTemplate ::= SEQUENCE {
511///     algorithm        AlgorithmIdentifier,
512///     subjectPublicKey BIT STRING OPTIONAL
513/// }
514/// ```
515///
516/// For EC keys the AlgorithmIdentifier carries the curve OID as a
517/// parameter.  For RSA keys the parameter is NULL.  The
518/// `subjectPublicKey` field is omitted (clients generate their own key).
519fn encode_spki_template(key_alg: &str) -> Result<Vec<u8>, KipukaError> {
520    use synta::{Encoder, Encoding, Null, ObjectIdentifier, Tag, tag};
521
522    let (alg_oid_components, param_oid_components) = parse_key_algorithm(key_alg)?;
523    let seq_tag = Tag::universal_constructed(tag::TAG_SEQUENCE);
524
525    let mut enc = Encoder::new(Encoding::Der);
526
527    // AlgorithmIdentifier SEQUENCE
528    enc.start_constructed_no_guard(seq_tag)
529        .map_err(|e| KipukaError::Internal(format!("AlgId SEQUENCE: {e}")))?;
530
531    let alg_oid = ObjectIdentifier::new(&alg_oid_components)
532        .map_err(|e| KipukaError::Internal(format!("AlgId OID: {e}")))?;
533    enc.encode(&alg_oid)
534        .map_err(|e| KipukaError::Internal(format!("AlgId OID encode: {e}")))?;
535
536    // Parameters: EC → curve OID, RSA → NULL.
537    if let Some(ref param_comps) = param_oid_components {
538        let param_oid = ObjectIdentifier::new(param_comps)
539            .map_err(|e| KipukaError::Internal(format!("curve OID: {e}")))?;
540        enc.encode(&param_oid)
541            .map_err(|e| KipukaError::Internal(format!("curve OID encode: {e}")))?;
542    } else {
543        enc.encode(&Null)
544            .map_err(|e| KipukaError::Internal(format!("NULL param encode: {e}")))?;
545    }
546
547    enc.end_constructed()
548        .map_err(|e| KipukaError::Internal(format!("AlgId end: {e}")))?;
549
550    // subjectPublicKey BIT STRING is omitted — client generates its own.
551
552    enc.finish()
553        .map_err(|e| KipukaError::Internal(format!("SPKI DER finish: {e}")))
554}
555
556/// Parse a key algorithm spec string into OID components.
557///
558/// Supported formats:
559/// - `"ec:P-256"` → (EC_PUBLIC_KEY, Some(EC_CURVE_P256))
560/// - `"ec:P-384"` → (EC_PUBLIC_KEY, Some(EC_CURVE_P384))
561/// - `"ec:P-521"` → (EC_PUBLIC_KEY, Some(EC_CURVE_P521))
562/// - `"rsa:2048"`, `"rsa:4096"`, etc. → (RSA_ENCRYPTION, None)
563///
564/// Returns `(algorithm_oid, optional_parameter_oid)`.
565fn parse_key_algorithm(spec: &str) -> Result<(Vec<u32>, Option<Vec<u32>>), KipukaError> {
566    let parts: Vec<&str> = spec.splitn(2, ':').collect();
567    if parts.len() != 2 {
568        return Err(KipukaError::Internal(format!(
569            "invalid key_algorithm format '{spec}': expected 'ec:<curve>' or 'rsa:<bits>'"
570        )));
571    }
572
573    match parts[0].to_lowercase().as_str() {
574        "ec" | "ecdsa" => {
575            let curve_oid = match parts[1] {
576                "P-256" | "p-256" | "secp256r1" | "prime256v1" => {
577                    synta_certificate::oids::EC_CURVE_P256.to_vec()
578                }
579                "P-384" | "p-384" | "secp384r1" => synta_certificate::oids::EC_CURVE_P384.to_vec(),
580                "P-521" | "p-521" | "secp521r1" => synta_certificate::oids::EC_CURVE_P521.to_vec(),
581                other => {
582                    return Err(KipukaError::Internal(format!(
583                        "unsupported EC curve '{other}'"
584                    )));
585                }
586            };
587            Ok((
588                synta_certificate::oids::EC_PUBLIC_KEY.to_vec(),
589                Some(curve_oid),
590            ))
591        }
592        "rsa" => Ok((synta_certificate::oids::RSA_ENCRYPTION.to_vec(), None)),
593        other => Err(KipukaError::Internal(format!(
594            "unsupported key algorithm family '{other}'"
595        ))),
596    }
597}
598
599/// Encode an extension-request template as an Attribute wrapping
600/// `id-aa-extensionReqTemplate` (OID 1.2.840.113549.1.9.16.2.62).
601///
602/// Each extension OID becomes an `ExtensionTemplate` with no value —
603/// the client must fill in appropriate values.
604///
605/// ```asn1
606/// Attribute ::= SEQUENCE {
607///     type   OID (id-aa-extensionReqTemplate),
608///     values SET OF ExtensionTemplates
609/// }
610/// ExtensionTemplate ::= SEQUENCE {
611///     extnID    OID,
612///     critical  BOOLEAN DEFAULT FALSE,   -- omitted
613///     extnValue OCTET STRING OPTIONAL    -- omitted
614/// }
615/// ```
616fn encode_extension_template(ext_oids: &[String]) -> Result<Vec<u8>, KipukaError> {
617    use synta::{Encoder, Encoding, ObjectIdentifier, Tag, tag};
618
619    let seq_tag = Tag::universal_constructed(tag::TAG_SEQUENCE);
620    let set_tag = Tag::universal_constructed(tag::TAG_SET);
621
622    let mut enc = Encoder::new(Encoding::Der);
623
624    // Attribute SEQUENCE
625    enc.start_constructed_no_guard(seq_tag)
626        .map_err(|e| KipukaError::Internal(format!("ExtReq Attribute SEQUENCE: {e}")))?;
627
628    // attrType = id-aa-extensionReqTemplate
629    let ext_req_oid = ObjectIdentifier::new(oids::EXTENSION_REQ_TEMPLATE_COMPONENTS)
630        .map_err(|e| KipukaError::Internal(format!("extensionReqTemplate OID: {e}")))?;
631    enc.encode(&ext_req_oid)
632        .map_err(|e| KipukaError::Internal(format!("extensionReqTemplate OID encode: {e}")))?;
633
634    // attrValues SET OF ExtensionTemplates
635    enc.start_constructed_no_guard(set_tag)
636        .map_err(|e| KipukaError::Internal(format!("ExtReq SET: {e}")))?;
637
638    // ExtensionTemplates SEQUENCE OF ExtensionTemplate
639    enc.start_constructed_no_guard(seq_tag)
640        .map_err(|e| KipukaError::Internal(format!("ExtensionTemplates SEQUENCE: {e}")))?;
641
642    for ext_oid_str in ext_oids {
643        let components = resolve_oid_components(ext_oid_str)?;
644        let oid = ObjectIdentifier::new(&components)
645            .map_err(|e| KipukaError::Internal(format!("ext OID '{ext_oid_str}': {e}")))?;
646
647        // ExtensionTemplate SEQUENCE { extnID OID }
648        // critical and extnValue are omitted — client fills in.
649        enc.start_constructed_no_guard(seq_tag)
650            .map_err(|e| KipukaError::Internal(format!("ExtensionTemplate SEQUENCE: {e}")))?;
651        enc.encode(&oid)
652            .map_err(|e| KipukaError::Internal(format!("ext OID encode: {e}")))?;
653        enc.end_constructed()
654            .map_err(|e| KipukaError::Internal(format!("ExtensionTemplate end: {e}")))?;
655    }
656
657    enc.end_constructed()
658        .map_err(|e| KipukaError::Internal(format!("ExtensionTemplates end: {e}")))?;
659
660    enc.end_constructed()
661        .map_err(|e| KipukaError::Internal(format!("ExtReq SET end: {e}")))?;
662
663    enc.end_constructed()
664        .map_err(|e| KipukaError::Internal(format!("ExtReq Attribute end: {e}")))?;
665
666    enc.finish()
667        .map_err(|e| KipukaError::Internal(format!("ExtReq DER finish: {e}")))
668}
669
670#[cfg(test)]
671mod tests {
672    use super::*;
673    use synta::{Decoder, Encoding, Length, ObjectIdentifier, Tag, tag};
674
675    fn decode_oid_at(decoder: &mut Decoder<'_>) -> ObjectIdentifier {
676        decoder.decode::<ObjectIdentifier>().expect("decode OID")
677    }
678
679    fn read_tl(decoder: &mut Decoder<'_>) -> (Tag, Length) {
680        let tag = decoder.read_tag().expect("read tag");
681        let len = decoder.read_length().expect("read length");
682        (tag, len)
683    }
684
685    #[test]
686    fn oid_constant_matches_rfc9908() {
687        assert_eq!(oids::CSR_TEMPLATE, "1.2.840.113549.1.9.16.2.61");
688        assert_eq!(
689            oids::CSR_TEMPLATE_COMPONENTS,
690            &[1, 2, 840, 113549, 1, 9, 16, 2, 61]
691        );
692        assert_eq!(oids::EXTENSION_REQ_TEMPLATE, "1.2.840.113549.1.9.16.2.62");
693    }
694
695    #[test]
696    fn encode_template_subject_only() {
697        let template = CsrTemplate {
698            subject: vec![
699                CsrTemplateRdn {
700                    oid: oids::ORGANIZATION_NAME.to_string(),
701                    value: Some("Example Corp".to_string()),
702                },
703                CsrTemplateRdn {
704                    oid: oids::COMMON_NAME.to_string(),
705                    value: None,
706                },
707            ],
708            key_algorithm: None,
709            required_extensions: vec![],
710        };
711
712        let der = encode_csr_attrs_with_template(&[], Some(&template)).unwrap();
713        assert!(!der.is_empty());
714
715        // Outer SEQUENCE (CsrAttrs)
716        let mut dec = Decoder::new(&der, Encoding::Der);
717        let (tag, _len) = read_tl(&mut dec);
718        assert!(tag.is_constructed());
719        assert_eq!(tag.number(), tag::TAG_SEQUENCE);
720
721        // Inside: Attribute SEQUENCE
722        let (tag, _len) = read_tl(&mut dec);
723        assert!(tag.is_constructed());
724        assert_eq!(tag.number(), tag::TAG_SEQUENCE);
725
726        // attrType = id-aa-certificationRequestInfoTemplate
727        let attr_oid = decode_oid_at(&mut dec);
728        let expected = ObjectIdentifier::new(oids::CSR_TEMPLATE_COMPONENTS).unwrap();
729        assert_eq!(attr_oid, expected);
730    }
731
732    #[test]
733    fn encode_template_ec_p256() {
734        let template = CsrTemplate {
735            subject: vec![],
736            key_algorithm: Some("ec:P-256".to_string()),
737            required_extensions: vec![],
738        };
739
740        let der = encode_csr_attrs_with_template(&[], Some(&template)).unwrap();
741        assert!(!der.is_empty());
742
743        // Verify the encoded DER is parseable — walk into the CRI template
744        // and find the AlgorithmIdentifier.
745        let inner = encode_cri_template(&template).unwrap();
746        let mut dec = Decoder::new(&inner, Encoding::Der);
747
748        // CRI SEQUENCE
749        let (tag, _) = read_tl(&mut dec);
750        assert_eq!(tag.number(), tag::TAG_SEQUENCE);
751
752        // version INTEGER (0)
753        let version: synta::Integer = dec.decode().unwrap();
754        assert_eq!(version.as_i64().unwrap(), 0);
755
756        // [0] IMPLICIT SPKI template
757        let (tag, _) = read_tl(&mut dec);
758        assert_eq!(tag.class(), synta::TagClass::ContextSpecific);
759        assert_eq!(tag.number(), 0);
760
761        // AlgorithmIdentifier SEQUENCE
762        let (tag, _) = read_tl(&mut dec);
763        assert_eq!(tag.number(), tag::TAG_SEQUENCE);
764
765        // algorithm OID = ecPublicKey
766        let alg_oid = decode_oid_at(&mut dec);
767        let ec_pk = ObjectIdentifier::new(synta_certificate::oids::EC_PUBLIC_KEY).unwrap();
768        assert_eq!(alg_oid, ec_pk);
769
770        // parameters OID = P-256
771        let curve_oid = decode_oid_at(&mut dec);
772        let p256 = ObjectIdentifier::new(synta_certificate::oids::EC_CURVE_P256).unwrap();
773        assert_eq!(curve_oid, p256);
774    }
775
776    #[test]
777    fn encode_template_rsa() {
778        let template = CsrTemplate {
779            subject: vec![],
780            key_algorithm: Some("rsa:2048".to_string()),
781            required_extensions: vec![],
782        };
783
784        let inner = encode_cri_template(&template).unwrap();
785        let mut dec = Decoder::new(&inner, Encoding::Der);
786
787        // CRI SEQUENCE
788        read_tl(&mut dec);
789        // version
790        let _: synta::Integer = dec.decode().unwrap();
791        // [0] SPKI
792        read_tl(&mut dec);
793        // AlgId SEQUENCE
794        read_tl(&mut dec);
795
796        let alg_oid = decode_oid_at(&mut dec);
797        let rsa = ObjectIdentifier::new(synta_certificate::oids::RSA_ENCRYPTION).unwrap();
798        assert_eq!(alg_oid, rsa);
799
800        // RSA params = NULL
801        let null: synta::Null = dec.decode().unwrap();
802        assert_eq!(null, synta::Null);
803    }
804
805    #[test]
806    fn encode_template_extensions_only() {
807        let template = CsrTemplate {
808            subject: vec![],
809            key_algorithm: None,
810            required_extensions: vec![
811                oids::SUBJECT_ALT_NAME.to_string(),
812                oids::KEY_USAGE.to_string(),
813            ],
814        };
815
816        let der = encode_csr_attrs_with_template(&[], Some(&template)).unwrap();
817        assert!(!der.is_empty());
818
819        // Verify the inner CRI template has [1] attributes.
820        let inner = encode_cri_template(&template).unwrap();
821        let mut dec = Decoder::new(&inner, Encoding::Der);
822
823        // CRI SEQUENCE
824        read_tl(&mut dec);
825        // version
826        let _: synta::Integer = dec.decode().unwrap();
827        // [1] attributes
828        let (tag, _) = read_tl(&mut dec);
829        assert_eq!(tag.class(), synta::TagClass::ContextSpecific);
830        assert_eq!(tag.number(), 1);
831    }
832
833    #[test]
834    fn encode_template_combined_with_oids() {
835        let template = CsrTemplate {
836            subject: vec![CsrTemplateRdn {
837                oid: oids::COMMON_NAME.to_string(),
838                value: Some("test.example.com".to_string()),
839            }],
840            key_algorithm: Some("ec:P-384".to_string()),
841            required_extensions: vec![oids::SUBJECT_ALT_NAME.to_string()],
842        };
843
844        let oid_list = vec![
845            oids::CHALLENGE_PASSWORD.to_string(),
846            oids::EXTENSION_REQUEST.to_string(),
847        ];
848
849        let der = encode_csr_attrs_with_template(&oid_list, Some(&template)).unwrap();
850        assert!(!der.is_empty());
851
852        // Outer CsrAttrs SEQUENCE should contain OIDs + Attribute.
853        let mut dec = Decoder::new(&der, Encoding::Der);
854        let (tag, _) = read_tl(&mut dec);
855        assert_eq!(tag.number(), tag::TAG_SEQUENCE);
856
857        // First two elements: challengePassword and extensionRequest OIDs.
858        let oid1 = decode_oid_at(&mut dec);
859        let challenge =
860            ObjectIdentifier::new(synta_certificate::oids::PKCS9_CHALLENGE_PASSWORD).unwrap();
861        assert_eq!(oid1, challenge);
862
863        let oid2 = decode_oid_at(&mut dec);
864        let ext_req =
865            ObjectIdentifier::new(synta_certificate::oids::PKCS9_EXTENSION_REQUEST).unwrap();
866        assert_eq!(oid2, ext_req);
867
868        // Third element: template Attribute SEQUENCE.
869        let (tag, _) = read_tl(&mut dec);
870        assert!(tag.is_constructed());
871        assert_eq!(tag.number(), tag::TAG_SEQUENCE);
872    }
873
874    #[test]
875    fn absent_rdn_value_omits_value_field() {
876        let rdns = vec![CsrTemplateRdn {
877            oid: oids::COMMON_NAME.to_string(),
878            value: None,
879        }];
880        let name_der = encode_subject_template(&rdns).unwrap();
881
882        // Name SEQUENCE → RDN SET → AttrTypeAndValue SEQUENCE → OID only.
883        let mut dec = Decoder::new(&name_der, Encoding::Der);
884        // Name SEQUENCE
885        read_tl(&mut dec);
886        // RDN SET
887        read_tl(&mut dec);
888        // AttrTypeAndValue SEQUENCE
889        let (_, atv_len) = read_tl(&mut dec);
890        let atv_len = atv_len.definite().unwrap();
891
892        // Decode the OID and measure its size.
893        let oid = decode_oid_at(&mut dec);
894        let cn = ObjectIdentifier::new(synta_certificate::oids::attr::COMMON_NAME).unwrap();
895        assert_eq!(oid, cn);
896
897        // The AttrTypeAndValue should contain only the OID — no value bytes.
898        let oid_encoded_len = synta::ToDer::to_der(&oid).unwrap().len();
899        assert_eq!(atv_len, oid_encoded_len);
900    }
901
902    #[test]
903    fn present_rdn_value_includes_utf8string() {
904        let rdns = vec![CsrTemplateRdn {
905            oid: oids::ORGANIZATION_NAME.to_string(),
906            value: Some("Test Org".to_string()),
907        }];
908        let name_der = encode_subject_template(&rdns).unwrap();
909
910        let mut dec = Decoder::new(&name_der, Encoding::Der);
911        // Name → RDN SET → ATV SEQUENCE
912        read_tl(&mut dec);
913        read_tl(&mut dec);
914        let (_, atv_len) = read_tl(&mut dec);
915        let atv_len = atv_len.definite().unwrap();
916
917        let oid = decode_oid_at(&mut dec);
918        let org = ObjectIdentifier::new(synta_certificate::oids::attr::ORGANIZATION).unwrap();
919        assert_eq!(oid, org);
920
921        // ATV length is larger than OID alone — value is present.
922        let oid_len = synta::ToDer::to_der(&oid).unwrap().len();
923        assert!(atv_len > oid_len);
924    }
925
926    #[test]
927    fn parse_key_algorithm_valid() {
928        let (alg, param) = parse_key_algorithm("ec:P-256").unwrap();
929        assert_eq!(alg, synta_certificate::oids::EC_PUBLIC_KEY);
930        assert_eq!(param.unwrap(), synta_certificate::oids::EC_CURVE_P256);
931
932        let (alg, param) = parse_key_algorithm("ec:P-384").unwrap();
933        assert_eq!(alg, synta_certificate::oids::EC_PUBLIC_KEY);
934        assert_eq!(param.unwrap(), synta_certificate::oids::EC_CURVE_P384);
935
936        let (alg, param) = parse_key_algorithm("ec:P-521").unwrap();
937        assert_eq!(alg, synta_certificate::oids::EC_PUBLIC_KEY);
938        assert_eq!(param.unwrap(), synta_certificate::oids::EC_CURVE_P521);
939
940        let (alg, param) = parse_key_algorithm("rsa:2048").unwrap();
941        assert_eq!(alg, synta_certificate::oids::RSA_ENCRYPTION);
942        assert!(param.is_none());
943
944        let (alg, param) = parse_key_algorithm("rsa:4096").unwrap();
945        assert_eq!(alg, synta_certificate::oids::RSA_ENCRYPTION);
946        assert!(param.is_none());
947    }
948
949    #[test]
950    fn parse_key_algorithm_invalid() {
951        assert!(parse_key_algorithm("dsa:1024").is_err());
952        assert!(parse_key_algorithm("ec:P-999").is_err());
953        assert!(parse_key_algorithm("no-colon").is_err());
954    }
955
956    #[test]
957    fn empty_template_returns_empty() {
958        let template = CsrTemplate {
959            subject: vec![],
960            key_algorithm: None,
961            required_extensions: vec![],
962        };
963        let der = encode_csr_attrs_with_template(&[], Some(&template)).unwrap();
964        assert!(der.is_empty());
965    }
966
967    #[test]
968    fn none_template_delegates_to_oid_only() {
969        let oid_list = vec![oids::CHALLENGE_PASSWORD.to_string()];
970        let with_template = encode_csr_attrs_with_template(&oid_list, None).unwrap();
971        let oid_only = encode_csr_attrs(&oid_list).unwrap();
972        assert_eq!(with_template, oid_only);
973    }
974
975    #[test]
976    fn mandatory_attributes_field_present_when_no_extensions() {
977        let template = CsrTemplate {
978            subject: vec![CsrTemplateRdn {
979                oid: oids::COMMON_NAME.to_string(),
980                value: None,
981            }],
982            key_algorithm: None,
983            required_extensions: vec![],
984        };
985
986        let inner = encode_cri_template(&template).unwrap();
987        let mut dec = Decoder::new(&inner, Encoding::Der);
988
989        // CRI SEQUENCE
990        read_tl(&mut dec);
991        // version
992        let _: synta::Integer = dec.decode().unwrap();
993        // subject Name SEQUENCE
994        let (tag, _) = read_tl(&mut dec);
995        assert_eq!(tag.number(), tag::TAG_SEQUENCE);
996
997        // Skip subject content — read remaining bytes for [1].
998        // Re-decode from scratch and walk past subject.
999        let inner2 = encode_cri_template(&template).unwrap();
1000
1001        // The last two bytes should be A1 00 (empty [1] IMPLICIT SET).
1002        assert!(inner2.len() >= 2);
1003        let tail = &inner2[inner2.len() - 2..];
1004        assert_eq!(tail, &[0xA1, 0x00], "mandatory [1] empty SET missing");
1005    }
1006}