terrazzo-terminal 0.2.8

A simple web-based terminal emulator built on Terrazzo.
use base64::Engine as _;
use base64::prelude::BASE64_STANDARD;
use openssl::nid::Nid;
use openssl::x509::X509;
use openssl::x509::X509Ref;
use trz_gateway_common::security_configuration::common::parse_pem_certificates;
use x509_parser::prelude::FromDer as _;
use x509_parser::prelude::X509Certificate;

use super::AddConversionFn;
use crate::converter::api::Language;

pub fn add_x509_pem(input: &str, add: &mut impl AddConversionFn) -> bool {
    if !input.contains("-----BEGIN CERTIFICATE-----") {
        return false;
    }
    let input = input
        .split('\n')
        .map(|line| line.trim())
        .collect::<Vec<_>>()
        .join("\n");
    let certificates = parse_pem_certificates(&input)
        .filter_map(|x509| x509.ok())
        .collect::<Vec<_>>();
    let mut result = false;
    for x509 in certificates {
        let Ok(Ok(mut text)) = x509.to_text().map(String::from_utf8) else {
            continue;
        };
        add_extensions(&x509, &mut text);
        add(Language::new(get_certificate_name(&x509)), text);
        result = true;
    }
    result
}

pub fn add_x509_base64(input: &[u8], add: &mut impl AddConversionFn) -> bool {
    let Ok(x509) = X509::from_der(input) else {
        return false;
    };
    let Ok(Ok(mut text)) = x509.to_text().map(String::from_utf8) else {
        return false;
    };
    add_extensions(&x509, &mut text);
    add(Language::new(get_certificate_name(&x509)), text);
    return true;
}

fn get_certificate_name(x509: &X509Ref) -> String {
    get_certificate_common_name(x509).unwrap_or_else(|| format!("{:?}", x509.subject_name()))
}

fn get_certificate_common_name(x509: &X509Ref) -> Option<String> {
    Some(
        x509.subject_name()
            .entries_by_nid(Nid::COMMONNAME)
            .next()?
            .data()
            .as_utf8()
            .ok()?
            .to_string(),
    )
}

fn add_extensions(x509: &X509Ref, text: &mut String) -> Option<()> {
    let der = x509.to_der().ok()?;
    let (_, certificate) = X509Certificate::from_der(&der).ok()?;
    let mut extensions = vec![];
    for extension in certificate.extensions() {
        let is_ascii = extension.value.iter().all(|c| c.is_ascii_graphic());
        extensions.push(Extension {
            oid: extension.oid.to_id_string(),
            critical: extension.critical,
            value: is_ascii
                .then(|| str::from_utf8(extension.value).ok().map(str::to_owned))
                .flatten()
                .unwrap_or_else(|| {
                    BASE64_STANDARD
                        .encode(extension.value)
                        .as_bytes()
                        .chunks(64)
                        .map(|chunk| std::str::from_utf8(chunk).unwrap())
                        .collect::<Vec<_>>()
                        .join("\n")
                }),
        });
    }
    if !extensions.is_empty() {
        text.extend(serde_yaml_ng::to_string(&Extensions { extensions }));
    }
    Some(())
}

#[derive(serde::Serialize)]
struct Extensions {
    extensions: Vec<Extension>,
}

#[derive(serde::Serialize)]
struct Extension {
    oid: String,
    critical: bool,
    value: String,
}

#[cfg(test)]
mod tests {
    use super::super::tests::GetConversionForTest as _;

