clementine_core/rpc/
interceptors.rs

1use tonic::{service::Interceptor, transport::CertificateDer, Request, Status};
2
3#[derive(Debug, Clone)]
4pub enum Interceptors {
5    OnlyAggregatorAndSelf {
6        aggregator_cert: CertificateDer<'static>,
7        our_cert: CertificateDer<'static>,
8    },
9    Noop,
10}
11
12fn is_internal(req: &Request<()>) -> bool {
13    // This normally doesn't exist but we add it in the AddMethodMiddleware
14    let Some(path) = req.metadata().get("grpc-method") else {
15        // No grpc method? this should not happen
16        tracing::error!("Missing grpc-method header in request");
17        return false;
18    };
19    path.as_bytes().starts_with(b"Internal")
20}
21
22impl Interceptor for Interceptors {
23    #[allow(clippy::result_large_err)]
24    fn call(&mut self, req: Request<()>) -> Result<Request<()>, Status> {
25        match self {
26            Interceptors::OnlyAggregatorAndSelf {
27                our_cert,
28                aggregator_cert,
29            } => only_aggregator_and_self(req, our_cert, aggregator_cert),
30            Interceptors::Noop => Ok(req),
31        }
32    }
33}
34
35#[allow(clippy::result_large_err)]
36fn only_aggregator_and_self(
37    req: Request<()>,
38    our_cert: &CertificateDer<'static>,
39    aggregator_cert: &CertificateDer<'static>,
40) -> Result<Request<()>, Status> {
41    let Some(peer_certs) = req.peer_certs() else {
42        if cfg!(test) {
43            // Test mode, we don't need to verify peer certificates
44            return Ok(req);
45        } else {
46            // If we're not in test mode, we need to check peer certificates
47            return Err(Status::unauthenticated(
48                "Failed to verify peer certificate, is TLS enabled?",
49            ));
50        }
51    };
52
53    // IMPORTANT: Only check the leaf (end-entity) certificate, which is always the first
54    // certificate in the chain. The leaf is the only certificate whose private key the peer
55    // proved possession of during the TLS handshake. Checking anywhere else in the chain
56    // would allow identity spoofing: an attacker could include a pinned cert as an
57    // intermediate in their chain without possessing its private key.
58    let Some(leaf_cert) = peer_certs.first() else {
59        return Err(Status::unauthenticated("Peer certificate chain is empty"));
60    };
61
62    if is_internal(&req) {
63        if leaf_cert == our_cert {
64            Ok(req)
65        } else {
66            Err(Status::unauthenticated(
67                "Unauthorized call to internal method (not self)",
68            ))
69        }
70    } else if leaf_cert == aggregator_cert || leaf_cert == our_cert {
71        Ok(req)
72    } else {
73        Err(Status::unauthenticated(
74            "Unauthorized call to method (not aggregator or self)",
75        ))
76    }
77}