ic_bn_lib/tls/
resolver.rs

1use std::{fmt::Debug, path::PathBuf, sync::Arc, time::Instant};
2
3use anyhow::{Context as _, Error};
4use ic_bn_lib_common::{traits::tls::ResolvesServerCert, types::http::ALPN_ACME};
5use prometheus::{
6    HistogramVec, IntCounterVec, Registry, register_histogram_vec_with_registry,
7    register_int_counter_vec_with_registry,
8};
9use rustls::{
10    server::{ClientHello, ResolvesServerCert as ResolvesServerCertRustls},
11    sign::CertifiedKey,
12};
13use tracing::debug;
14
15use crate::tls::{pem_convert_to_rustls, pem_load_rustls, pem_load_rustls_single};
16
17#[derive(Debug, Clone)]
18pub struct Metrics {
19    resolve_count: IntCounterVec,
20    supported_scheme: IntCounterVec,
21    supported_cipher: IntCounterVec,
22    resolve_duration: HistogramVec,
23}
24
25impl Metrics {
26    pub fn new(registry: &Registry) -> Self {
27        Self {
28            resolve_count: register_int_counter_vec_with_registry!(
29                format!("tls_resolver_total"),
30                format!("Counts the number of resolves"),
31                &["found"],
32                registry
33            )
34            .unwrap(),
35
36            supported_scheme: register_int_counter_vec_with_registry!(
37                format!("tls_resolver_supported_scheme"),
38                format!("Counts the number clients that support given scheme"),
39                &["scheme"],
40                registry
41            )
42            .unwrap(),
43
44            supported_cipher: register_int_counter_vec_with_registry!(
45                format!("tls_resolver_supported_cipher"),
46                format!("Counts the number clients that support given ciphersuite"),
47                &["cipher"],
48                registry
49            )
50            .unwrap(),
51
52            resolve_duration: register_histogram_vec_with_registry!(
53                format!("tls_resolver_duration_sec"),
54                format!("Records the duration of resolves in seconds"),
55                &["found"],
56                [0.0001, 0.0005, 0.001, 0.002, 0.004, 0.008, 0.016, 0.032].to_vec(),
57                registry
58            )
59            .unwrap(),
60        }
61    }
62}
63
64/// Combines several certificate resolvers into one.
65/// Only one Rustls-compatible resolver can be used since it consumes ClientHello.
66#[derive(Debug, derive_new::new)]
67pub struct AggregatingResolver {
68    acme: Option<Arc<dyn ResolvesServerCertRustls>>,
69    resolvers: Vec<Arc<dyn ResolvesServerCert>>,
70    metrics: Metrics,
71}
72
73impl AggregatingResolver {
74    fn resolve_inner(&self, ch: ClientHello) -> Option<Arc<CertifiedKey>> {
75        // Accept missing SNI e.g. for testing cases when we're accessed over IP directly
76        let sni = ch.server_name().unwrap_or("").to_string();
77
78        // Check if we have an ACME ALPN and pass it to the ACME resolver (if we have one)
79        if ch.alpn().is_some_and(|mut x| x.all(|x| x == ALPN_ACME))
80            && let Some(acme) = &self.acme
81        {
82            return acme.resolve(ch);
83        }
84
85        let alpn = ch
86            .alpn()
87            .map(|x| x.map(String::from_utf8_lossy).collect::<Vec<_>>());
88
89        // Iterate over our resolvers to find matching cert if any.
90        let cert = self
91            .resolvers
92            .iter()
93            .find_map(|x| x.resolve(&ch))
94            // Otherwise try the ACME resolver that consumes ClientHello
95            .or_else(|| self.acme.as_ref().and_then(|x| x.resolve(ch)));
96
97        if cert.is_some() {
98            return cert;
99        }
100
101        debug!(
102            "AggregatingResolver: No certificate found for SNI '{}' (ALPN {:?}), trying fallback",
103            sni, alpn
104        );
105
106        // Check if any of the resolvers provide us with a fallback
107        self.resolvers.iter().find_map(|x| x.resolve_any())
108    }
109}
110
111// Implement certificate resolving for Rustls
112impl ResolvesServerCertRustls for AggregatingResolver {
113    fn resolve(&self, ch: ClientHello) -> Option<Arc<CertifiedKey>> {
114        for v in ch.signature_schemes() {
115            self.metrics
116                .supported_scheme
117                .with_label_values(&[v.as_str().unwrap_or("unknown")])
118                .inc();
119        }
120
121        for v in ch.cipher_suites() {
122            self.metrics
123                .supported_cipher
124                .with_label_values(&[v.as_str().unwrap_or("unknown")])
125                .inc();
126        }
127
128        let start = Instant::now();
129        let r = self.resolve_inner(ch);
130        let found = if r.is_some() { "yes" } else { "no" };
131
132        self.metrics
133            .resolve_duration
134            .with_label_values(&[found])
135            .observe(start.elapsed().as_secs_f64());
136
137        self.metrics.resolve_count.with_label_values(&[found]).inc();
138
139        r
140    }
141}
142
143/// Rustls certificate resolver that always provides a single certificate
144#[derive(Clone, Debug)]
145pub struct StubResolver(Arc<CertifiedKey>);
146
147impl StubResolver {
148    /// Creates `StubResolver` by parsing PEM-encoded cert & key from provided slices
149    pub fn new(cert: &[u8], key: &[u8]) -> Result<Self, Error> {
150        Ok(Self(Arc::new(
151            pem_convert_to_rustls(key, cert).context("unable to parse cert and/or key")?,
152        )))
153    }
154
155    /// Creates `StubResolver` by loading PEM-encoded cert & key from provided files
156    pub fn new_from_files(cert: PathBuf, key: PathBuf) -> Result<Self, Error> {
157        Ok(Self(Arc::new(
158            pem_load_rustls(key, cert).context("unable to load certificates")?,
159        )))
160    }
161
162    /// Creates `StubResolver` by loading PEM-encoded cert & key from provided concatenated file
163    pub fn new_from_file(pem: PathBuf) -> Result<Self, Error> {
164        Ok(Self(Arc::new(
165            pem_load_rustls_single(pem).context("unable to load certificates")?,
166        )))
167    }
168}
169
170impl ResolvesServerCertRustls for StubResolver {
171    fn resolve(&self, _client_hello: ClientHello) -> Option<Arc<CertifiedKey>> {
172        Some(self.0.clone())
173    }
174}