    #[tokio::test]
    async fn single_x509_pem() {
        const CERTIFICATE: &str = r#"
-----BEGIN CERTIFICATE-----
    MIIBtDCCAVmgAwIBAgIVANSN+BUl1Kf8XjE8anSpXGs1HfaWMAoGCCqGSM49BAMC
 MDcxETAPBgNVBAoMCFRlcnJhenpvMSIwIAYDVQQDDBlUZXJyYXp6byBUZXJtaW5h
bCBSb290IENBMB4XDTI1MDYwNjEwMDEyN1oXDTQ1MDYwMTEwMDEyN1owNzERMA8G
   A1UECgwIVGVycmF6em8xIjAgBgNVBAMMGVRlcnJhenpvIFRlcm1pbmFsIFJvb3Qg
Q0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATGiH+iC1+6+3YxaWLEW8V1RsHQ
+fToNIBWRRJEV3q9z5YwZWHLZj8RfWCPsc01rKja1lnhfwEGd5qd9UUQk36go0Iw
QDAdBgNVHQ4EFgQUEC5YRL04bEDiZ9oic1PZc7bR9P4wDwYDVR0TAQH/BAUwAwEB
/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSQAwRgIhAJuRb4MWDitsOJqy
VOj7ugn3k0TlZV3rPSRmuL20bjeeAiEAhVOBRet9JDnQbjG/0SG8QVdJplLL66By
RD66UosBh50=
-----END CERTIFICATE-----
"#;
        let conversion = CERTIFICATE
            .get_conversion("Terrazzo Terminal Root CA")
            .await
            .to_ascii_lowercase();
        assert!(conversion.contains(&"Issuer".to_ascii_lowercase()));
        assert!(conversion.contains(&"Subject".to_ascii_lowercase()));
        assert!(conversion.contains(&"Not Before".to_ascii_lowercase()));
        assert!(conversion.contains(&"Not After".to_ascii_lowercase()));

        let conversion = CERTIFICATE
            .replace("\n", "\n\t")
            .as_str()
            .get_conversion("Terrazzo Terminal Root CA")
            .await
            .to_ascii_lowercase();
        assert!(conversion.contains(&"Issuer".to_ascii_lowercase()));

        assert_eq!(
            vec!["Terrazzo Terminal Root CA"],
            CERTIFICATE.get_languages().await
        );
    }

    #[tokio::test]
    async fn single_x509_der() {
        const CERTIFICATE: &str = r#"
    MIIBtDCCAVmgAwIBAgIVANSN+BUl1Kf8XjE8anSpXGs1HfaWMAoGCCqGSM49BAMC
 MDcxETAPBgNVBAoMCFRlcnJhenpvMSIwIAYDVQQDDBlUZXJyYXp6byBUZXJtaW5h
bCBSb290IENBMB4XDTI1MDYwNjEwMDEyN1oXDTQ1MDYwMTEwMDEyN1owNzERMA8G
   A1UECgwIVGVycmF6em8xIjAgBgNVBAMMGVRlcnJhenpvIFRlcm1pbmFsIFJvb3Qg
Q0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATGiH+iC1+6+3YxaWLEW8V1RsHQ
+fToNIBWRRJEV3q9z5YwZWHLZj8RfWCPsc01rKja1lnhfwEGd5qd9UUQk36go0Iw
QDAdBgNVHQ4EFgQUEC5YRL04bEDiZ9oic1PZc7bR9P4wDwYDVR0TAQH/BAUwAwEB
/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwIDSQAwRgIhAJuRb4MWDitsOJqy
VOj7ugn3k0TlZV3rPSRmuL20bjeeAiEAhVOBRet9JDnQbjG/0SG8QVdJplLL66By
RD66UosBh50=
"#;
        let conversion = CERTIFICATE
            .get_conversion("Terrazzo Terminal Root CA")
            .await;
        assert!(conversion.contains("Terrazzo Terminal Root CA"));
        assert_eq!(
            vec!["ASN.1", "Terrazzo Terminal Root CA"],
            CERTIFICATE.get_languages().await
        );
    }

