Skip to content

Commit

Permalink
chore(tools): update jet-doctor
Browse files Browse the repository at this point in the history
  • Loading branch information
CBenoit committed Sep 27, 2024
1 parent 6506b08 commit 5ef5da8
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 48 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build-tools.yml
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.tool }}-${{ matrix.arch }}-${{ matrix.platform }}
path: ${{ matrix.tool }}.dmg
path: ${{ matrix.tool }}.dmg
11 changes: 5 additions & 6 deletions tools/jet-doctor/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jet-doctor"
version = "0.1.0"
version = "0.2.0"
authors = ["Devolutions Inc. <[email protected]>"]
edition = "2021"
publish = false
Expand All @@ -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"
211 changes: 170 additions & 41 deletions tools/jet-doctor/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<PathBuf>,
#[short('n')]
subject_name: Option<String>,
#[short('p')]
server_port: Option<u16>,
}

fn main() -> ExitCode {
Expand All @@ -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 {
Expand All @@ -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<u16>,
root_store: rustls::RootCertStore,
server_certificates: &mut Vec<rustls::pki_types::CertificateDer<'static>>,
) -> 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<rustls::pki_types::CertificateDer<'static>>,
) -> 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::<anyhow::Result<_>>()?
}
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(())
}

0 comments on commit 5ef5da8

Please sign in to comment.