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}