From 5ef5da8b97436119fddc7ce0c093382b6fef7500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20CORTIER?= Date: Fri, 27 Sep 2024 17:43:02 +0900 Subject: [PATCH] chore(tools): update jet-doctor --- .github/workflows/build-tools.yml | 2 +- tools/jet-doctor/Cargo.toml | 11 +- tools/jet-doctor/src/main.rs | 211 ++++++++++++++++++++++++------ 3 files changed, 176 insertions(+), 48 deletions(-) diff --git a/.github/workflows/build-tools.yml b/.github/workflows/build-tools.yml index 76911431a..0e8dfb319 100644 --- a/.github/workflows/build-tools.yml +++ b/.github/workflows/build-tools.yml @@ -219,4 +219,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: ${{ matrix.tool }}-${{ matrix.arch }}-${{ matrix.platform }} - path: ${{ matrix.tool }}.dmg \ No newline at end of file + path: ${{ matrix.tool }}.dmg diff --git a/tools/jet-doctor/Cargo.toml b/tools/jet-doctor/Cargo.toml index 5f924ebc3..f07eb84c3 100644 --- a/tools/jet-doctor/Cargo.toml +++ b/tools/jet-doctor/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "jet-doctor" -version = "0.1.0" +version = "0.2.0" authors = ["Devolutions Inc. "] edition = "2021" publish = false @@ -16,11 +16,10 @@ pem = "3.0" shadow-rs = "0.21" openssl-probe = "0.1" -# Same dependency as tokio-tungstenite 0.20 -# https://crates.io/crates/tokio-tungstenite/0.20.1/dependencies -rustls-webpki = "0.101" -rustls = "0.21" -rustls-native-certs = "0.6" +# Same dependency as tokio-tungstenite 0.21.0 +# https://crates.io/crates/tokio-tungstenite/0.21.0/dependencies +rustls = "0.22" +rustls-native-certs = "0.7" [build-dependencies] shadow-rs = "0.21" diff --git a/tools/jet-doctor/src/main.rs b/tools/jet-doctor/src/main.rs index 376eff632..675c3d5ba 100644 --- a/tools/jet-doctor/src/main.rs +++ b/tools/jet-doctor/src/main.rs @@ -40,15 +40,19 @@ macro_rules! diagnostic { macro_rules! output { ( $dst:expr, $($arg:tt)* ) => { - writeln!( $dst, $($arg)* ).context("write output")? + writeln!( $dst, $($arg)* ).context("write output") }; } /// Sanity checks for Devolutions Gateway and Jetsocat. #[derive(OnlyArgs)] struct Args { + #[short('c')] check_cert: Option, + #[short('n')] subject_name: Option, + #[short('p')] + server_port: Option, } fn main() -> ExitCode { @@ -61,12 +65,31 @@ fn main() -> ExitCode { println!("> Build date: {}", build::BUILD_TIME); let mut success = true; + let mut root_store = rustls::RootCertStore::empty(); + let mut server_certificates = Vec::new(); + + success &= diagnostic!(openssl_probe()); + success &= diagnostic!(check_root_store(&mut root_store)); + + if let Some(subject_name) = args.subject_name.as_deref() { + success &= diagnostic!(fetch_chain( + subject_name, + args.server_port, + root_store.clone(), + &mut server_certificates + )); + } - success &= diagnostic!(ssl_probe()); - success &= diagnostic!(check_root_store()); + if let Some(chain_file_path) = &args.check_cert { + success &= diagnostic!(read_chain(&chain_file_path, &mut server_certificates)); + } - if let Some(cert_path) = &args.check_cert { - success &= diagnostic!(check_cert(&cert_path, args.subject_name.as_deref())); + if !server_certificates.is_empty() { + success &= diagnostic!(check_end_entity_cert( + &server_certificates, + args.subject_name.as_deref() + )); + success &= diagnostic!(check_chain(&server_certificates)); } if success { @@ -76,74 +99,180 @@ fn main() -> ExitCode { } } -fn ssl_probe(mut out: impl fmt::Write) -> anyhow::Result<()> { +fn openssl_probe(mut out: impl fmt::Write) -> anyhow::Result<()> { let result = openssl_probe::probe(); - output!(out, "cert_file = {:?}", result.cert_file); - output!(out, "cert_dir = {:?}", result.cert_dir); + output!(out, "cert_file = {:?}", result.cert_file)?; + output!(out, "cert_dir = {:?}", result.cert_dir)?; Ok(()) } -fn check_root_store(mut out: impl fmt::Write) -> anyhow::Result<()> { - let mut root_store = rustls::RootCertStore::empty(); - +fn check_root_store(mut out: impl fmt::Write, root_store: &mut rustls::RootCertStore) -> anyhow::Result<()> { for cert in rustls_native_certs::load_native_certs().context("failed to load native certificates")? { - let cert = rustls::Certificate(cert.0); - - if let Err(e) = root_store.add(&cert) { - output!(out, "Invalid root certificate: {e}"); + let cert_der = cert.to_vec(); + if let Err(e) = root_store.add(cert) { + output!(out, "Invalid root certificate: {e}")?; - let pem = pem::Pem::new("CERTIFICATE", cert.0); - output!(out, "{pem}"); + let pem = pem::Pem::new("CERTIFICATE", cert_der); + output!(out, "{pem}")?; } } Ok(()) } -fn check_cert(mut out: impl fmt::Write, cert_path: &Path, subject_name: Option<&str>) -> anyhow::Result<()> { - output!(out, "Read file at {}", cert_path.display()); +fn fetch_chain( + mut out: impl fmt::Write, + subject_name: &str, + port: Option, + root_store: rustls::RootCertStore, + server_certificates: &mut Vec>, +) -> anyhow::Result<()> { + use std::io::Write as _; + use std::net::TcpStream; + + output!(out, "Connect to {subject_name}…")?; + + let mut socket = + TcpStream::connect((subject_name, port.unwrap_or(443))).context("failed to connect to server...")?; + + let config = rustls::ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let config = std::sync::Arc::new(config); + let subject_name = rustls::pki_types::ServerName::try_from(subject_name.to_owned()).context("invalid DNS name")?; + let mut client = rustls::ClientConnection::new(config, subject_name).context("failed to create TLS client")?; + + output!(out, "Fetch server certificates…")?; + + loop { + if client.wants_read() { + client.read_tls(&mut socket).context("read_tls failed")?; + client.process_new_packets().context("process_new_packets failed")?; + } - let cert_val = std::fs::read(cert_path).context("read file from disk")?; + if client.wants_write() { + client.write_tls(&mut socket).context("write_tls failed")?; + } - let cert_der = match pem::parse(&cert_val) { - Ok(cert_pem) => { - output!(out, "Detected PEM format"); + socket.flush().context("flush failed")?; - let pem_tag = cert_pem.tag(); + if let Some(peer_certificates) = client.peer_certificates() { + for certificate in peer_certificates { + let pem = pem::Pem::new("CERTIFICATE", certificate.to_vec()); + output!(out, "{pem}")?; - if pem_tag != "CERTIFICATE" { - output!(out, "WARNING: unexpected PEM tag: {pem_tag}"); + server_certificates.push(certificate.clone().into_owned()); } - cert_pem.into_contents() + break; } - Err(pem::PemError::MalformedFraming | pem::PemError::NotUtf8(_)) => { - output!(out, "Read as raw DER"); - cert_val + } + + Ok(()) +} + +fn read_chain( + mut out: impl fmt::Write, + chain_file_path: &Path, + server_certificates: &mut Vec>, +) -> anyhow::Result<()> { + output!(out, "Read file at {}", chain_file_path.display())?; + + let file_contents = std::fs::read(chain_file_path).context("read file from disk")?; + + let parsed = match pem::parse_many(&file_contents) { + Ok(pems) => { + output!(out, "Detected PEM format")?; + + pems.into_iter() + .enumerate() + .map(|(idx, pem)| { + let pem_tag = pem.tag(); + + if pem_tag != "CERTIFICATE" { + output!(out, "WARNING: unexpected PEM tag for certificate {idx}: {pem_tag}")?; + } + + anyhow::Ok(pem.into_contents()) + }) + .collect::>()? } - Err(e) => { - return Err(anyhow::Error::new(e).context("read file as PEM")); + Err(pem::PemError::MalformedFraming | pem::PemError::NotUtf8(_)) => { + output!(out, "Read as raw DER…")?; + vec![file_contents] } + Err(e) => return Err(anyhow::Error::new(e).context("read file as PEM")), }; - output!(out, "Decode end entity certificate"); + for certificate in parsed.into_iter() { + let pem = pem::Pem::new("CERTIFICATE", certificate.clone()); + output!(out, "{pem}")?; + + server_certificates.push(rustls::pki_types::CertificateDer::from(certificate)); + } + + Ok(()) +} + +fn check_end_entity_cert( + mut out: impl fmt::Write, + server_certificates: &[rustls::pki_types::CertificateDer<'static>], + subject_name: Option<&str>, +) -> anyhow::Result<()> { + let end_entity_cert = server_certificates.first().cloned().context("empty chain")?; + + output!(out, "Decode end entity certificate")?; let end_entity_cert = - webpki::EndEntityCert::try_from(cert_der.as_slice()).context("decode end entity certificate")?; + rustls::server::ParsedCertificate::try_from(&end_entity_cert).context("parse end entity certificate")?; if let Some(subject_name) = subject_name { - output!(out, "Verify validity for DNS name"); + output!(out, "Verify validity for DNS name")?; - let subject_name = webpki::SubjectNameRef::try_from_ascii_str(subject_name) - .ok() - .context("invalid subject name")?; + let subject_name = rustls::pki_types::ServerName::try_from(subject_name).context("invalid DNS name")?; - end_entity_cert - .verify_is_valid_for_subject_name(subject_name) - .context("verify DNS name")?; + rustls::client::verify_server_name(&end_entity_cert, &subject_name).context("verify DNS name")?; } Ok(()) } + +fn check_chain( + mut out: impl fmt::Write, + server_certificates: &[rustls::pki_types::CertificateDer<'static>], +) -> anyhow::Result<()> { + use rustls::client::verify_server_cert_signed_by_trust_anchor; + + let mut certs = server_certificates.iter().cloned(); + + let end_entity_cert = certs.next().context("empty chain")?; + + output!(out, "Decode end entity certificate")?; + + let end_entity_cert = + rustls::server::ParsedCertificate::try_from(&end_entity_cert).context("parse end entity certificate")?; + + let intermediates: Vec<_> = certs.collect(); + + let mut root_store = rustls::RootCertStore::empty(); + let root_certs = rustls_native_certs::load_native_certs().context("failed to load native certificates")?; + root_store.add_parsable_certificates(root_certs); + + let now = rustls::pki_types::UnixTime::now(); + + let ring_crypto_provider = rustls::crypto::ring::default_provider(); + + verify_server_cert_signed_by_trust_anchor( + &end_entity_cert, + &root_store, + &intermediates, + now, + ring_crypto_provider.signature_verification_algorithms.all, + ) + .context("failed to verify certification chain")?; + + Ok(()) +}