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//! No authentication required per RFC 7030 §4.5.
9//!
10//! Per-label attribute variation is supported (RHELBU-3536 R31):
11//! different EST labels can advertise different required attributes
12//! based on their enrollment profile.
13
14use std::sync::Arc;
15
16use axum::extract::State;
17use axum::http::{HeaderValue, StatusCode, header};
18use axum::response::{IntoResponse, Response};
19
20use crate::auth::OptionalAuth;
21use crate::error::KipukaError;
22use crate::routes::LabelExtractor;
23use crate::routes::est::{content_types, encode_est_base64};
24use crate::state::AppState;
25
26/// Well-known OIDs for CSR attributes.
27pub mod oids {
28    /// challengePassword (1.2.840.113549.1.9.7) — used for POP linking.
29    pub const CHALLENGE_PASSWORD: &str = "1.2.840.113549.1.9.7";
30
31    /// extensionRequest (1.2.840.113549.1.9.14) — certificate extension request.
32    pub const EXTENSION_REQUEST: &str = "1.2.840.113549.1.9.14";
33
34    /// ecPublicKey (1.2.840.10045.2.1) — EC key algorithm.
35    pub const EC_PUBLIC_KEY: &str = "1.2.840.10045.2.1";
36
37    /// rsaEncryption (1.2.840.113549.1.1.1) — RSA key algorithm.
38    pub const RSA_ENCRYPTION: &str = "1.2.840.113549.1.1.1";
39
40    /// id-ecPublicKey secp256r1 (1.2.840.10045.3.1.7) — P-256 curve.
41    pub const SECP256R1: &str = "1.2.840.10045.3.1.7";
42
43    /// id-ecPublicKey secp384r1 (1.3.132.0.34) — P-384 curve.
44    pub const SECP384R1: &str = "1.3.132.0.34";
45
46    /// keyUsage (2.5.29.15).
47    pub const KEY_USAGE: &str = "2.5.29.15";
48
49    /// extKeyUsage (2.5.29.37).
50    pub const EXT_KEY_USAGE: &str = "2.5.29.37";
51
52    /// subjectAltName (2.5.29.17).
53    pub const SUBJECT_ALT_NAME: &str = "2.5.29.17";
54}
55
56/// `GET /.well-known/est/csrattrs`
57///
58/// Returns the CSR attributes that the server expects in enrollment
59/// requests for the resolved label.
60///
61/// # Response
62///
63/// | Header         | Value                 |
64/// |----------------|-----------------------|
65/// | Status         | `200 OK` or `204 No Content` |
66/// | Content-Type   | `application/csrattrs` |
67///
68/// The body is a base64-encoded DER sequence of `AttrOrOID` values
69/// (RFC 7030 §4.5.2):
70///
71/// ```asn1
72/// CsrAttrs ::= SEQUENCE SIZE (1..MAX) OF AttrOrOID
73/// AttrOrOID ::= CHOICE {
74///     oid OBJECT IDENTIFIER,
75///     attribute Attribute
76/// }
77/// ```
78///
79/// # Authentication
80///
81/// No authentication required per RFC 7030 §4.5.
82///
83/// # Errors
84///
85/// - `404 Not Found` — unknown EST label
86/// - `500 Internal Server Error` — attribute encoding failure
87pub async fn get_csrattrs(
88    _auth: OptionalAuth,
89    label: LabelExtractor,
90    State(state): State<Arc<AppState>>,
91) -> Result<Response, KipukaError> {
92    let ca_id = label.ca_id();
93
94    tracing::debug!(
95        ca_id = %ca_id,
96        label = %label.label,
97        "serving CSR attributes"
98    );
99
100    // Determine the attribute set: per-label overrides global.
101    let attributes = if !label.csr_attributes.is_empty() {
102        &label.csr_attributes
103    } else {
104        &state.config.est.csr_attributes
105    };
106
107    // If no attributes are configured, return 204 No Content per RFC 7030 §4.5.1.
108    if attributes.is_empty() {
109        return Ok(StatusCode::NO_CONTENT.into_response());
110    }
111
112    // Encode the attributes as a DER SEQUENCE of OIDs.
113    //
114    // TODO: Replace with proper ASN.1 encoding via `synta` or `der` crate.
115    //
116    // The proper implementation would:
117    // 1. For each OID string, encode it as a DER OBJECT IDENTIFIER
118    // 2. Wrap in a SEQUENCE
119    // 3. Base64-encode the result
120    //
121    // For now, build a placeholder that encodes the OID strings.
122    let csrattrs_der = encode_csr_attrs(attributes)?;
123
124    let body = encode_est_base64(&csrattrs_der);
125
126    let mut resp = (StatusCode::OK, body).into_response();
127    resp.headers_mut().insert(
128        header::CONTENT_TYPE,
129        HeaderValue::from_static(content_types::CSR_ATTRS),
130    );
131    resp.headers_mut().insert(
132        header::HeaderName::from_static("content-transfer-encoding"),
133        HeaderValue::from_static(content_types::TRANSFER_ENCODING_BASE64),
134    );
135
136    Ok(resp)
137}
138
139/// Encode CSR attribute OID strings into DER format.
140///
141/// TODO: Replace with proper ASN.1 construction via `synta` or `der` crate.
142fn encode_csr_attrs(oid_strings: &[String]) -> Result<Vec<u8>, KipukaError> {
143    if oid_strings.is_empty() {
144        return Ok(Vec::new());
145    }
146
147    // Placeholder: encode OID strings into a minimal DER SEQUENCE.
148    //
149    // Real implementation:
150    //   let mut seq = der::asn1::SequenceOf::<der::asn1::ObjectIdentifier, MAX>::new();
151    //   for oid_str in oid_strings {
152    //       let oid = der::asn1::ObjectIdentifier::new(oid_str)?;
153    //       seq.add(oid)?;
154    //   }
155    //   let der_bytes = seq.to_der()?;
156
157    // For now, return an empty SEQUENCE (0x30 0x00).
158    // This is technically valid ASN.1 but does not encode any OIDs.
159    Ok(vec![0x30, 0x00])
160}