Line data Source code
1 : use std::{io::Write, os::unix::fs::OpenOptionsExt, path::Path, time::Duration};
2 :
3 : use anyhow::{Context, Result, bail};
4 : use compute_api::responses::TlsConfig;
5 : use ring::digest;
6 : use x509_cert::Certificate;
7 :
8 : #[derive(Clone, Copy)]
9 : pub struct CertDigest(digest::Digest);
10 :
11 0 : pub async fn watch_cert_for_changes(cert_path: String) -> tokio::sync::watch::Receiver<CertDigest> {
12 0 : let mut digest = compute_digest(&cert_path).await;
13 0 : let (tx, rx) = tokio::sync::watch::channel(digest);
14 0 : tokio::spawn(async move {
15 0 : while !tx.is_closed() {
16 0 : let new_digest = compute_digest(&cert_path).await;
17 0 : if digest.0.as_ref() != new_digest.0.as_ref() {
18 0 : digest = new_digest;
19 0 : _ = tx.send(digest);
20 0 : }
21 :
22 0 : tokio::time::sleep(Duration::from_secs(60)).await
23 : }
24 0 : });
25 0 : rx
26 0 : }
27 :
28 0 : async fn compute_digest(cert_path: &str) -> CertDigest {
29 : loop {
30 0 : match try_compute_digest(cert_path).await {
31 0 : Ok(d) => break d,
32 0 : Err(e) => {
33 0 : tracing::error!("could not read cert file {e:?}");
34 0 : tokio::time::sleep(Duration::from_secs(1)).await
35 : }
36 : }
37 : }
38 0 : }
39 :
40 0 : async fn try_compute_digest(cert_path: &str) -> Result<CertDigest> {
41 0 : let data = tokio::fs::read(cert_path).await?;
42 : // sha256 is extremely collision resistent. can safely assume the digest to be unique
43 0 : Ok(CertDigest(digest::digest(&digest::SHA256, &data)))
44 0 : }
45 :
46 : pub const SERVER_CRT: &str = "server.crt";
47 : pub const SERVER_KEY: &str = "server.key";
48 :
49 0 : pub fn update_key_path_blocking(pg_data: &Path, tls_config: &TlsConfig) {
50 : loop {
51 0 : match try_update_key_path_blocking(pg_data, tls_config) {
52 0 : Ok(()) => break,
53 0 : Err(e) => {
54 0 : tracing::error!(error = ?e, "could not create key file");
55 0 : std::thread::sleep(Duration::from_secs(1))
56 : }
57 : }
58 : }
59 0 : }
60 :
61 : // Postgres requires the keypath be "secure". This means
62 : // 1. Owned by the postgres user.
63 : // 2. Have permission 600.
64 0 : fn try_update_key_path_blocking(pg_data: &Path, tls_config: &TlsConfig) -> Result<()> {
65 0 : let key = std::fs::read_to_string(&tls_config.key_path)?;
66 0 : let crt = std::fs::read_to_string(&tls_config.cert_path)?;
67 :
68 : // to mitigate a race condition during renewal.
69 0 : verify_key_cert(&key, &crt)?;
70 :
71 0 : let mut key_file = std::fs::OpenOptions::new()
72 0 : .write(true)
73 0 : .create(true)
74 0 : .truncate(true)
75 0 : .mode(0o600)
76 0 : .open(pg_data.join(SERVER_KEY))?;
77 :
78 0 : let mut crt_file = std::fs::OpenOptions::new()
79 0 : .write(true)
80 0 : .create(true)
81 0 : .truncate(true)
82 0 : .mode(0o600)
83 0 : .open(pg_data.join(SERVER_CRT))?;
84 :
85 0 : key_file.write_all(key.as_bytes())?;
86 0 : crt_file.write_all(crt.as_bytes())?;
87 :
88 0 : Ok(())
89 0 : }
90 :
91 2 : fn verify_key_cert(key: &str, cert: &str) -> Result<()> {
92 : use x509_cert::der::oid::db::rfc5912::ECDSA_WITH_SHA_256;
93 :
94 2 : let certs = Certificate::load_pem_chain(cert.as_bytes())
95 2 : .context("decoding PEM encoded certificates")?;
96 :
97 : // First certificate is our server-cert,
98 : // all the rest of the certs are the CA cert chain.
99 2 : let Some(cert) = certs.first() else {
100 0 : bail!("no certificates found");
101 : };
102 :
103 2 : match cert.signature_algorithm.oid {
104 : ECDSA_WITH_SHA_256 => {
105 2 : let key = p256::SecretKey::from_sec1_pem(key).context("parse key")?;
106 :
107 2 : let a = key.public_key().to_sec1_bytes();
108 2 : let b = cert
109 2 : .tbs_certificate
110 2 : .subject_public_key_info
111 2 : .subject_public_key
112 2 : .raw_bytes();
113 2 :
114 2 : if *a != *b {
115 1 : bail!("private key file does not match certificate")
116 1 : }
117 : }
118 0 : _ => bail!("unknown TLS key type"),
119 : }
120 :
121 1 : Ok(())
122 2 : }
123 :
124 : #[cfg(test)]
125 : mod tests {
126 : use super::verify_key_cert;
127 :
128 : /// Real certificate chain file, generated by cert-manager in dev.
129 : /// The server auth certificate has expired since 2025-04-24T15:41:35Z.
130 : const CERT: &str = "
131 : -----BEGIN CERTIFICATE-----
132 : MIICCDCCAa+gAwIBAgIQKhLomFcNULbZA/bPdGzaSzAKBggqhkjOPQQDAjBEMQsw
133 : CQYDVQQGEwJVUzESMBAGA1UEChMJTmVvbiBJbmMuMSEwHwYDVQQDExhOZW9uIEs4
134 : cyBJbnRlcm1lZGlhdGUgQ0EwHhcNMjUwNDIzMTU0MTM1WhcNMjUwNDI0MTU0MTM1
135 : WjBBMT8wPQYDVQQDEzZjb21wdXRlLXdpc3B5LWdyYXNzLXcwY21laWp3LmRlZmF1
136 : bHQuc3ZjLmNsdXN0ZXIubG9jYWwwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATF
137 : QCcG2m/EVHAiZtSsYgVnHgoTjUL/Jtwfdrpvz2t0bVRZmBmSKhlo53uPV9Y5eKFG
138 : AmR54p9/gT2eO3xU7vAgo4GFMIGCMA4GA1UdDwEB/wQEAwIFoDAMBgNVHRMBAf8E
139 : AjAAMB8GA1UdIwQYMBaAFFR2JAhXkeiNQNEixTvAYIwxUu3QMEEGA1UdEQQ6MDiC
140 : NmNvbXB1dGUtd2lzcHktZ3Jhc3MtdzBjbWVpancuZGVmYXVsdC5zdmMuY2x1c3Rl
141 : ci5sb2NhbDAKBggqhkjOPQQDAgNHADBEAiBLG22wKG8XS9e9RxBT+kmUx/kIThcP
142 : DIpp7jx0PrFcdQIgEMTdnXpx5Cv/Z0NIEDxtMHUD7G0vuRPfztki36JuakM=
143 : -----END CERTIFICATE-----
144 : -----BEGIN CERTIFICATE-----
145 : MIICFzCCAb6gAwIBAgIUbbX98N2Ip6lWAONRk8dU9hSz+YIwCgYIKoZIzj0EAwIw
146 : RDELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCU5lb24gSW5jLjEhMB8GA1UEAxMYTmVv
147 : biBBV1MgSW50ZXJtZWRpYXRlIENBMB4XDTI1MDQyMjE1MTAxMFoXDTI1MDcyMTE1
148 : MTAxMFowRDELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCU5lb24gSW5jLjEhMB8GA1UE
149 : AxMYTmVvbiBLOHMgSW50ZXJtZWRpYXRlIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0D
150 : AQcDQgAE5++m5owqNI4BPMTVNIUQH0qvU7pYhdpHGVGhdj/Lgars6ROvE6uSNQV4
151 : SAmJN5HBzj5/6kLQaTPWpXW7EHXjK6OBjTCBijAOBgNVHQ8BAf8EBAMCAQYwEgYD
152 : VR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUVHYkCFeR6I1A0SLFO8BgjDFS7dAw
153 : HwYDVR0jBBgwFoAUgHfNXfyKtHO0V9qoLOWCjkNiaI8wJAYDVR0eAQH/BBowGKAW
154 : MBSCEi5zdmMuY2x1c3Rlci5sb2NhbDAKBggqhkjOPQQDAgNHADBEAiBObVFFdXaL
155 : QpOXmN60dYUNnQRwjKreFduEkQgOdOlssgIgVAdJJQFgvlrvEOBhY8j5WyeKRwUN
156 : k/ALs6KpgaFBCGY=
157 : -----END CERTIFICATE-----
158 : -----BEGIN CERTIFICATE-----
159 : MIIB4jCCAYegAwIBAgIUFlxWFn/11yoGdmD+6gf+yQMToS0wCgYIKoZIzj0EAwIw
160 : ODELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCU5lb24gSW5jLjEVMBMGA1UEAxMMTmVv
161 : biBSb290IENBMB4XDTI1MDQwMzA3MTUyMloXDTI2MDQwMzA3MTUyMlowRDELMAkG
162 : A1UEBhMCVVMxEjAQBgNVBAoTCU5lb24gSW5jLjEhMB8GA1UEAxMYTmVvbiBBV1Mg
163 : SW50ZXJtZWRpYXRlIENBMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEqonG/IQ6
164 : ZxtEtOUTkkoNopPieXDO5CBKUkNFTGeJEB7OxRlSpYJgsBpaYIaD6Vc4sVk3thIF
165 : p+pLw52idQOIN6NjMGEwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w
166 : HQYDVR0OBBYEFIB3zV38irRztFfaqCzlgo5DYmiPMB8GA1UdIwQYMBaAFKh7M4/G
167 : FHvr/ORDQZt4bMLlJvHCMAoGCCqGSM49BAMCA0kAMEYCIQCbS4x7QPslONzBYbjC
168 : UQaQ0QLDW4CJHvQ4u4gbWFG87wIhAJMsHQHjP9qTT27Q65zQCR7O8QeLAfha1jrH
169 : Ag/LsxSr
170 : -----END CERTIFICATE-----
171 : ";
172 :
173 : /// The key corresponding to [`CERT`]
174 : const KEY: &str = "
175 : -----BEGIN EC PRIVATE KEY-----
176 : MHcCAQEEIDnAnrqmIJjndCLWP1iIO5X3X63Aia48TGpGuMXwvm6IoAoGCCqGSM49
177 : AwEHoUQDQgAExUAnBtpvxFRwImbUrGIFZx4KE41C/ybcH3a6b89rdG1UWZgZkioZ
178 : aOd7j1fWOXihRgJkeeKff4E9njt8VO7wIA==
179 : -----END EC PRIVATE KEY-----
180 : ";
181 :
182 : /// An incorrect key.
183 : const INCORRECT_KEY: &str = "
184 : -----BEGIN EC PRIVATE KEY-----
185 : MHcCAQEEIL6WqqBDyvM0HWz7Ir5M5+jhFWB7IzOClGn26OPrzHCXoAoGCCqGSM49
186 : AwEHoUQDQgAE7XVvdOy5lfwtNKb+gJEUtnG+DrnnXLY5LsHDeGQKV9PTRcEMeCrG
187 : YZzHyML4P6Sr4yi2ts+4B9i47uvAG8+XwQ==
188 : -----END EC PRIVATE KEY-----
189 : ";
190 :
191 : #[test]
192 1 : fn certificate_verification() {
193 1 : verify_key_cert(KEY, CERT).unwrap();
194 1 : }
195 :
196 : #[test]
197 : #[should_panic(expected = "private key file does not match certificate")]
198 1 : fn certificate_verification_fail() {
199 1 : verify_key_cert(INCORRECT_KEY, CERT).unwrap();
200 1 : }
201 : }
|