Skip to content

Commit ae5f9f3

Browse files
committed
Add support for native root certs
1 parent d545a1c commit ae5f9f3

File tree

6 files changed

+230
-7
lines changed

6 files changed

+230
-7
lines changed

Cargo.lock

Lines changed: 4 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nativelink-config/src/stores.rs

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -858,16 +858,28 @@ pub enum StoreType {
858858
#[derive(Serialize, Deserialize, Debug, Clone)]
859859
pub struct ClientTlsConfig {
860860
/// Path to the certificate authority to use to validate the remote.
861-
#[serde(deserialize_with = "convert_string_with_shellexpand")]
862-
pub ca_file: String,
861+
///
862+
/// Default: None
863+
#[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
864+
pub ca_file: Option<String>,
863865

864866
/// Path to the certificate file for client authentication.
865-
#[serde(deserialize_with = "convert_optional_string_with_shellexpand")]
867+
///
868+
/// Default: None
869+
#[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
866870
pub cert_file: Option<String>,
867871

868872
/// Path to the private key file for client authentication.
869-
#[serde(deserialize_with = "convert_optional_string_with_shellexpand")]
873+
///
874+
/// Default: None
875+
#[serde(default, deserialize_with = "convert_optional_string_with_shellexpand")]
870876
pub key_file: Option<String>,
877+
878+
/// If set the client will use the native roots for TLS connections.
879+
///
880+
/// Default: false
881+
#[serde(default)]
882+
pub use_native_roots: Option<bool>,
871883
}
872884

873885
#[derive(Serialize, Deserialize, Debug, Clone)]

nativelink-util/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ rust_test_suite(
100100
"tests/proto_stream_utils_test.rs",
101101
"tests/resource_info_test.rs",
102102
"tests/retry_test.rs",
103+
"tests/tls_utils_test.rs",
103104
],
104105
compile_data = [
105106
"tests/data/SekienAkashita.jpg",
@@ -124,6 +125,7 @@ rust_test_suite(
124125
"@crates//:rand",
125126
"@crates//:serde_json",
126127
"@crates//:sha2",
128+
"@crates//:tempfile",
127129
"@crates//:tokio",
128130
"@crates//:tokio-stream",
129131
"@crates//:tokio-util",

nativelink-util/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ rand = { version = "0.9.0", default-features = false, features = [
5151
rlimit = { version = "0.10.2", default-features = false }
5252
serde = { version = "1.0.219", default-features = false }
5353
sha2 = { version = "0.10.8", default-features = false }
54+
tempfile = "3.20.0"
5455
tokio = { version = "1.44.1", features = [
5556
"fs",
5657
"io-util",
@@ -62,6 +63,7 @@ tokio-stream = { version = "0.1.17", features = [
6263
], default-features = false }
6364
tokio-util = { version = "0.7.14" }
6465
tonic = { version = "0.13.0", features = [
66+
"tls-native-roots",
6567
"tls-ring",
6668
"transport",
6769
], default-features = false }

nativelink-util/src/tls_utils.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,21 @@ pub fn load_client_config(
2323
return Ok(None);
2424
};
2525

26+
if config.use_native_roots == Some(true) {
27+
return Ok(Some(
28+
tonic::transport::ClientTlsConfig::new().with_native_roots(),
29+
));
30+
}
31+
32+
let Some(ca_file) = &config.ca_file else {
33+
return Err(make_err!(
34+
Code::Internal,
35+
"CA certificate must be provided if not using native root certificates"
36+
));
37+
};
38+
2639
let read_config = tonic::transport::ClientTlsConfig::new().ca_certificate(
27-
tonic::transport::Certificate::from_pem(std::fs::read_to_string(&config.ca_file)?),
40+
tonic::transport::Certificate::from_pem(std::fs::read_to_string(ca_file)?),
2841
);
2942
let config = if let Some(client_certificate) = &config.cert_file {
3043
let Some(client_key) = &config.key_file else {
@@ -93,6 +106,11 @@ pub fn endpoint_from(
93106
.tls_config(tls_config)
94107
.map_err(|e| make_input_err!("Setting mTLS configuration: {e:?}"))?
95108
} else {
109+
if endpoint.scheme_str() == Some("https") {
110+
return Err(make_input_err!(
111+
"The scheme of {endpoint} is https or grpcs, but no TLS configuration was provided"
112+
));
113+
}
96114
tonic::transport::Endpoint::from(endpoint)
97115
};
98116

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright 2025 The NativeLink Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use nativelink_config::stores::ClientTlsConfig;
16+
use nativelink_error::Error;
17+
use nativelink_macro::nativelink_test;
18+
use nativelink_util::tls_utils::{endpoint_from, load_client_config};
19+
use tempfile::NamedTempFile;
20+
21+
#[nativelink_test]
22+
async fn test_load_client_config_none() -> Result<(), Error> {
23+
let config = load_client_config(&None)?;
24+
assert!(config.is_none());
25+
Ok(())
26+
}
27+
28+
#[nativelink_test]
29+
async fn test_load_client_config_native_roots() -> Result<(), Error> {
30+
let config = load_client_config(&Some(ClientTlsConfig {
31+
use_native_roots: Some(true),
32+
ca_file: None,
33+
cert_file: None,
34+
key_file: None,
35+
}))?;
36+
assert!(config.is_some());
37+
Ok(())
38+
}
39+
40+
#[nativelink_test]
41+
async fn test_load_client_config_missing_ca() -> Result<(), Error> {
42+
let result = load_client_config(&Some(ClientTlsConfig {
43+
use_native_roots: None,
44+
ca_file: None,
45+
cert_file: None,
46+
key_file: None,
47+
}));
48+
assert!(matches!(
49+
result,
50+
Err(e) if e.to_string().contains("CA certificate must be provided")
51+
));
52+
Ok(())
53+
}
54+
55+
#[nativelink_test]
56+
async fn test_load_client_config_cert_without_key() -> Result<(), Error> {
57+
let temp_file = NamedTempFile::new()?;
58+
let result = load_client_config(&Some(ClientTlsConfig {
59+
use_native_roots: None,
60+
ca_file: Some(temp_file.path().to_str().unwrap().to_string()),
61+
cert_file: Some("tls.crt".to_string()),
62+
key_file: None,
63+
}));
64+
assert!(matches!(
65+
result,
66+
Err(e) if e.to_string().contains("Client certificate specified, but no key")
67+
));
68+
Ok(())
69+
}
70+
71+
#[nativelink_test]
72+
async fn test_load_client_config_key_without_cert() -> Result<(), Error> {
73+
let temp_file = NamedTempFile::new()?;
74+
let result = load_client_config(&Some(ClientTlsConfig {
75+
use_native_roots: None,
76+
ca_file: Some(temp_file.path().to_str().unwrap().to_string()),
77+
cert_file: None,
78+
key_file: Some("tls.key".to_string()),
79+
}));
80+
assert!(matches!(
81+
result,
82+
Err(e) if e.to_string().contains("Client key specified, but no certificate")
83+
));
84+
Ok(())
85+
}
86+
87+
#[nativelink_test]
88+
async fn test_load_client_config_with_cert_files() -> Result<(), Error> {
89+
let temp_file = NamedTempFile::new()?;
90+
let config = load_client_config(&Some(ClientTlsConfig {
91+
use_native_roots: None,
92+
ca_file: Some(temp_file.path().to_str().unwrap().to_string()),
93+
cert_file: Some(temp_file.path().to_str().unwrap().to_string()),
94+
key_file: Some(temp_file.path().to_str().unwrap().to_string()),
95+
}))?;
96+
assert!(config.is_some());
97+
Ok(())
98+
}
99+
100+
#[nativelink_test]
101+
async fn test_endpoint_from_http() -> Result<(), Error> {
102+
let endpoint = endpoint_from("http://localhost:50051", None)?;
103+
assert_eq!(endpoint.uri().scheme_str(), Some("http"));
104+
assert_eq!(endpoint.uri().host(), Some("localhost"));
105+
assert_eq!(endpoint.uri().port_u16(), Some(50051));
106+
Ok(())
107+
}
108+
109+
#[nativelink_test]
110+
async fn test_endpoint_from_https_with_tls() -> Result<(), Error> {
111+
let tls_config = load_client_config(&Some(ClientTlsConfig {
112+
use_native_roots: Some(true),
113+
ca_file: None,
114+
cert_file: None,
115+
key_file: None,
116+
}))?;
117+
let endpoint = endpoint_from("https://example.com", tls_config)?;
118+
assert_eq!(endpoint.uri().scheme_str(), Some("https"));
119+
assert_eq!(endpoint.uri().host(), Some("example.com"));
120+
Ok(())
121+
}
122+
123+
#[nativelink_test]
124+
async fn test_endpoint_from_grpcs_with_tls() -> Result<(), Error> {
125+
let tls_config = load_client_config(&Some(ClientTlsConfig {
126+
use_native_roots: Some(true),
127+
ca_file: None,
128+
cert_file: None,
129+
key_file: None,
130+
}))?;
131+
let endpoint = endpoint_from("grpcs://example.com", tls_config)?;
132+
assert_eq!(endpoint.uri().scheme_str(), Some("https"));
133+
assert_eq!(endpoint.uri().host(), Some("example.com"));
134+
Ok(())
135+
}
136+
137+
#[nativelink_test]
138+
async fn test_endpoint_from_https_without_tls() -> Result<(), Error> {
139+
let result = endpoint_from("https://example.com", None);
140+
assert!(matches!(
141+
result,
142+
Err(e) if e.to_string().contains("is https or grpcs, but no TLS configuration was provided")
143+
));
144+
Ok(())
145+
}
146+
147+
#[nativelink_test]
148+
async fn test_endpoint_from_http_with_tls() -> Result<(), Error> {
149+
let tls_config = load_client_config(&Some(ClientTlsConfig {
150+
use_native_roots: Some(true),
151+
ca_file: None,
152+
cert_file: None,
153+
key_file: None,
154+
}))?;
155+
let result = endpoint_from("http://example.com:8080", tls_config);
156+
assert!(matches!(
157+
result,
158+
Err(e) if e.to_string().contains("but the scheme is not https or grpcs")
159+
));
160+
Ok(())
161+
}
162+
163+
#[nativelink_test]
164+
async fn test_endpoint_from_invalid_uri() -> Result<(), Error> {
165+
let result = endpoint_from("not a valid uri", None);
166+
assert!(matches!(
167+
result,
168+
Err(e) if e.to_string().contains("Unable to parse endpoint")
169+
));
170+
Ok(())
171+
}
172+
173+
#[nativelink_test]
174+
async fn test_endpoint_from_missing_authority() -> Result<(), Error> {
175+
let tls_config = load_client_config(&Some(ClientTlsConfig {
176+
use_native_roots: Some(true),
177+
ca_file: None,
178+
cert_file: None,
179+
key_file: None,
180+
}))?;
181+
let result = endpoint_from("/path/no/authority", tls_config);
182+
assert!(matches!(
183+
result,
184+
Err(e) if e.to_string().contains("Unable to determine authority of endpoint")
185+
));
186+
Ok(())
187+
}

0 commit comments

Comments
 (0)