Skip to main content

kipuka/routes/
cacerts.rs

1//! `GET /.well-known/est/cacerts` — CA Certificates Request.
2//!
3//! RFC 7030 §4.1: EST clients request the current CA certificates to
4//! establish an Explicit TA database.  The response is a PKCS#7
5//! certs-only message containing all CA certificates in the chain.
6//!
7//! This endpoint does not require authentication (RFC 7030 §4.1:
8//! "the EST client can request a copy of the current CA certificates").
9
10use std::sync::Arc;
11
12use axum::extract::State;
13use axum::http::{HeaderValue, StatusCode, header};
14use axum::response::{IntoResponse, Response};
15
16use crate::auth::OptionalAuth;
17use crate::error::KipukaError;
18use crate::routes::LabelExtractor;
19use crate::routes::est::{content_types, encode_est_base64};
20use crate::state::AppState;
21
22/// `GET /.well-known/est/cacerts`
23///
24/// Returns PKCS#7 certs-only with all CA certificates in the chain.
25///
26/// # Response
27///
28/// | Header         | Value                                        |
29/// |----------------|----------------------------------------------|
30/// | Status         | `200 OK`                                     |
31/// | Content-Type   | `application/pkcs7-mime; smime-type=certs-only` |
32/// | Content-Transfer-Encoding | `base64`                        |
33///
34/// The body is the base64-encoded DER representation of a PKCS#7
35/// `SignedData` structure with no signerInfos and a single
36/// `certificates` field containing the CA certificate chain.
37///
38/// # Authentication
39///
40/// No authentication required per RFC 7030 §4.1.
41///
42/// # Errors
43///
44/// - `404 Not Found` — unknown EST label
45/// - `500 Internal Server Error` — CA certificate not available
46pub async fn get_cacerts(
47    _auth: OptionalAuth,
48    label: LabelExtractor,
49    State(state): State<Arc<AppState>>,
50) -> Result<Response, KipukaError> {
51    let ca_id = label.ca_id();
52
53    tracing::debug!(
54        ca_id = %ca_id,
55        label = %label.label,
56        "serving CA certificates"
57    );
58
59    // Look up the CA state.
60    let ca = state.get_ca(ca_id).ok_or_else(|| {
61        tracing::error!(ca_id = %ca_id, "CA not found for cacerts request");
62        KipukaError::NotFound
63    })?;
64
65    let chain: &[Vec<u8>] = if !ca.cert_chain.is_empty() {
66        &ca.cert_chain
67    } else {
68        tracing::warn!(ca_id = %ca_id, "cert_chain empty; serving CA cert only");
69        std::slice::from_ref(&ca.cert_der)
70    };
71    let pkcs7_der = build_certs_only_pkcs7(chain)?;
72
73    // Base64-encode per RFC 7030 §4.1.
74    let body = encode_est_base64(&pkcs7_der);
75
76    let mut resp = (StatusCode::OK, body).into_response();
77    resp.headers_mut().insert(
78        header::CONTENT_TYPE,
79        HeaderValue::from_static(content_types::PKCS7_CERTS),
80    );
81    resp.headers_mut().insert(
82        header::HeaderName::from_static("content-transfer-encoding"),
83        HeaderValue::from_static(content_types::TRANSFER_ENCODING_BASE64),
84    );
85
86    // Audit log (best-effort).
87    state
88        .record_audit_event("cacerts", &format!("ca_id={ca_id}"))
89        .await;
90
91    Ok(resp)
92}
93
94/// Build a PKCS#7 certs-only SignedData containing the given DER-encoded
95/// certificate(s).
96///
97/// Produces the RFC 5652 §5.1 degenerate (certs-only) SignedData structure:
98///
99/// ```text
100/// ContentInfo ::= SEQUENCE {
101///     contentType  OBJECT IDENTIFIER (id-signedData),
102///     content [0]  EXPLICIT SignedData
103/// }
104/// SignedData ::= SEQUENCE {
105///     version             INTEGER (1),
106///     digestAlgorithms    SET (empty),
107///     encapContentInfo    SEQUENCE { contentType OID (id-data) },
108///     certificates   [0]  IMPLICIT SET OF Certificate,
109///     signerInfos         SET (empty)
110/// }
111/// ```
112pub(crate) fn build_certs_only_pkcs7(
113    certs_der: &[impl AsRef<[u8]>],
114) -> Result<Vec<u8>, KipukaError> {
115    use synta::{Encoder, Encoding, ObjectIdentifier, Tag, tag};
116
117    if certs_der.is_empty() {
118        return Err(KipukaError::Ca(
119            "No certificates provided for PKCS#7".into(),
120        ));
121    }
122
123    // OID constants from synta-certificate.
124    let oid_signed_data = ObjectIdentifier::new(synta_certificate::oids::CMS_SIGNED_DATA)
125        .map_err(|e| KipukaError::Ca(format!("id-signedData OID: {e}")))?;
126    let oid_data = ObjectIdentifier::new(synta_certificate::oids::CMS_DATA)
127        .map_err(|e| KipukaError::Ca(format!("id-data OID: {e}")))?;
128
129    let seq_tag = Tag::universal_constructed(tag::TAG_SEQUENCE);
130    let set_tag = Tag::universal_constructed(tag::TAG_SET);
131    let ctx0_tag = Tag::context_specific_constructed(0);
132
133    let mut enc = Encoder::new(Encoding::Der);
134
135    // ContentInfo SEQUENCE
136    enc.start_constructed_no_guard(seq_tag)
137        .map_err(|e| KipukaError::Ca(format!("ContentInfo SEQUENCE: {e}")))?;
138
139    // contentType: id-signedData
140    enc.encode(&oid_signed_data)
141        .map_err(|e| KipukaError::Ca(format!("contentType OID: {e}")))?;
142
143    // content [0] EXPLICIT
144    enc.start_constructed_no_guard(ctx0_tag)
145        .map_err(|e| KipukaError::Ca(format!("[0] EXPLICIT: {e}")))?;
146
147    // SignedData SEQUENCE
148    enc.start_constructed_no_guard(seq_tag)
149        .map_err(|e| KipukaError::Ca(format!("SignedData SEQUENCE: {e}")))?;
150
151    // version INTEGER 1
152    let version = synta::Integer::from_i64(1);
153    enc.encode(&version)
154        .map_err(|e| KipukaError::Ca(format!("version INTEGER: {e}")))?;
155
156    // digestAlgorithms SET (empty)
157    enc.start_constructed_no_guard(set_tag)
158        .map_err(|e| KipukaError::Ca(format!("digestAlgorithms SET: {e}")))?;
159    enc.end_constructed()
160        .map_err(|e| KipukaError::Ca(format!("digestAlgorithms end: {e}")))?;
161
162    // encapContentInfo SEQUENCE { contentType: id-data }
163    enc.start_constructed_no_guard(seq_tag)
164        .map_err(|e| KipukaError::Ca(format!("encapContentInfo SEQUENCE: {e}")))?;
165    enc.encode(&oid_data)
166        .map_err(|e| KipukaError::Ca(format!("id-data OID: {e}")))?;
167    enc.end_constructed()
168        .map_err(|e| KipukaError::Ca(format!("encapContentInfo end: {e}")))?;
169
170    // certificates [0] IMPLICIT SET OF Certificate
171    enc.start_constructed_no_guard(ctx0_tag)
172        .map_err(|e| KipukaError::Ca(format!("certificates [0]: {e}")))?;
173    for cert in certs_der {
174        enc.write_bytes(cert.as_ref());
175    }
176    enc.end_constructed()
177        .map_err(|e| KipukaError::Ca(format!("certificates end: {e}")))?;
178
179    // signerInfos SET (empty)
180    enc.start_constructed_no_guard(set_tag)
181        .map_err(|e| KipukaError::Ca(format!("signerInfos SET: {e}")))?;
182    enc.end_constructed()
183        .map_err(|e| KipukaError::Ca(format!("signerInfos end: {e}")))?;
184
185    // Close SignedData SEQUENCE
186    enc.end_constructed()
187        .map_err(|e| KipukaError::Ca(format!("SignedData end: {e}")))?;
188
189    // Close [0] EXPLICIT
190    enc.end_constructed()
191        .map_err(|e| KipukaError::Ca(format!("[0] end: {e}")))?;
192
193    // Close ContentInfo SEQUENCE
194    enc.end_constructed()
195        .map_err(|e| KipukaError::Ca(format!("ContentInfo end: {e}")))?;
196
197    let pkcs7_der = enc
198        .finish()
199        .map_err(|e| KipukaError::Ca(format!("PKCS#7 DER finish: {e}")))?;
200
201    tracing::debug!(
202        pkcs7_len = pkcs7_der.len(),
203        cert_count = certs_der.len(),
204        "built PKCS#7 certs-only SignedData"
205    );
206
207    Ok(pkcs7_der)
208}