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}