Skip to content

Commit ebd4e37

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

File tree

6 files changed

+136
-7
lines changed

6 files changed

+136
-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: 14 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 {
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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::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+
}

0 commit comments

Comments
 (0)