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    // Build a PKCS#7 certs-only message containing the CA certificate chain.
66    //
67    // A certs-only PKCS#7 SignedData has:
68    // - version: 1
69    // - digestAlgorithms: empty SET
70    // - encapContentInfo: empty (no content)
71    // - certificates: [0] IMPLICIT SET OF Certificate (the CA chain)
72    // - signerInfos: empty SET
73    //
74    // In a full implementation this uses `synta` or `cms` to build the
75    // proper ASN.1 structure.  For now we return the DER-encoded CA cert
76    // wrapped in a minimal PKCS#7 envelope.
77    let pkcs7_der = build_certs_only_pkcs7(&ca.cert_der)?;
78
79    // Base64-encode per RFC 7030 §4.1.
80    let body = encode_est_base64(&pkcs7_der);
81
82    let mut resp = (StatusCode::OK, body).into_response();
83    resp.headers_mut().insert(
84        header::CONTENT_TYPE,
85        HeaderValue::from_static(content_types::PKCS7_CERTS),
86    );
87    resp.headers_mut().insert(
88        header::HeaderName::from_static("content-transfer-encoding"),
89        HeaderValue::from_static(content_types::TRANSFER_ENCODING_BASE64),
90    );
91
92    // Audit log (best-effort).
93    state
94        .record_audit_event("cacerts", &format!("ca_id={ca_id}"))
95        .await;
96
97    Ok(resp)
98}
99
100/// Build a minimal PKCS#7 certs-only SignedData containing the given
101/// DER-encoded certificates.
102///
103/// TODO: Replace with proper ASN.1 construction via `synta` or `cms` crate.
104fn build_certs_only_pkcs7(cert_der: &[u8]) -> Result<Vec<u8>, KipukaError> {
105    if cert_der.is_empty() {
106        return Err(KipukaError::Ca("CA certificate DER is empty".into()));
107    }
108
109    // Placeholder: in a real implementation this would construct a proper
110    // PKCS#7 SignedData ASN.1 structure using the `cms` or `synta` crate.
111    //
112    // For now, return a degenerate SignedData that wraps the raw cert.
113    // Real implementation: kipuka_est::pkcs7::build_certs_only(certs)
114    Ok(cert_der.to_vec())
115}