1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
//! Shared mutual-TLS (client-certificate) configuration for HTTP connectors.
//!
//! This is a pure data + validation type — it deliberately has **no** dependency
//! on any TLS or HTTP crate, so `faucet-core` stays lightweight. Each HTTP
//! source that supports mTLS (`rest` / `xml` / `graphql`) owns the small,
//! feature-gated code that turns a [`TlsClientConfig`] into a `reqwest::Identity`.
use crate::FaucetError;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
/// Client-certificate (mutual TLS) configuration for the HTTP sources.
///
/// Supply **either** a PEM certificate + key pair (`client_cert` + `client_key`)
/// **or** a PKCS#12 identity file (`client_identity_pkcs12` [+ `pkcs12_password`]).
/// PEM values may be inline or pulled in with `${file:…}` / `${secret:…}` /
/// `${vault:…}`; the PKCS#12 value is a path to a `.p12`/`.pfx` file (its binary
/// content can't be embedded in a text config).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default, PartialEq, Eq)]
pub struct TlsClientConfig {
/// PEM-encoded client certificate chain. Pair with `client_key`.
pub client_cert: Option<String>,
/// PEM-encoded PKCS#8 private key. Pair with `client_cert`.
pub client_key: Option<String>,
/// Path to a PKCS#12 (`.p12`/`.pfx`) identity file — an alternative to the
/// PEM pair.
pub client_identity_pkcs12: Option<String>,
/// Password for the PKCS#12 file (omit or empty if the file is unencrypted).
pub pkcs12_password: Option<String>,
/// Minimum negotiated TLS version: `"1.2"` or `"1.3"`. Defaults to the
/// TLS backend's own minimum when unset.
pub min_version: Option<String>,
}
impl TlsClientConfig {
/// Validate the shape before any network setup: exactly one identity source
/// (PEM pair XOR PKCS#12), no half-specified PEM pair, and a recognized
/// `min_version`. Cheap and dependency-free — safe to call in an infallible
/// connector's registry-side validation.
pub fn validate(&self) -> Result<(), FaucetError> {
let has_pem = self.client_cert.is_some() || self.client_key.is_some();
let has_p12 = self.client_identity_pkcs12.is_some();
if has_pem && has_p12 {
return Err(FaucetError::Config(
"tls: specify either a PEM pair (client_cert + client_key) or \
client_identity_pkcs12, not both"
.into(),
));
}
if !has_pem && !has_p12 {
return Err(FaucetError::Config(
"tls: provide a PEM pair (client_cert + client_key) or \
client_identity_pkcs12"
.into(),
));
}
if has_pem && (self.client_cert.is_none() || self.client_key.is_none()) {
return Err(FaucetError::Config(
"tls: client_cert and client_key must be provided together".into(),
));
}
if let Some(v) = &self.min_version
&& !matches!(v.as_str(), "1.2" | "1.3")
{
return Err(FaucetError::Config(format!(
"tls: unsupported min_version {v:?} (expected \"1.2\" or \"1.3\")"
)));
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::TlsClientConfig;
fn pem() -> TlsClientConfig {
TlsClientConfig {
client_cert: Some("cert".into()),
client_key: Some("key".into()),
..Default::default()
}
}
#[test]
fn valid_pem_pair_passes() {
assert!(pem().validate().is_ok());
}
#[test]
fn valid_pkcs12_passes() {
let c = TlsClientConfig {
client_identity_pkcs12: Some("/path/id.p12".into()),
pkcs12_password: Some("pw".into()),
..Default::default()
};
assert!(c.validate().is_ok());
}
#[test]
fn pem_and_pkcs12_together_is_rejected() {
let c = TlsClientConfig {
client_cert: Some("cert".into()),
client_key: Some("key".into()),
client_identity_pkcs12: Some("/path/id.p12".into()),
..Default::default()
};
assert!(c.validate().is_err());
}
#[test]
fn empty_config_is_rejected() {
assert!(TlsClientConfig::default().validate().is_err());
}
#[test]
fn half_pem_pair_is_rejected() {
let cert_only = TlsClientConfig {
client_cert: Some("cert".into()),
..Default::default()
};
assert!(cert_only.validate().is_err());
let key_only = TlsClientConfig {
client_key: Some("key".into()),
..Default::default()
};
assert!(key_only.validate().is_err());
}
#[test]
fn min_version_is_validated() {
let mut c = pem();
c.min_version = Some("1.3".into());
assert!(c.validate().is_ok());
c.min_version = Some("1.2".into());
assert!(c.validate().is_ok());
c.min_version = Some("1.1".into());
assert!(c.validate().is_err());
}
}