Skip to main content

cli_shared/
tls_trust.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Shared TLS-trust failure wording for hosted bootstrap and Git HTTPS.
3
4/// Operator-facing CA setting named by every TLS trust failure.
5pub const REMOTE_TLS_CA_CERT_SETTING: &str = "HEDDLE_REMOTE_TLS_CA_CERT";
6
7/// True when a transport error is a peer-certificate trust failure.
8pub fn is_tls_trust_failure(message: &str) -> bool {
9    let normalized = message.to_ascii_lowercase();
10    normalized.contains("invalid peer certificate")
11        || normalized.contains("unknownissuer")
12        || normalized.contains("unknown issuer")
13        || normalized.contains("certificateunknown")
14}
15
16/// Append the CA configuration that fixes an untrusted peer certificate.
17pub fn annotate_tls_trust_failure(error: impl std::fmt::Display) -> String {
18    let message = error.to_string();
19    if is_tls_trust_failure(&message) && !message.contains(REMOTE_TLS_CA_CERT_SETTING) {
20        format!(
21            "{message}; trust this server's CA with {REMOTE_TLS_CA_CERT_SETTING}=/path/to/ca.pem"
22        )
23    } else {
24        message
25    }
26}
27
28/// Walk an error source chain and annotate a TLS trust failure if any link is one.
29pub fn annotate_error_chain_tls_trust_failure(error: &(dyn std::error::Error + 'static)) -> String {
30    let mut message = error.to_string();
31    let mut current = error.source();
32    let mut trust_failure = is_tls_trust_failure(&message);
33    while let Some(err) = current {
34        let next = err.to_string();
35        trust_failure |= is_tls_trust_failure(&next);
36        if !message.contains(&next) {
37            message.push_str(": ");
38            message.push_str(&next);
39        }
40        current = err.source();
41    }
42    if trust_failure {
43        annotate_tls_trust_failure(message)
44    } else {
45        message
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::{REMOTE_TLS_CA_CERT_SETTING, annotate_tls_trust_failure, is_tls_trust_failure};
52
53    #[test]
54    fn unknown_issuer_is_a_tls_trust_failure() {
55        assert!(is_tls_trust_failure(
56            "invalid peer certificate: UnknownIssuer"
57        ));
58        assert!(!is_tls_trust_failure("connection refused"));
59    }
60
61    #[test]
62    fn annotation_names_the_ca_setting_once() {
63        let annotated = annotate_tls_trust_failure("invalid peer certificate: UnknownIssuer");
64        assert!(annotated.contains(REMOTE_TLS_CA_CERT_SETTING));
65        assert_eq!(
66            annotate_tls_trust_failure(annotated.as_str()),
67            annotated,
68            "already-annotated messages must stay stable"
69        );
70    }
71
72    #[test]
73    fn annotation_walks_a_source_chain() {
74        #[derive(Debug)]
75        struct Root(&'static str);
76        impl std::fmt::Display for Root {
77            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78                f.write_str(self.0)
79            }
80        }
81        impl std::error::Error for Root {}
82
83        #[derive(Debug)]
84        struct Wrapper {
85            source: Root,
86        }
87        impl std::fmt::Display for Wrapper {
88            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89                f.write_str("error sending request")
90            }
91        }
92        impl std::error::Error for Wrapper {
93            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
94                Some(&self.source)
95            }
96        }
97
98        let error = Wrapper {
99            source: Root("invalid peer certificate: UnknownIssuer"),
100        };
101        let annotated = super::annotate_error_chain_tls_trust_failure(&error);
102        assert!(annotated.contains("error sending request"));
103        assert!(annotated.contains("UnknownIssuer"));
104        assert!(annotated.contains(REMOTE_TLS_CA_CERT_SETTING));
105    }
106}