LCOV - code coverage report
Current view: top level - compute_tools/src - tls.rs (source / functions) Coverage Total Hit
Test: 2620485e474b48c32427149a5d91ef8fc2cd649e.info Lines: 30.8 % 78 24
Test Date: 2025-05-01 22:50:11 Functions: 25.0 % 12 3

            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              : }
        

Generated by: LCOV version 2.1-beta