inspect-cert-chain 0.0.8

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

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

use byteorder::{BigEndian, ByteOrder as _};
use clap::Parser;
use const_oid::{
    db::rfc5912::{ID_EC_PUBLIC_KEY, RSA_ENCRYPTION},
    ObjectIdentifier,
};
use der::Decode as _;
use itertools::Itertools as _;
use x509_cert::Certificate;

mod ext;
mod fetch;
mod util;

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

    #[clap(long, conflicts_with = "host")]
    file: Option<String>,
}

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

fn main() {
    let args = Args::parse();

    let certs = if let Some(host) = &args.host {
        fetch::cert_chain(host)
    } else if let Some(file) = args.file {
        let mut input = if file == "-" {
            let mut buf = String::new();
            let n_bytes = io::stdin().read_to_string(&mut buf).unwrap();
            log::trace!("read {n_bytes} from stdin");
            Box::new(io::Cursor::new(buf)) as Box<dyn io::BufRead>
        } else {
            let file = fs::File::open(file).unwrap();
            Box::new(io::BufReader::new(file)) as Box<dyn io::BufRead>
        };

        rustls_pemfile::certs(&mut input)
            .unwrap()
            .into_iter()
            .map(|der| x509_cert::Certificate::from_der(&der).unwrap())
            .collect()
    } else {
        eprintln!("use --host or --file");
        return;
    };

    for cert in certs.into_iter() {
        print_cert_info(&cert);

        println!();
        println!();
    }
}

fn print_cert_info(cert: &Certificate) {
    println!("Certificate");
    println!("===========");

    let tbs = &cert.tbs_certificate;

    let tbs_cert = &tbs;
    println!("Subject: {}", tbs_cert.subject);

    println!("Issuer: {}", tbs.issuer);

    println!("Version: {:?}", tbs.version);
    println!(
        "Serial Number:\n  {}",
        util::openssl_hex(tbs.serial_number.as_bytes(), 20).join("\n  ")
    );

    println!(
        "Signature Algorithm: {}",
        util::oid_desc_or_raw(&cert.signature_algorithm.oid)
    );
    util::assert_null_params(&cert.signature_algorithm);

    // TODO: doesn't work ?
    println!(
        "Issuer Serial Number:\n  {}",
        tbs.issuer_unique_id
            .as_ref()
            .map(|serial| util::openssl_hex(serial.as_bytes().unwrap(), 20).join("\n  "))
            .unwrap_or_else(|| "<unknown>".to_owned())
    );
    println!("Validity:");
    println!(
        "  Not Before: {} ({})",
        tbs.validity.not_before,
        util::duration_since_now_fmt(tbs.validity.not_before),
    );
    println!(
        "  Not After: {} ({})",
        tbs.validity.not_after,
        util::duration_since_now_fmt(tbs.validity.not_after),
    );

    // if let Some(name_constraints) = anchor.name_constraints {
    //     println!("Name Constraints: {:?}", name_constraints);
    // }

    println!("Subject Public Key Info:");

    let spki = &tbs_cert.subject_public_key_info;
    let alg = &spki.algorithm;

    match () {
        _ if alg.oid == ID_EC_PUBLIC_KEY => {
            let ec_subtype = alg
                .parameters
                .as_ref()
                .unwrap()
                .decode_as::<ObjectIdentifier>()
                .unwrap();

            let ec_type = util::oid_desc_or_raw(&alg.oid);
            let ec_subtype = util::oid_desc_or_raw(&ec_subtype);

            let public_key_bytes = spki.subject_public_key.as_bytes().unwrap();
            let public_key = util::openssl_hex(public_key_bytes, 15).join("\n    ");

            println!("  Algorithm: {ec_type} ({ec_subtype})");
            println!("  Public Key:\n    {public_key}");
        }

        _ if alg.oid == RSA_ENCRYPTION => {
            let algorithm = util::oid_desc_or_raw(&alg.oid);
            println!("  Algorithm: {algorithm}");

            let rsa_details =
                pkcs1::RsaPublicKey::from_der(spki.subject_public_key.as_bytes().unwrap()).unwrap();

            println!("  RSA:");

            let exp_bytes = rsa_details.public_exponent.as_bytes();
            let exp = BigEndian::read_uint(exp_bytes, exp_bytes.len());
            println!("    Exponent: {exp} (0x{exp:0x})");
            let mod_bytes = rsa_details.modulus.as_bytes();
            println!(
                "    Modulus({} bit):\n      {}",
                mod_bytes.len() * 8,
                util::openssl_hex(mod_bytes, 32).join("\n      ")
            );
        }

        _ => {
            let alg = util::oid_desc_or_raw(&alg.oid);
            println!("  Algorithm: {alg}");
        }
    }

    if let Some(extensions) = &tbs.extensions {
        println!("Extensions:");

        for ext in extensions {
            println!(
                "  ID: {}{}",
                util::oid_desc_or_raw(&ext.extn_id),
                if ext.critical { " (critical)" } else { "" }
            );
            println!("  Extension value:\n    {}", ext::interpret_val(ext));
            println!();
        }
    }

    println!("Signature:");
    println!(
        "  {}",
        util::openssl_hex(cert.signature.as_bytes().unwrap(), 20).join("\n  ")
    );
}