Skip to main content

uselesskey_tonic/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Integration between uselesskey X.509 fixtures and `tonic` transport TLS types.
4//!
5//! This crate provides extension traits that convert uselesskey certificates and
6//! chains into `tonic::transport::{Identity, Certificate, ServerTlsConfig, ClientTlsConfig}`.
7//!
8//! # Features
9//!
10//! - `x509` (default) - X.509 certificates and chain support
11//!
12//! # Example
13//!
14#![cfg_attr(feature = "x509", doc = "```")]
15#![cfg_attr(not(feature = "x509"), doc = "```ignore")]
16//! use uselesskey_core::Factory;
17//! use uselesskey_x509::{ChainSpec, X509FactoryExt};
18//! use uselesskey_tonic::{TonicClientTlsExt, TonicServerTlsExt};
19//!
20//! let fx = Factory::random();
21//! let chain = fx.x509_chain("grpc-service", ChainSpec::new("test.example.com"));
22//!
23//! let server_tls = chain.server_tls_config_tonic();
24//! let client_tls = chain.client_tls_config_tonic("test.example.com");
25//! # let _ = (server_tls, client_tls);
26//! ```
27
28use tonic::transport::{Certificate, ClientTlsConfig, Identity, ServerTlsConfig};
29
30/// Convert uselesskey fixtures into `tonic::transport::Identity`.
31#[cfg(feature = "x509")]
32pub trait TonicIdentityExt {
33    /// Convert the fixture to a tonic identity.
34    fn identity_tonic(&self) -> Identity;
35}
36
37#[cfg(feature = "x509")]
38impl TonicIdentityExt for uselesskey_x509::X509Cert {
39    fn identity_tonic(&self) -> Identity {
40        Identity::from_pem(self.cert_pem(), self.private_key_pkcs8_pem())
41    }
42}
43
44#[cfg(feature = "x509")]
45impl TonicIdentityExt for uselesskey_x509::X509Chain {
46    fn identity_tonic(&self) -> Identity {
47        Identity::from_pem(self.chain_pem(), self.leaf_private_key_pkcs8_pem())
48    }
49}
50
51/// Build `tonic::transport::ServerTlsConfig` from uselesskey fixtures.
52#[cfg(feature = "x509")]
53pub trait TonicServerTlsExt {
54    /// Build a server TLS config with server identity.
55    fn server_tls_config_tonic(&self) -> ServerTlsConfig;
56}
57
58#[cfg(feature = "x509")]
59impl TonicServerTlsExt for uselesskey_x509::X509Cert {
60    fn server_tls_config_tonic(&self) -> ServerTlsConfig {
61        ServerTlsConfig::new().identity(self.identity_tonic())
62    }
63}
64
65#[cfg(feature = "x509")]
66impl TonicServerTlsExt for uselesskey_x509::X509Chain {
67    fn server_tls_config_tonic(&self) -> ServerTlsConfig {
68        ServerTlsConfig::new().identity(self.identity_tonic())
69    }
70}
71
72/// Build `tonic::transport::ClientTlsConfig` from uselesskey fixtures.
73#[cfg(feature = "x509")]
74pub trait TonicClientTlsExt {
75    /// Build a client TLS config that trusts the fixture CA/cert.
76    fn client_tls_config_tonic(&self, domain_name: impl Into<String>) -> ClientTlsConfig;
77}
78
79#[cfg(feature = "x509")]
80impl TonicClientTlsExt for uselesskey_x509::X509Cert {
81    fn client_tls_config_tonic(&self, domain_name: impl Into<String>) -> ClientTlsConfig {
82        ClientTlsConfig::new()
83            .domain_name(domain_name)
84            .ca_certificate(Certificate::from_pem(self.cert_pem()))
85    }
86}
87
88#[cfg(feature = "x509")]
89impl TonicClientTlsExt for uselesskey_x509::X509Chain {
90    fn client_tls_config_tonic(&self, domain_name: impl Into<String>) -> ClientTlsConfig {
91        ClientTlsConfig::new()
92            .domain_name(domain_name)
93            .ca_certificate(Certificate::from_pem(self.root_cert_pem()))
94    }
95}
96
97/// Build mutual TLS (`mTLS`) configs from X.509 chains.
98#[cfg(feature = "x509")]
99pub trait TonicMtlsExt {
100    /// Build a server TLS config requiring client certificates trusted by the chain root.
101    fn server_tls_config_mtls_tonic(&self) -> ServerTlsConfig;
102
103    /// Build a client TLS config with a client identity and root trust.
104    fn client_tls_config_mtls_tonic(&self, domain_name: impl Into<String>) -> ClientTlsConfig;
105}
106
107#[cfg(feature = "x509")]
108impl TonicMtlsExt for uselesskey_x509::X509Chain {
109    fn server_tls_config_mtls_tonic(&self) -> ServerTlsConfig {
110        ServerTlsConfig::new()
111            .identity(self.identity_tonic())
112            .client_ca_root(Certificate::from_pem(self.root_cert_pem()))
113    }
114
115    fn client_tls_config_mtls_tonic(&self, domain_name: impl Into<String>) -> ClientTlsConfig {
116        self.client_tls_config_tonic(domain_name)
117            .identity(self.identity_tonic())
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::{TonicClientTlsExt, TonicIdentityExt, TonicMtlsExt, TonicServerTlsExt};
124    use std::sync::OnceLock;
125    use uselesskey_core::{Factory, Seed};
126    use uselesskey_x509::{ChainSpec, X509FactoryExt, X509Spec};
127
128    static FX: OnceLock<Factory> = OnceLock::new();
129
130    fn fx() -> Factory {
131        FX.get_or_init(|| {
132            let seed = Seed::from_env_value("uselesskey-tonic-inline-test-seed-v1")
133                .expect("test seed should always parse");
134            Factory::deterministic(seed)
135        })
136        .clone()
137    }
138
139    #[test]
140    fn self_signed_identity_and_tls_configs_build() {
141        let fx = fx();
142        let cert = fx.x509_self_signed("grpc-self-signed", X509Spec::self_signed("localhost"));
143
144        let _identity = cert.identity_tonic();
145        let _server = cert.server_tls_config_tonic();
146        let _client = cert.client_tls_config_tonic("localhost");
147    }
148
149    #[test]
150    fn chain_identity_and_tls_configs_build() {
151        let fx = fx();
152        let chain = fx.x509_chain("grpc-chain", ChainSpec::new("test.example.com"));
153
154        let _identity = chain.identity_tonic();
155        let _server = chain.server_tls_config_tonic();
156        let _client = chain.client_tls_config_tonic("test.example.com");
157    }
158
159    #[test]
160    fn chain_mtls_configs_build() {
161        let fx = fx();
162        let chain = fx.x509_chain("grpc-mtls", ChainSpec::new("test.example.com"));
163
164        let _server = chain.server_tls_config_mtls_tonic();
165        let _client = chain.client_tls_config_mtls_tonic("test.example.com");
166    }
167
168    #[test]
169    fn deterministic_chain_material_stays_stable() {
170        let seed = Seed::from_env_value("grpc-tonic-stability").expect("seed");
171        let fx = Factory::deterministic(seed);
172
173        let a = fx.x509_chain("stable", ChainSpec::new("det.example.com"));
174        fx.clear_cache();
175        let b = fx.x509_chain("stable", ChainSpec::new("det.example.com"));
176
177        assert_eq!(a.chain_pem(), b.chain_pem());
178        assert_eq!(a.root_cert_pem(), b.root_cert_pem());
179        assert_eq!(
180            a.leaf_private_key_pkcs8_pem(),
181            b.leaf_private_key_pkcs8_pem()
182        );
183    }
184}