1use crate::error::{Error, Result};
18use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier};
19use rustls::crypto::{CryptoProvider, WebPkiSupportedAlgorithms};
20use rustls::server::danger::{ClientCertVerified, ClientCertVerifier};
21use rustls::{ClientConfig, DigitallySignedStruct, DistinguishedName, ServerConfig, SignatureScheme};
22use rustls_pki_types::pem::PemObject;
23use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName, UnixTime};
24use sha2::{Digest, Sha256};
25use std::collections::HashSet;
26use std::path::{Path, PathBuf};
27use std::sync::Arc;
28
29pub const PEER_NAME: &str = "tsaur-peer";
31
32pub const MAX_REVOCATION_FILE: u64 = 1 << 20;
34
35pub struct Identity {
36 pub cert: CertificateDer<'static>,
37 pub key: PrivateKeyDer<'static>,
38 pub fingerprint: [u8; 32],
39}
40
41impl Clone for Identity {
42 fn clone(&self) -> Self {
43 Identity { cert: self.cert.clone(), key: self.key.clone_key(), fingerprint: self.fingerprint }
44 }
45}
46
47pub fn fingerprint(cert: &CertificateDer<'_>) -> [u8; 32] {
48 let mut h = Sha256::new();
49 h.update(cert.as_ref());
50 h.finalize().into()
51}
52
53pub fn cert_path(identity: &Path) -> PathBuf {
54 PathBuf::from(format!("{}.crt", identity.display()))
55}
56
57fn provider() -> Arc<CryptoProvider> {
58 Arc::new(rustls::crypto::ring::default_provider())
59}
60
61pub fn generate(path: &Path) -> Result<Identity> {
64 let key = rcgen::KeyPair::generate().map_err(|e| Error::Crypto(format!("key generation: {e}")))?;
65 let params = rcgen::CertificateParams::new(vec![PEER_NAME.to_string()]).map_err(|e| Error::Crypto(format!("certificate parameters: {e}")))?;
66 let cert = params.self_signed(&key).map_err(|e| Error::Crypto(format!("self-signed certificate: {e}")))?;
67 std::fs::write(path, key.serialize_pem())?;
68 std::fs::write(cert_path(path), cert.pem())?;
69 load(path)
70}
71
72pub fn load(path: &Path) -> Result<Identity> {
74 let key_pem = std::fs::read(path).map_err(|e| Error::Missing(format!("identity {}: {e}", path.display())))?;
75 let cert_pem = std::fs::read(cert_path(path)).map_err(|e| Error::Missing(format!("certificate {}: {e}", cert_path(path).display())))?;
76 let key = PrivateKeyDer::from_pem_slice(&key_pem).map_err(|e| Error::Crypto(format!("identity {}: {e}", path.display())))?;
77 let cert = CertificateDer::from_pem_slice(&cert_pem).map_err(|e| Error::Crypto(format!("certificate {}: {e}", cert_path(path).display())))?;
78 let fingerprint = fingerprint(&cert);
79 Ok(Identity { cert, key, fingerprint })
80}
81
82pub fn parse_fingerprint(hex_str: &str) -> Result<[u8; 32]> {
83 let v = hex::decode(hex_str.trim().replace(':', "")).map_err(|_| Error::Invalid("fingerprint must be 64 hex characters".into()))?;
84 let arr: [u8; 32] = v.as_slice().try_into().map_err(|_| Error::Invalid("fingerprint must be 32 bytes".into()))?;
85 Ok(arr)
86}
87
88#[derive(Clone, Debug, Default, PartialEq, Eq)]
92pub struct Revocations(HashSet<[u8; 32]>);
93
94impl Revocations {
95 pub fn load(path: &Path) -> Result<Revocations> {
97 let origin = path.display().to_string();
98 let len = std::fs::metadata(path).map_err(|e| Error::Missing(format!("revocation file {origin}: {e}")))?.len();
99 if len > MAX_REVOCATION_FILE {
100 return Err(Error::Limit(format!("revocation file {origin}: {len} bytes, more than the {MAX_REVOCATION_FILE}-byte limit; is this the right file?")));
101 }
102 let text = std::fs::read_to_string(path).map_err(|e| Error::Missing(format!("revocation file {origin}: {e}")))?;
103 Self::parse(&text, &origin)
104 }
105
106 pub fn parse(text: &str, origin: &str) -> Result<Revocations> {
108 if text.len() as u64 > MAX_REVOCATION_FILE {
109 return Err(Error::Limit(format!("revocation file {origin}: more than the {MAX_REVOCATION_FILE}-byte limit")));
110 }
111 let mut set = HashSet::new();
112 for (i, raw) in text.lines().enumerate() {
113 let line = raw.split('#').next().unwrap_or("").trim();
114 if line.is_empty() {
115 continue;
116 }
117 let fp = parse_fingerprint(line).map_err(|_| Error::Invalid(format!("revocation file {origin}, line {}: fingerprint must be 64 hex characters", i + 1)))?;
118 set.insert(fp);
119 }
120 Ok(Revocations(set))
121 }
122
123 pub fn contains(&self, fp: &[u8; 32]) -> bool {
124 self.0.contains(fp)
125 }
126
127 pub fn len(&self) -> usize {
128 self.0.len()
129 }
130
131 pub fn is_empty(&self) -> bool {
132 self.0.is_empty()
133 }
134
135 pub fn iter(&self) -> impl Iterator<Item = &[u8; 32]> {
136 self.0.iter()
137 }
138}
139
140impl FromIterator<[u8; 32]> for Revocations {
141 fn from_iter<I: IntoIterator<Item = [u8; 32]>>(iter: I) -> Self {
142 Revocations(iter.into_iter().collect())
143 }
144}
145
146#[derive(Debug)]
149struct PinnedServer {
150 expected: [u8; 32],
151 revoked: Revocations,
152 algs: WebPkiSupportedAlgorithms,
153}
154
155impl ServerCertVerifier for PinnedServer {
156 fn verify_server_cert(
157 &self,
158 end_entity: &CertificateDer<'_>,
159 _intermediates: &[CertificateDer<'_>],
160 _server_name: &ServerName<'_>,
161 _ocsp: &[u8],
162 _now: UnixTime,
163 ) -> std::result::Result<ServerCertVerified, rustls::Error> {
164 let fp = fingerprint(end_entity);
165 if self.revoked.contains(&fp) {
166 Err(rustls::Error::General(format!("peer certificate fingerprint {} is revoked", hex::encode(fp))))
167 } else if fp == self.expected {
168 Ok(ServerCertVerified::assertion())
169 } else {
170 Err(rustls::Error::General(format!("peer certificate fingerprint {} does not match the pinned identity", hex::encode(fp))))
171 }
172 }
173
174 fn verify_tls12_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
175 rustls::crypto::verify_tls12_signature(message, cert, dss, &self.algs)
176 }
177
178 fn verify_tls13_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
179 rustls::crypto::verify_tls13_signature(message, cert, dss, &self.algs)
180 }
181
182 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
183 self.algs.supported_schemes()
184 }
185}
186
187#[derive(Debug)]
190struct PinnedClients {
191 allowed: Vec<[u8; 32]>,
192 revoked: Revocations,
193 algs: WebPkiSupportedAlgorithms,
194}
195
196impl ClientCertVerifier for PinnedClients {
197 fn root_hint_subjects(&self) -> &[DistinguishedName] {
198 &[]
199 }
200
201 fn verify_client_cert(&self, end_entity: &CertificateDer<'_>, _intermediates: &[CertificateDer<'_>], _now: UnixTime) -> std::result::Result<ClientCertVerified, rustls::Error> {
202 let fp = fingerprint(end_entity);
203 if self.revoked.contains(&fp) {
204 Err(rustls::Error::General(format!("client certificate fingerprint {} is revoked", hex::encode(fp))))
205 } else if self.allowed.contains(&fp) {
206 Ok(ClientCertVerified::assertion())
207 } else {
208 Err(rustls::Error::General(format!("client certificate fingerprint {} is not in the allow list", hex::encode(fp))))
209 }
210 }
211
212 fn verify_tls12_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
213 rustls::crypto::verify_tls12_signature(message, cert, dss, &self.algs)
214 }
215
216 fn verify_tls13_signature(&self, message: &[u8], cert: &CertificateDer<'_>, dss: &DigitallySignedStruct) -> std::result::Result<HandshakeSignatureValid, rustls::Error> {
217 rustls::crypto::verify_tls13_signature(message, cert, dss, &self.algs)
218 }
219
220 fn supported_verify_schemes(&self) -> Vec<SignatureScheme> {
221 self.algs.supported_schemes()
222 }
223}
224
225pub fn server_config(identity: &Identity, allowed: &[[u8; 32]]) -> Result<Arc<ServerConfig>> {
229 server_config_with(identity, allowed, &Revocations::default())
230}
231
232pub fn server_config_with(identity: &Identity, allowed: &[[u8; 32]], revoked: &Revocations) -> Result<Arc<ServerConfig>> {
236 let provider = provider();
237 let algs = provider.signature_verification_algorithms;
238 let builder = ServerConfig::builder_with_provider(provider).with_protocol_versions(&[&rustls::version::TLS13]).map_err(|e| Error::Crypto(format!("tls: {e}")))?;
239 let cert_chain = vec![identity.cert.clone()];
240 let config = if allowed.is_empty() {
241 builder.with_no_client_auth().with_single_cert(cert_chain, identity.key.clone_key())
242 } else {
243 builder.with_client_cert_verifier(Arc::new(PinnedClients { allowed: allowed.to_vec(), revoked: revoked.clone(), algs })).with_single_cert(cert_chain, identity.key.clone_key())
244 }
245 .map_err(|e| Error::Crypto(format!("tls server configuration: {e}")))?;
246 Ok(Arc::new(config))
247}
248
249pub fn client_config(identity: Option<&Identity>, peer: [u8; 32]) -> Result<Arc<ClientConfig>> {
252 client_config_with(identity, peer, &Revocations::default())
253}
254
255pub fn client_config_with(identity: Option<&Identity>, peer: [u8; 32], revoked: &Revocations) -> Result<Arc<ClientConfig>> {
258 let provider = provider();
259 let algs = provider.signature_verification_algorithms;
260 let builder = ClientConfig::builder_with_provider(provider)
261 .with_safe_default_protocol_versions()
262 .map_err(|e| Error::Crypto(format!("tls: {e}")))?
263 .dangerous()
264 .with_custom_certificate_verifier(Arc::new(PinnedServer { expected: peer, revoked: revoked.clone(), algs }));
265 let config = match identity {
266 Some(id) => builder.with_client_auth_cert(vec![id.cert.clone()], id.key.clone_key()).map_err(|e| Error::Crypto(format!("tls client configuration: {e}")))?,
267 None => builder.with_no_client_auth(),
268 };
269 Ok(Arc::new(config))
270}
271
272pub fn server_name() -> ServerName<'static> {
273 ServerName::try_from(PEER_NAME.to_string()).expect("constant peer name")
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
281 fn revocation_file_parses_comments_blank_lines_and_colons() {
282 let a = "3f".repeat(32);
283 let b: String = (0..32).map(|_| "7B:").collect::<String>().trim_end_matches(':').to_string();
284 let text = format!("# tsaur revocations\n\n{a} # laptop key lost\r\n {b}\n\n# trailing comment only\n");
285 let r = Revocations::parse(&text, "test").unwrap();
286 assert_eq!(r.len(), 2);
287 assert!(r.contains(&[0x3f; 32]) && r.contains(&[0x7b; 32]));
288 assert_eq!(Revocations::parse(&format!("{a}\n{a}\n"), "t").unwrap().len(), 1);
290 assert!(Revocations::parse("", "t").unwrap().is_empty());
291 assert!(Revocations::parse("# only comments\n\n", "t").unwrap().is_empty());
292 let err = Revocations::parse("# ok\n\nnot-a-fingerprint\n", "revoked.txt").unwrap_err().to_string();
294 assert!(err.contains("revoked.txt, line 3"), "{err}");
295 let err = Revocations::parse(&"ab".repeat(31), "t").unwrap_err().to_string();
296 assert!(err.contains("line 1"), "{err}");
297 let big = format!("{a}\n").repeat((MAX_REVOCATION_FILE as usize / 65) + 2);
299 assert!(matches!(Revocations::parse(&big, "t"), Err(Error::Limit(_))));
300 }
301}