inspect-cert-chain 0.0.14

OpenSSL-like text output for debugging certificate chains
#![deny(rust_2018_idioms, future_incompatible)]

use std::{
    fs,
    io::{self, Read},
};

use clap::Parser;
use der::{Decode as _, Encode as _};
use eyre::WrapErr as _;
use pem_rfc7468::{LineEnding, PemLabel as _};
use x509_cert::Certificate;

mod ext;
mod fetch;
mod info;
mod util;

cfg_if::cfg_if! {
    if #[cfg(windows)] {
        const LINE_ENDING: LineEnding = LineEnding::CRLF;
    } else {
        const LINE_ENDING: LineEnding = LineEnding::LF;
    }
}

#[derive(Debug, Parser)]
#[command(author, version, about, long_about = None)]
struct Args {
    /// Download certificate chain from remote host.
    #[clap(long, conflicts_with = "file")]
    host: Option<String>,

    /// When provided, writes downloaded chain to file in PEM format.
    #[clap(long, conflicts_with = "file")]
    dump: Option<camino::Utf8PathBuf>,

    /// Inspect a local certificate chain in PEM format.
    #[clap(long, conflicts_with = "host")]
    file: Option<camino::Utf8PathBuf>,

    #[arg(short, long, action = clap::ArgAction::Count)]
    verbose: u8,
}

// let anchor = &TLS_SERVER_ROOTS.0[3]; // seems to have wrong modulus ?!?

fn main() -> eyre::Result<()> {
    color_eyre::install()?;

    let args = Args::parse();

    if args.verbose == 0 {
        pretty_env_logger::try_init_timed()?;
    } else {
        std::env::set_var("RUST_LOG", "info");
        pretty_env_logger::try_init_timed()?;
    }

    let certs = if let Some(host) = &args.host {
        tracing::info!("fetching certificate chain from remote host: {host}");
        fetch::cert_chain(host)?
    } else if let Some(file) = &args.file {
        let mut input = if file == "-" {
            tracing::info!("reading certificate chain from stdin: {file}");

            let mut buf = String::new();
            let n_bytes = io::stdin().read_to_string(&mut buf).unwrap();
            tracing::trace!("read {n_bytes} from stdin");
            Box::new(io::Cursor::new(buf)) as Box<dyn io::BufRead>
        } else {
            tracing::info!("reading certificate chain from file: {file}");

            let file =
                fs::File::open(file).wrap_err_with(|| format!("could not open file: {file}"))?;
            Box::new(io::BufReader::new(file)) as Box<dyn io::BufRead>
        };

        tracing::debug!("reading certificate chain PEM files");
        let certs = rustls_pemfile::certs(&mut input).collect::<Result<Vec<_>, _>>()?;

        tracing::debug!("parsing certificate chain");
        certs
            .into_iter()
            .map(|der| x509_cert::Certificate::from_der(&der))
            .collect::<Result<_, _>>()?
    } else {
        return Err(eyre::eyre!("use --host or --file"));
    };

    let n_certs = certs.len();
    tracing::info!("chain contains {n_certs} certificates");

    if n_certs == 0 {
        return Err(eyre::eyre!("chain contained 0 certificates"));
    }

    for cert in &certs {
        info::print_cert_info(cert);

        println!();
        println!();
    }

    if let Some(dump_path) = args.dump {
        tracing::info!("writing chain to {dump_path}");

        let pem_chain = certs
            .into_iter()
            .map(|cert| {
                let mut der = Vec::with_capacity(256);
                cert.encode_to_vec(&mut der)
                    .wrap_err("failed to convert certificate back to DER encoding")?;

                pem_rfc7468::encode_string(Certificate::PEM_LABEL, LINE_ENDING, &der)
                    .wrap_err("failed to encode DER certificate to PEM format")
            })
            .collect::<Result<Vec<_>, _>>()?
            .concat();

        fs::write(&dump_path, pem_chain)
            .wrap_err_with(|| format!("failed to dump downloaded cert chain to {dump_path}"))?;
    }

    Ok(())
}