    #[tokio::test]
    async fn x509_chain() {
        const CERTIFICATE: &str = r#"
-----BEGIN CERTIFICATE-----
MIIIvTCCCGSgAwIBAgIUKA0KjYMYx9iWca5bQRJP/uya/T4wCgYIKoZIzj0EAwIw
NzERMA8GA1UECgwIVGVycmF6em8xIjAgBgNVBAMMGVRlcnJhenpvIFRlcm1pbmFs
IFJvb3QgQ0EwHhcNMjUwNzAzMTc1NzM1WhcNMjUxMDAxMTc1NzM1WjARMQ8wDQYD
VQQDDAZRd2VydHkwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATdtKVt+VVwOTuu
hswmVT2PVSqZrB3NVs/tVa09QGlbW5htyGupxfkoERuXWwiXoPZ9ILE/Lg/ae5q4
UhTtHPyho4IHcjCCB24wHQYDVR0OBBYEFHkQhATtZpUOaS6HqIxoEzWsKTPVMAwG
A1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMCAGA1UdJQEB/wQWMBQGCCsGAQUF
BwMBBggrBgEFBQcDAjBzBgNVHSMEbDBqgBQQLlhEvThsQOJn2iJzU9lzttH0/qE7
pDkwNzERMA8GA1UECgwIVGVycmF6em8xIjAgBgNVBAMMGVRlcnJhenpvIFRlcm1p
bmFsIFJvb3QgQ0GCFQDUjfgVJdSn/F4xPGp0qVxrNR32ljARBgNVHREECjAIggZR
d2VydHkwggaDBgorBgEEAYI3CmMBBIIGczCCBm8GCSqGSIb3DQEHAqCCBmAwggZc
AgEDMQ0wCwYJYIZIAWUDBAIBMEwGCSqGSIb3DQEHAaA/BD1Rd2VydHk6MTc1MTU2
NTQ1NToxNzU5MzQxNDU1OpEVFK4rDwTrpkGRY8wb957kiiXUhE+zGbpxz7jaIOXS
oIIEjTCCAjAwggHXoAMCAQICFHPrBUZV0qb/WeUJoHdunbTwjNm3MAoGCCqGSM49
BAMCMDcxETAPBgNVBAoMCFRlcnJhenpvMSIwIAYDVQQDDBlUZXJyYXp6byBUZXJt
aW5hbCBSb290IENBMB4XDTI1MDYwNjEwMDEyN1oXDTQ1MDYwMTEwMDEyN1owPzER
MA8GA1UECgwIVGVycmF6em8xKjAoBgNVBAMMIVRlcnJhenpvIFRlcm1pbmFsIElu
dGVybWVkaWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABAfgjXnTrtjz
zr4/003q4GIlxKdiGlPsj9ZKNnu9SrM2ro3+lzz9XCCmXCNKQSBZyanVL6YjS+UX
K/r+xxG8efOjgbgwgbUwHQYDVR0OBBYEFDe9wI0um5s0TumS31Uh++z0LcYdMA8G
A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMHMGA1UdIwRsMGqAFBAuWES9
OGxA4mfaInNT2XO20fT+oTukOTA3MREwDwYDVQQKDAhUZXJyYXp6bzEiMCAGA1UE
AwwZVGVycmF6em8gVGVybWluYWwgUm9vdCBDQYIVANSN+BUl1Kf8XjE8anSpXGs1
HfaWMAoGCCqGSM49BAMCA0cAMEQCIDtLGiNSIQQOSVrsByTfMYOmKJxC2kVpLPJj
i6d9c1XMAiAenWr5SBEA+UlCXZHcnkQpBHEx94nsn0gfct0VPMeVqzCCAlUwggH8
oAMCAQICFQDY4zejNs9G+5ZkGxS4jtwN/j73MzAKBggqhkjOPQQDAjA/MREwDwYD
VQQKDAhUZXJyYXp6bzEqMCgGA1UEAwwhVGVycmF6em8gVGVybWluYWwgSW50ZXJt
ZWRpYXRlIENBMB4XDTI1MDYwNjEwMDEyN1oXDTQ1MDYwMTEwMDEyN1owJzERMA8G
A1UECgwIVGVycmF6em8xEjAQBgNVBAMMCWxvY2FsaG9zdDBZMBMGByqGSM49AgEG
CCqGSM49AwEHA0IABFENJlIiAJzTUiMKCCU36uRE9vbxnnjDoikW4ldg+S3crfiW
GQOrnWmkXnmbPpHhgvvfpaJGdqMdOa43QGU7/4mjgewwgekwHQYDVR0OBBYEFPjE
1LH/QqDMCKe1uvrTXyTJ00h5MAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeA
MCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjByBgNVHSMEazBpgBQ3
vcCNLpubNE7pkt9VIfvs9C3GHaE7pDkwNzERMA8GA1UECgwIVGVycmF6em8xIjAg
BgNVBAMMGVRlcnJhenpvIFRlcm1pbmFsIFJvb3QgQ0GCFHPrBUZV0qb/WeUJoHdu
nbTwjNm3MBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAKBggqhkjOPQQDAgNHADBEAiBY
HxIi7bJWfEaZ/7KOIEp9Qg2QXXbsVu6kSfF2alyM2wIgFJWPvI5vzLL42wPH4XZd
BQhcddRsAYEiHdSf5wPnZKIxggFnMIIBYwIBA4AU+MTUsf9CoMwIp7W6+tNfJMnT
SHkwCwYJYIZIAWUDBAIBoIHkMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJ
KoZIhvcNAQkFMQ8XDTI1MDcwMzE3NTczNVowLwYJKoZIhvcNAQkEMSIEICAGOlWd
od5k4+LevYrmrJXnrL/vecL8NPkJP1hWt755MHkGCSqGSIb3DQEJDzFsMGowCwYJ
YIZIAWUDBAEqMAsGCWCGSAFlAwQBFjALBglghkgBZQMEAQIwCgYIKoZIhvcNAwcw
DgYIKoZIhvcNAwICAgCAMA0GCCqGSIb3DQMCAgFAMAcGBSsOAwIHMA0GCCqGSIb3
DQMCAgEoMAoGCCqGSM49BAMCBEgwRgIhAJRZdiJVQ7g93r8yuyTvb8XR5ETMM3bb
ltQlIbpLHpcbAiEAxiG+q9FAadM7OihsEv9XNXJtsQfvbtgvtj4uPZl64NYwCgYI
KoZIzj0EAwIDRwAwRAIgK2gPIIZ9vlvcri6dW+96WM5u5wKa1CADK02ZxpyThFkC
IH0+EtbcGRS0S+pPg4PlTbttE8pP61nGTg/U3Lf17WZ+
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
MIIEVzCCAj+gAwIBAgIRALBXPpFzlydw27SHyzpFKzgwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMjQwMzEzMDAwMDAw
WhcNMjcwMzEyMjM1OTU5WjAyMQswCQYDVQQGEwJVUzEWMBQGA1UEChMNTGV0J3Mg
RW5jcnlwdDELMAkGA1UEAxMCRTYwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATZ8Z5G
h/ghcWCoJuuj+rnq2h25EqfUJtlRFLFhfHWWvyILOR/VvtEKRqotPEoJhC6+QJVV
6RlAN2Z17TJOdwRJ+HB7wxjnzvdxEP6sdNgA1O1tHHMWMxCcOrLqbGL0vbijgfgw
gfUwDgYDVR0PAQH/BAQDAgGGMB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcD
ATASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSTJ0aYA6lRaI6Y1sRCSNsj
v1iU0jAfBgNVHSMEGDAWgBR5tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcB
AQQmMCQwIgYIKwYBBQUHMAKGFmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0g
BAwwCjAIBgZngQwBAgEwJwYDVR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVu
Y3Iub3JnLzANBgkqhkiG9w0BAQsFAAOCAgEAfYt7SiA1sgWGCIpunk46r4AExIRc
MxkKgUhNlrrv1B21hOaXN/5miE+LOTbrcmU/M9yvC6MVY730GNFoL8IhJ8j8vrOL
pMY22OP6baS1k9YMrtDTlwJHoGby04ThTUeBDksS9RiuHvicZqBedQdIF65pZuhp
eDcGBcLiYasQr/EO5gxxtLyTmgsHSOVSBcFOn9lgv7LECPq9i7mfH3mpxgrRKSxH
pOoZ0KXMcB+hHuvlklHntvcI0mMMQ0mhYj6qtMFStkF1RpCG3IPdIwpVCQqu8GV7
s8ubknRzs+3C/Bm19RFOoiPpDkwvyNfvmQ14XkyqqKK5oZ8zhD32kFRQkxa8uZSu
h4aTImFxknu39waBxIRXE4jKxlAmQc4QjFZoq1KmQqQg0J/1JF8RlFvJas1VcjLv
YlvUB2t6npO6oQjB3l+PNf0DpQH7iUx3Wz5AjQCi6L25FjyE06q6BZ/QlmtYdl/8
ZYao4SRqPEs/6cAiF+Qf5zg2UkaWtDphl1LKMuTNLotvsX99HP69V2faNyegodQ0
LyTApr/vT01YPE46vNsDLgK+4cL6TrzC/a4WcmF5SRJ938zrv/duJHLXQIku5v0+
EwOy59Hdm0PT/Er/84dDV0CSjdR/2XuZM3kpysSKLgD1cKiDA+IRguODCxfO9cyY
Ig46v9mFmBvyH04=
-----END CERTIFICATE-----
"#;
        assert_eq!(vec!["E6", "Qwerty"], CERTIFICATE.get_languages().await);
        let conversion = CERTIFICATE
            .get_conversion("Qwerty")
            .await
            .to_ascii_lowercase();
        assert!(conversion.contains(&"Issuer".to_ascii_lowercase()));
        let conversion = CERTIFICATE.get_conversion("E6").await.to_ascii_lowercase();
        assert!(conversion.contains(&"Issuer".to_ascii_lowercase()));
    }
}