telnetz 0.4.0

telnet like commandline tool for connecting to TCP and TLS sockets
use std::net::{Ipv4Addr, Ipv6Addr};
use std::time::{SystemTime, UNIX_EPOCH};

use base64::{Engine as _, engine::general_purpose::STANDARD};
use rustls::ClientConnection;
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use x509_cert::Certificate;
use x509_cert::der::{Decode, Encode};
use x509_cert::ext::pkix::{SubjectAltName, name::GeneralName};

pub fn inspect(connection: &ClientConnection, verified: bool) -> Value
{
    let certificates: Vec<_> = connection
        .peer_certificates()
        .unwrap_or_default()
        .iter()
        .map(|der| certificate_info(der.as_ref()))
        .collect();
    json!({
        "protocol_version": connection.protocol_version().map(|version| format!("{version:?}")),
        "cipher_suite": connection.negotiated_cipher_suite().map(|suite| format!("{:?}", suite.suite())),
        "verification": if verified { "verified" } else { "skipped" },
        "certificates": certificates,
    })
}

fn certificate_info(der: &[u8]) -> Value
{
    let fingerprint = Sha256::digest(der)
        .iter()
        .map(|byte| format!("{byte:02X}"))
        .collect::<Vec<_>>()
        .join(":");
    let mut info = json!({"sha256_fingerprint": fingerprint, "parse_error": null});
    let certificate = match Certificate::from_der(der)
    {
        Ok(certificate) => certificate,
        Err(err) =>
        {
            info["parse_error"] = json!(err.to_string());
            return info;
        }
    };
    let tbs = certificate.tbs_certificate();
    info["subject"] = json!(tbs.subject().to_string());
    info["issuer"] = json!(tbs.issuer().to_string());
    info["serial_number"] = json!(tbs.serial_number().to_string());
    info["not_before"] = json!(tbs.validity().not_before.to_string());
    info["not_after"] = json!(tbs.validity().not_after.to_string());
    let before = tbs.validity().not_before.to_unix_duration().as_secs();
    let after = tbs.validity().not_after.to_unix_duration().as_secs();
    info["not_before_unix"] = json!(before);
    info["not_after_unix"] = json!(after);
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    info["expired"] = json!(now > after);
    info["not_yet_valid"] = json!(now < before);
    let names = match tbs.get_extension::<SubjectAltName>()
    {
        Ok(Some((_, names))) => names.0.into_iter().map(name_info).collect::<Vec<_>>(),
        Ok(None) => Vec::new(),
        Err(err) =>
        {
            info["parse_error"] = json!(err.to_string());
            Vec::new()
        }
    };
    info["subject_alt_names"] = json!(names);
    info
}

fn name_info(name: GeneralName) -> Value
{
    match name
    {
        GeneralName::DnsName(value) => json!({"type": "DNS", "value": value.as_str()}),
        GeneralName::Rfc822Name(value) => json!({"type": "email", "value": value.as_str()}),
        GeneralName::UniformResourceIdentifier(value) =>
        {
            json!({"type": "URI", "value": value.as_str()})
        }
        GeneralName::DirectoryName(value) =>
        {
            json!({"type": "directory", "value": value.to_string()})
        }
        GeneralName::RegisteredId(value) =>
        {
            json!({"type": "registered_id", "value": value.to_string()})
        }
        GeneralName::IpAddress(value) =>
        {
            let address = if let Ok(v4) = <[u8; 4]>::try_from(value.as_bytes())
            {
                Some(Ipv4Addr::from(v4).to_string())
            }
            else if let Ok(v6) = <[u8; 16]>::try_from(value.as_bytes())
            {
                Some(Ipv6Addr::from(v6).to_string())
            }
            else
            {
                None
            };
            json!({"type": "IP", "value": address, "raw_base64": STANDARD.encode(value.as_bytes())})
        }
        other =>
        {
            json!({"type": "other", "der_base64": other.to_der().ok().map(|der| STANDARD.encode(der))})
        }
    }
}