bitwarden_access_connector/integrations/active_directory/
tls.rs1use std::{path::Path, sync::Arc};
2
3use bitwarden_pam::RotationFailureCode;
4use rustls::{
5 ClientConfig, RootCertStore,
6 pki_types::{CertificateDer, pem::PemObject as _},
7};
8use rustls_platform_verifier::Verifier;
9
10use super::classify::{Phase, target_unreachable};
11use crate::{
12 error::{Retry, SafeDetail},
13 integrations::IntegrationError,
14};
15
16pub(super) fn tls_config(
21 ca_certificate: Option<&str>,
22 phase: Phase,
23) -> Result<Arc<ClientConfig>, IntegrationError> {
24 let provider = Arc::new(rustls::crypto::ring::default_provider());
25 let verifier = match ca_certificate {
26 Some(path) => Verifier::new_with_extra_roots(read_anchors(path, phase)?, provider.clone()),
27 None => Verifier::new(provider.clone()),
28 }
29 .map_err(|_| target_unreachable(Retry::Permanent, phase.settled(), "TlsVerifierUnavailable"))?;
30 let config = ClientConfig::builder_with_provider(provider)
31 .with_safe_default_protocol_versions()
32 .map_err(|_| anchor_failure(phase, "TlsProviderUnavailable"))?
33 .dangerous()
36 .with_custom_certificate_verifier(Arc::new(verifier))
37 .with_no_client_auth();
38 Ok(Arc::new(config))
39}
40
41fn read_anchors(
42 path: &str,
43 phase: Phase,
44) -> Result<Vec<CertificateDer<'static>>, IntegrationError> {
45 let anchors: Vec<CertificateDer<'static>> = CertificateDer::pem_file_iter(Path::new(path))
46 .map_err(|_| anchor_failure(phase, "CaCertificateUnreadable"))?
47 .collect::<Result<Vec<_>, _>>()
48 .map_err(|_| anchor_failure(phase, "CaCertificateInvalid"))?;
49 if anchors.is_empty() {
50 return Err(anchor_failure(phase, "CaCertificateEmpty"));
51 }
52
53 let mut scratch = RootCertStore::empty();
55 for anchor in &anchors {
56 scratch
57 .add(anchor.clone())
58 .map_err(|_| anchor_failure(phase, "CaCertificateInvalid"))?;
59 }
60 Ok(anchors)
61}
62
63fn anchor_failure(phase: Phase, kind: &'static str) -> IntegrationError {
64 IntegrationError::new(
65 Retry::Permanent,
66 phase.settled(),
67 RotationFailureCode::CredentialsUnresolved,
68 SafeDetail::from_kind(kind),
69 )
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use crate::integrations::TargetEffect;
76
77 const TEST_CA_PEM: &str = "\
79-----BEGIN CERTIFICATE-----\n\
80MIIDJTCCAg2gAwIBAgIUP9bWk+gm4n6VqzhqhQz9YDiwR/UwDQYJKoZIhvcNAQEL\n\
81BQAwIjEgMB4GA1UEAwwXcm90YXRpb24tZGFlbW9uLXRlc3QtY2EwHhcNMjYwODMx\n\
82MjIzODU5WhcNMzYwODI4MjIzODU5WjAiMSAwHgYDVQQDDBdyb3RhdGlvbi1kYWVt\n\
83b24tdGVzdC1jYTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALCVyUmM\n\
8486vormZ7E/vOxdZMvJOYVjp17OrAI1tPWq0PywOizZ1gJORWXjArJFT0PoG2OqmA\n\
85hafaOoZTeZH0J4DwEjgGVn/rGVIa8f5a8qjfnJdDn23fnqa2Wd+9Tf1/LDeCUZil\n\
867jZ4NqtddZe+GOkkWJXB+55u7JhxPyX/AdfVK9idmGANXGuCOUZhTLSnz+p5SrLP\n\
87WWDgQeAHTvFlWcpu8eff5Agj5cdhqyJiBcpaLzga0xiUifpdG4yEU/7H/909XKOQ\n\
88/O0wiPwnObHwOVFALDppeHo9FtGmvK0OQArxgo40gaeWEvctURyXc6Lqu5u8eZXV\n\
89MX5nIS+hPAYLZ50CAwEAAaNTMFEwHQYDVR0OBBYEFE7MLaoHaLKLcBeZsdAfGTk0\n\
900ToLMB8GA1UdIwQYMBaAFE7MLaoHaLKLcBeZsdAfGTk00ToLMA8GA1UdEwEB/wQF\n\
91MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAJyvTvcTCL6s3MPlhYHrcCAfc6/PcVro\n\
92NvLM9fOUvmlqdlyXyvwZ/FOwGogLUSVVR+68wLQ4NFac4nhyeaApiFsKoHP+bA6X\n\
93bsgmmsTYuH0fw43szFTlI+Lw2a8Ai9Nx8JTeyILL9FzCV6ranWW7YcQBEPTvpfB7\n\
94vBWeCGTA+sHqgBbltU/5jsWYslt9YhOpxkoJHHxyt2JA8XYCpdSkfgFOsjReofHn\n\
95SavmVaBabR7Vq3QVTfvp+/WO41z7hGKsGAZ2dVldkHeOjgu4oktTOeblDbnZogZ2\n\
96D2kYx4ka5O/dEuqcrMf/qd6GW5H2PJcrdc4PtRJeIBDR0TgweD2xjJ0=\n\
97-----END CERTIFICATE-----\n";
98
99 fn write_temp(contents: &str) -> tempfile::NamedTempFile {
100 use std::io::Write as _;
101 let mut f = tempfile::NamedTempFile::new().expect("temp file");
102 f.write_all(contents.as_bytes()).expect("write pem");
103 f.flush().expect("flush");
104 f
105 }
106
107 #[test]
108 fn valid_ca_certificate_builds_a_client_config() {
109 let f = write_temp(TEST_CA_PEM);
110 let path = f.path().to_str().expect("utf8 path");
111 assert!(tls_config(Some(path), Phase::BeforeWrite).is_ok());
112 }
113
114 #[test]
115 fn no_ca_certificate_builds_a_client_config() {
116 assert!(tls_config(None, Phase::BeforeWrite).is_ok());
117 }
118
119 #[test]
120 fn missing_ca_certificate_file_is_fatal() {
121 let err = tls_config(
122 Some("/nonexistent/path/to/ca-bundle-2f8a1c.pem"),
123 Phase::BeforeWrite,
124 )
125 .expect_err("a missing PEM must not fall back to platform roots");
126 assert_eq!(err.retry, Retry::Permanent);
127 assert_eq!(err.code, RotationFailureCode::CredentialsUnresolved);
128 assert_eq!(err.detail.as_str(), "error kind: CaCertificateUnreadable");
129 }
130
131 #[test]
132 fn malformed_ca_certificate_is_fatal() {
133 let truncated = "-----BEGIN CERTIFICATE-----\nMIIDJTCCAg2gAwIBAgIUP9bWk\n";
134 let f = write_temp(truncated);
135 let path = f.path().to_str().expect("utf8 path");
136 let err = tls_config(Some(path), Phase::BeforeWrite)
137 .expect_err("a malformed PEM must not fall back to platform roots");
138 assert_eq!(err.retry, Retry::Permanent);
139 assert_eq!(err.detail.as_str(), "error kind: CaCertificateInvalid");
140 }
141
142 #[test]
143 fn ca_certificate_without_a_certificate_is_fatal() {
144 let f = write_temp("# no PEM sections here\n");
145 let path = f.path().to_str().expect("utf8 path");
146 let err = tls_config(Some(path), Phase::BeforeWrite)
147 .expect_err("a PEM with no certificate must not fall back to platform roots");
148 assert_eq!(err.retry, Retry::Permanent);
149 assert_eq!(err.detail.as_str(), "error kind: CaCertificateEmpty");
150 }
151
152 #[test]
153 fn ca_certificate_path_never_reaches_the_error_detail() {
154 let path = "/etc/secrets/ad-ca-9f3b2e.pem";
155 let err = tls_config(Some(path), Phase::BeforeWrite).expect_err("missing file");
156 assert!(
157 !err.to_string().contains("9f3b2e"),
158 "the configured path must not appear in the error: {err}"
159 );
160 }
161
162 #[test]
163 fn ca_certificate_failure_after_rotation_reports_applied() {
164 let err =
165 tls_config(Some("/nonexistent/ca.pem"), Phase::AfterWrite).expect_err("missing file");
166 assert_eq!(err.effect, TargetEffect::Applied);
167 }
168}