Skip to content

Commit e4cd887

Browse files
committed
client: proxy-aware TLS cert gen
+ need to add lexe trust anchor to client TLS verifier so we authenticate the reverse proxy
1 parent 8c22315 commit e4cd887

File tree

6 files changed

+341
-152
lines changed

6 files changed

+341
-152
lines changed

common/src/api/runner.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::api::UserPk;
55
pub type Port = u16;
66

77
/// Used to return the port of a loaded node.
8-
#[derive(Clone, Debug, Deserialize, Serialize)]
8+
#[derive(Clone, Debug, Serialize, Deserialize)]
99
pub struct PortReply {
1010
pub port: Port,
1111
}

common/src/attest/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
pub mod cert;
22
pub mod verify;
33

4-
pub use verify::ServerCertVerifier;
4+
pub use verify::{EnclavePolicy, ServerCertVerifier};

common/src/attest/verify.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ impl rustls::client::ServerCertVerifier for ServerCertVerifier {
135135

136136
Ok(verified_token)
137137
}
138+
139+
fn request_scts(&self) -> bool {
140+
false
141+
}
138142
}
139143

140144
struct AttestEvidence<'a> {

common/src/client.rs

Lines changed: 332 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,332 @@
1+
// hello
2+
3+
use std::sync::Arc;
4+
use std::time::SystemTime;
5+
6+
use anyhow::{Context, Result};
7+
use rustls::client::{ServerCertVerifier, WebPkiVerifier};
8+
use rustls::RootCertStore;
9+
10+
use crate::client_node_certs::{CaCert, ClientCert, NodeCert};
11+
use crate::rng::Crng;
12+
use crate::root_seed::RootSeed;
13+
use crate::{attest, ed25519};
14+
15+
/// A [`rustls`] TLS cert verifier for the client to connect to a now
16+
/// provisioned and possibly running node through the reverse proxy.
17+
struct ClientRunCertVerifier {
18+
lexe_verifier: WebPkiVerifier,
19+
node_verifier: WebPkiVerifier,
20+
}
21+
22+
/// A [`rustls`] TLS cert verifier for the client to connect to a provisioning
23+
/// node through the reverse proxy.
24+
struct ClientProvisionCertVerifier {
25+
lexe_verifier: WebPkiVerifier,
26+
attest_verifier: attest::ServerCertVerifier,
27+
}
28+
29+
// -- rustls TLS configs -- //
30+
31+
pub fn node_tls_config(
32+
node_cert: &NodeCert,
33+
ca_cert: &CaCert,
34+
) -> anyhow::Result<rustls::ServerConfig> {
35+
let ca_cert_der = ca_cert
36+
.serialize_der_signed()
37+
.context("Failed to self-sign + DER-serialize CA cert")?;
38+
let node_cert_der = node_cert
39+
.serialize_der_signed(ca_cert)
40+
.context("Failed to sign + DER-serialize node cert w/ CA cert")?;
41+
let node_key_der = node_cert.serialize_key_der();
42+
43+
let mut trust_anchors = rustls::RootCertStore::empty();
44+
trust_anchors
45+
.add(&rustls::Certificate(ca_cert_der))
46+
.context("rustls failed to deserialize CA cert DER bytes")?;
47+
48+
let client_verifier =
49+
rustls::server::AllowAnyAuthenticatedClient::new(trust_anchors);
50+
51+
// TODO(phlip9): use exactly TLSv1.3, ciphersuite TLS13_AES_128_GCM_SHA256,
52+
// and key exchange X25519
53+
let mut config = rustls::ServerConfig::builder()
54+
.with_safe_defaults()
55+
.with_client_cert_verifier(client_verifier)
56+
.with_single_cert(
57+
vec![rustls::Certificate(node_cert_der)],
58+
rustls::PrivateKey(node_key_der),
59+
)
60+
.context("Failed to build rustls::ServerConfig")?;
61+
config.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
62+
63+
Ok(config)
64+
}
65+
66+
pub fn client_provision_tls_config(
67+
lexe_trust_anchor: &rustls::Certificate,
68+
expect_dummy_quote: bool,
69+
enclave_policy: attest::EnclavePolicy,
70+
) -> Result<rustls::ClientConfig> {
71+
let verifier = ClientProvisionCertVerifier {
72+
lexe_verifier: lexe_verifier(lexe_trust_anchor)?,
73+
attest_verifier: attest::ServerCertVerifier {
74+
expect_dummy_quote,
75+
enclave_policy,
76+
},
77+
};
78+
79+
// TODO(phlip9): use exactly TLSv1.3, ciphersuite TLS13_AES_128_GCM_SHA256,
80+
// and key exchange X25519
81+
let mut config = rustls::ClientConfig::builder()
82+
.with_safe_defaults()
83+
.with_custom_certificate_verifier(Arc::new(verifier))
84+
.with_no_client_auth();
85+
// TODO(phlip9): ensure this matches the reqwest config
86+
config.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
87+
88+
Ok(config)
89+
}
90+
91+
pub fn client_run_tls_config(
92+
rng: &mut dyn Crng,
93+
lexe_trust_anchor: &rustls::Certificate,
94+
root_seed: &RootSeed,
95+
) -> Result<rustls::ClientConfig> {
96+
// derive the shared client-node CA cert from the root seed
97+
let ca_cert_key_pair = root_seed.derive_client_ca_key_pair();
98+
let ca_cert = CaCert::from_key_pair(ca_cert_key_pair)
99+
.context("Failed to build node-client CA cert")?;
100+
let ca_cert_der = rustls::Certificate(
101+
ca_cert
102+
.serialize_der_signed()
103+
.context("Failed to sign and serialize node-client CA cert")?,
104+
);
105+
106+
// the derived CA cert is our trust root for node connections
107+
let mut roots = RootCertStore::empty();
108+
roots
109+
.add(&ca_cert_der)
110+
.context("Failed to re-parse node-client CA cert")?;
111+
let node_verifier = WebPkiVerifier::new(roots, None);
112+
113+
let verifier = ClientRunCertVerifier {
114+
lexe_verifier: lexe_verifier(lexe_trust_anchor)?,
115+
node_verifier,
116+
};
117+
118+
// sample an ephemeral key pair for the child cert
119+
let client_cert_key_pair = ed25519::gen_key_pair(rng);
120+
let client_cert = ClientCert::from_key_pair(client_cert_key_pair)
121+
.context("Failed to build ephemeral client cert")?;
122+
let client_cert_der = rustls::Certificate(
123+
client_cert
124+
.serialize_der_signed(&ca_cert)
125+
.context("Failed to sign and serialize ephemeral client cert")?,
126+
);
127+
let client_key_der = rustls::PrivateKey(client_cert.serialize_key_der());
128+
129+
// TODO(phlip9): use exactly TLSv1.3, ciphersuite TLS13_AES_128_GCM_SHA256,
130+
// and key exchange X25519
131+
let mut config = rustls::ClientConfig::builder()
132+
.with_safe_defaults()
133+
.with_custom_certificate_verifier(Arc::new(verifier))
134+
// TODO(phlip9): do we need to use a custom cert resovler that doesn't
135+
// send the client cert for the reverse proxy connection?
136+
.with_single_cert(vec![client_cert_der], client_key_der)
137+
.context("Failed to build rustls::ClientConfig")?;
138+
// TODO(phlip9): ensure this matches the reqwest config
139+
config.alpn_protocols = vec!["h2".into(), "http/1.1".into()];
140+
141+
Ok(config)
142+
}
143+
144+
fn lexe_verifier(
145+
lexe_trust_anchor: &rustls::Certificate,
146+
) -> Result<WebPkiVerifier> {
147+
let mut lexe_roots = RootCertStore::empty();
148+
lexe_roots
149+
.add(lexe_trust_anchor)
150+
.context("Failed to deserialize lexe trust anchor")?;
151+
// TODO(phlip9): our web-facing certs will actually support cert
152+
// transparency
153+
let ct_policy = None;
154+
let lexe_verifier = WebPkiVerifier::new(lexe_roots, ct_policy);
155+
Ok(lexe_verifier)
156+
}
157+
158+
// -- impl ClientProvisionCertVerifier -- //
159+
160+
impl ServerCertVerifier for ClientProvisionCertVerifier {
161+
fn verify_server_cert(
162+
&self,
163+
end_entity: &rustls::Certificate,
164+
intermediates: &[rustls::Certificate],
165+
server_name: &rustls::ServerName,
166+
scts: &mut dyn Iterator<Item = &[u8]>,
167+
ocsp_response: &[u8],
168+
now: SystemTime,
169+
) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
170+
let maybe_dns_name = match server_name {
171+
rustls::ServerName::DnsName(dns) => Some(dns.as_ref()),
172+
_ => None,
173+
};
174+
175+
match maybe_dns_name {
176+
Some("provision.lexe.tech") => {
177+
self.attest_verifier.verify_server_cert(
178+
end_entity,
179+
intermediates,
180+
server_name,
181+
scts,
182+
ocsp_response,
183+
now,
184+
)
185+
}
186+
_ => self.lexe_verifier.verify_server_cert(
187+
end_entity,
188+
intermediates,
189+
server_name,
190+
scts,
191+
ocsp_response,
192+
now,
193+
),
194+
}
195+
}
196+
197+
fn request_scts(&self) -> bool {
198+
false
199+
}
200+
}
201+
202+
// -- impl ClientProvisionCertVerifier -- //
203+
204+
impl ServerCertVerifier for ClientRunCertVerifier {
205+
fn verify_server_cert(
206+
&self,
207+
end_entity: &rustls::Certificate,
208+
intermediates: &[rustls::Certificate],
209+
server_name: &rustls::ServerName,
210+
scts: &mut dyn Iterator<Item = &[u8]>,
211+
ocsp_response: &[u8],
212+
now: SystemTime,
213+
) -> Result<rustls::client::ServerCertVerified, rustls::Error> {
214+
let maybe_dns_name = match server_name {
215+
rustls::ServerName::DnsName(dns) => Some(dns.as_ref()),
216+
_ => None,
217+
};
218+
219+
match maybe_dns_name {
220+
Some("run.lexe.tech") => self.node_verifier.verify_server_cert(
221+
end_entity,
222+
intermediates,
223+
server_name,
224+
scts,
225+
ocsp_response,
226+
now,
227+
),
228+
_ => self.lexe_verifier.verify_server_cert(
229+
end_entity,
230+
intermediates,
231+
server_name,
232+
scts,
233+
ocsp_response,
234+
now,
235+
),
236+
}
237+
}
238+
239+
fn request_scts(&self) -> bool {
240+
false
241+
}
242+
}
243+
244+
#[cfg(test)]
245+
mod test {
246+
use std::sync::Arc;
247+
248+
use secrecy::Secret;
249+
use tokio::io::{duplex, AsyncReadExt, AsyncWriteExt};
250+
use tokio_rustls::rustls;
251+
252+
use super::*;
253+
use crate::rng::SmallRng;
254+
255+
#[tokio::test]
256+
async fn test_tls_handshake_succeeds() {
257+
// a fake pair of connected streams
258+
let (client_stream, server_stream) = duplex(4096);
259+
260+
let seed = [0x42; 32];
261+
let dns_name = "run.lexe.tech";
262+
263+
// client tries to connect
264+
let client = async move {
265+
// should be able to independently derive CA key pair
266+
let seed = RootSeed::new(Secret::new(seed));
267+
let mut rng = SmallRng::new();
268+
269+
// should be unused since no proxy
270+
let lexe_root =
271+
CaCert::from_key_pair(ed25519::from_seed(&[0xa1; 32]))
272+
.unwrap()
273+
.serialize_der_signed()
274+
.unwrap();
275+
let lexe_root = rustls::Certificate(lexe_root);
276+
277+
let config = crate::client::client_run_tls_config(
278+
&mut rng, &lexe_root, &seed,
279+
)
280+
.unwrap();
281+
282+
let connector = tokio_rustls::TlsConnector::from(Arc::new(config));
283+
let sni = rustls::ServerName::try_from(dns_name).unwrap();
284+
let mut stream =
285+
connector.connect(sni, client_stream).await.unwrap();
286+
287+
// client: >> send "hello"
288+
289+
stream.write_all(b"hello").await.unwrap();
290+
stream.flush().await.unwrap();
291+
stream.shutdown().await.unwrap();
292+
293+
// client: << recv "goodbye"
294+
295+
let mut resp = Vec::new();
296+
stream.read_to_end(&mut resp).await.unwrap();
297+
298+
assert_eq!(&resp, b"goodbye");
299+
};
300+
301+
// node should accept handshake
302+
let node = async move {
303+
// should be able to independently derive CA key pair
304+
let seed = RootSeed::new(Secret::new(seed));
305+
let ca_key_pair = seed.derive_client_ca_key_pair();
306+
let ca_cert = CaCert::from_key_pair(ca_key_pair).unwrap();
307+
308+
let node_key_pair = ed25519::from_seed(&[0xf0; 32]);
309+
let dns_names = vec![dns_name.to_owned()];
310+
let node_cert =
311+
NodeCert::from_key_pair(node_key_pair, dns_names).unwrap();
312+
313+
let config = node_tls_config(&node_cert, &ca_cert).unwrap();
314+
let acceptor = tokio_rustls::TlsAcceptor::from(Arc::new(config));
315+
let mut stream = acceptor.accept(server_stream).await.unwrap();
316+
317+
// node: >> recv "hello"
318+
319+
let mut req = Vec::new();
320+
stream.read_to_end(&mut req).await.unwrap();
321+
322+
assert_eq!(&req, b"hello");
323+
324+
// node: << send "goodbye"
325+
326+
stream.write_all(b"goodbye").await.unwrap();
327+
stream.shutdown().await.unwrap();
328+
};
329+
330+
tokio::join!(client, node);
331+
}
332+
}

0 commit comments

Comments
 (0)