Skip to main content

x509_certificate_printer/
lib.rs

1use std::cmp::min;
2use std::fmt::Write as _;
3use std::net::{Ipv4Addr, Ipv6Addr};
4use base64::{Engine, engine::general_purpose::STANDARD};
5use x509_parser::extensions::GeneralName;
6use x509_parser::objects;
7use x509_parser::prelude::*;
8use x509_parser::public_key::PublicKey;
9use x509_parser::signature_algorithm::SignatureAlgorithm;
10
11// This is all taken from https://github.com/rusticata/x509-parser/blob/master/examples/print-cert.rs
12
13fn format_hex_dump(bytes: &[u8], max_len: usize) -> String {
14    let m = min(bytes.len(), max_len);
15    let hex: String = bytes[..m].iter().map(|b| format!("{:02x}", b)).collect();
16    if bytes.len() > max_len {
17        format!("{}... <continued>", hex)
18    } else {
19        hex
20    }
21}
22
23fn format_oid(oid: &asn1_rs::Oid) -> String {
24    match objects::oid2sn(oid, objects::oid_registry()) {
25        Ok(s) => s.to_owned(),
26        _ => format!("{oid}"),
27    }
28}
29
30fn format_generalname(gn: &GeneralName) -> String {
31    match gn {
32        GeneralName::DNSName(name) => format!("DNSName:{name}"),
33        GeneralName::DirectoryName(n) => format!("DirName:{n}"),
34        GeneralName::EDIPartyName(obj) => format!("EDIPartyName:{obj:?}"),
35        GeneralName::IPAddress(n) => format!("IPAddress:{n:?}"),
36        GeneralName::OtherName(oid, n) => format!("OtherName:{oid}, {n:?}"),
37        GeneralName::RFC822Name(n) => format!("RFC822Name:{n}"),
38        GeneralName::RegisteredID(oid) => format!("RegisteredID:{oid}"),
39        GeneralName::URI(n) => format!("URI:{n}"),
40        GeneralName::X400Address(obj) => format!("X400Address:{obj:?}"),
41        GeneralName::Invalid(_, _) => "Invalid".to_string(),
42    }
43}
44
45fn format_x509_extension(oid: &asn1_rs::Oid, ext: &X509Extension) -> String {
46    let mut output = String::new();
47    writeln!(
48        output,
49        "    [crit:{} l:{}] {}: ",
50        ext.critical,
51        ext.value.len(),
52        format_oid(oid)
53    )
54    .unwrap();
55    match ext.parsed_extension() {
56        ParsedExtension::AuthorityKeyIdentifier(aki) => {
57            output.push_str("      X509v3 Authority Key Identifier\n");
58            if let Some(key_id) = &aki.key_identifier {
59                output.push_str(&format!("        Key Identifier: {key_id:x}\n"));
60            }
61            if let Some(issuer) = &aki.authority_cert_issuer {
62                for name in issuer {
63                    output.push_str(&format!("        Cert Issuer: {name}\n"));
64                }
65            }
66            if let Some(serial) = &aki.authority_cert_serial {
67                output.push_str(&format!("        Cert Serial: {}\n", format_serial(serial)));
68            }
69        }
70        ParsedExtension::BasicConstraints(bc) => {
71            output.push_str(&format!("      X509v3 CA: {}\n", bc.ca));
72        }
73        ParsedExtension::CRLDistributionPoints(points) => {
74            output.push_str("      X509v3 CRL Distribution Points:\n");
75            for point in points.iter() {
76                if let Some(name) = &point.distribution_point {
77                    output.push_str(&format!("        Full Name: {name:?}\n"));
78                }
79                if let Some(reasons) = &point.reasons {
80                    output.push_str(&format!("        Reasons: {reasons}\n"));
81                }
82                if let Some(crl_issuer) = &point.crl_issuer {
83                    output.push_str("        CRL Issuer: ");
84                    for gn in crl_issuer {
85                        output.push_str(&format!("{} ", format_generalname(gn)));
86                    }
87                    output.push('\n');
88                }
89                output.push('\n');
90            }
91        }
92        ParsedExtension::KeyUsage(ku) => {
93            output.push_str(&format!("      X509v3 Key Usage: {ku}\n"));
94        }
95        ParsedExtension::NSCertType(ty) => {
96            output.push_str(&format!("      Netscape Cert Type: {ty}\n"));
97        }
98        ParsedExtension::SubjectAlternativeName(san) => {
99            for name in &san.general_names {
100                let s = match name {
101                    GeneralName::DNSName(s) => format!("DNS:{s}"),
102                    GeneralName::IPAddress(bytes) => {
103                        let ip = match bytes.len() {
104                            4 => {
105                                let ip = Ipv4Addr::new(bytes[0], bytes[1], bytes[2], bytes[3]);
106                                format!("{ip}")
107                            }
108                            16 => {
109                                let octets: [u8; 16] = (*bytes).try_into().unwrap();
110                                let ip = Ipv6Addr::from(octets);
111                                format!("{ip}")
112                            }
113                            l => format!("invalid (len={l})"),
114                        };
115                        format!("IP Address:{ip}")
116                    }
117                    _ => format!("{name:?}"),
118                };
119                output.push_str(&format!("      X509v3 SAN: {s}\n"));
120            }
121        }
122        ParsedExtension::SubjectKeyIdentifier(subject_key_id) => {
123            output.push_str(&format!(
124                "      X509v3 Subject Key Identifier: {}\n",
125                hex::encode(subject_key_id.0)
126            ));
127        }
128        x => output.push_str(&format!("      {x:?}\n")),
129    }
130    output
131}
132
133fn format_x509_digest_algorithm(alg: &AlgorithmIdentifier, level: usize) -> String {
134    let mut output = String::new();
135    let indent = " ".repeat(level);
136    writeln!(output, "{}Oid: {}", indent, format_oid(&alg.algorithm)).unwrap();
137    if let Some(parameter) = &alg.parameters {
138        let s = match parameter.tag() {
139            asn1_rs::Tag::Oid => {
140                let oid = parameter.as_oid().unwrap();
141                format_oid(&oid)
142            }
143            _ => format!("{}", parameter.tag()),
144        };
145        writeln!(output, "{}Parameter: <PRESENT> {}", indent, s).unwrap();
146        let bytes = parameter.as_bytes();
147        output.push_str(&format_hex_dump(bytes, 32));
148    } else {
149        output.push_str(&format!("{}Parameter: <ABSENT>", indent));
150    }
151    output
152}
153
154fn format_serial(b: &[u8]) -> String {
155    b.iter()
156        .map(|b| format!("{:02x}", b))
157        .collect::<Vec<_>>()
158        .join(":")
159}
160
161fn format_number_to_hex_with_colon(b: &[u8], row_size: usize) -> Vec<String> {
162    let mut v = Vec::with_capacity(1 + b.len() / row_size);
163    for r in b.chunks(row_size) {
164        let s = r.iter().fold(String::with_capacity(3 * r.len()), |a, b| {
165            a + &format!("{b:02x}:")
166        });
167        v.push(s)
168    }
169    v
170}
171
172pub trait PrettyPrinter {
173    fn pretty_print(&self) -> String;
174    fn to_pem(&self) -> String;
175}
176
177impl PrettyPrinter for X509Certificate<'_> {
178    fn pretty_print(&self) -> String {
179        format_x509_info(self)
180    }
181
182    fn to_pem(&self) -> String {
183        let encoded = STANDARD.encode(self.as_raw());
184        let lines: Vec<String> = encoded
185            .as_bytes()
186            .chunks(64)
187            .map(|chunk| std::str::from_utf8(chunk).unwrap().to_string())
188            .collect();
189        format!(
190            "-----BEGIN CERTIFICATE-----\n{}\n-----END CERTIFICATE-----",
191            lines.join("\n")
192        )
193    }
194}
195
196fn format_x509_info(x509: &X509Certificate) -> String {
197    let mut output = String::new();
198
199    let version = x509.version();
200    if version.0 < 3 {
201        writeln!(output, "  Version: {}", version).unwrap();
202    } else {
203        writeln!(output, "  Version: INVALID({})", version.0).unwrap();
204    }
205    writeln!(
206        output,
207        "  Serial: {}",
208        x509.tbs_certificate.raw_serial_as_string()
209    )
210    .unwrap();
211    writeln!(output, "  Subject: {}", x509.subject()).unwrap();
212    writeln!(output, "  Issuer: {}", x509.issuer()).unwrap();
213    output.push_str("  Validity:\n");
214    writeln!(output, "    NotBefore: {}", x509.validity().not_before).unwrap();
215    writeln!(output, "    NotAfter:  {}", x509.validity().not_after).unwrap();
216    writeln!(output, "    is_valid:  {}", x509.validity().is_valid()).unwrap();
217    output.push_str("  Subject Public Key Info:\n");
218    output.push_str(&format_x509_ski(x509.public_key()));
219    output.push_str(&format_x509_signature_algorithm(
220        &x509.signature_algorithm,
221        4,
222    ));
223
224    output.push_str("  Signature Value:\n");
225    let sig_bytes: &[u8] = match &x509.signature_value.data {
226        std::borrow::Cow::Borrowed(b) => b,
227        std::borrow::Cow::Owned(v) => v.as_slice(),
228    };
229    for l in format_number_to_hex_with_colon(sig_bytes, 16) {
230        writeln!(output, "      {l}").unwrap();
231    }
232    output.push_str("  Extensions:\n");
233    for ext in x509.extensions() {
234        output.push_str(&format_x509_extension(&ext.oid, ext));
235    }
236    output.push('\n');
237    output
238}
239
240fn format_x509_signature_algorithm(
241    signature_algorithm: &AlgorithmIdentifier,
242    indent: usize,
243) -> String {
244    let mut output = String::new();
245    match SignatureAlgorithm::try_from(signature_algorithm) {
246        Ok(sig_alg) => {
247            output.push_str("  Signature Algorithm: ");
248            match sig_alg {
249                SignatureAlgorithm::DSA => output.push_str("DSA\n"),
250                SignatureAlgorithm::ECDSA => output.push_str("ECDSA\n"),
251                SignatureAlgorithm::ED25519 => output.push_str("ED25519\n"),
252                SignatureAlgorithm::RSA => output.push_str("RSA\n"),
253                SignatureAlgorithm::RSASSA_PSS(params) => {
254                    output.push_str("RSASSA-PSS\n");
255                    let indent_s = " ".repeat(indent + 2);
256                    output.push_str(&format!(
257                        "{}Hash Algorithm: {}\n",
258                        indent_s,
259                        format_oid(params.hash_algorithm_oid()),
260                    ));
261                    output.push_str(&format!("{}Mask Generation Function: ", indent_s));
262                    if let Ok(mask_gen) = params.mask_gen_algorithm() {
263                        output.push_str(&format!(
264                            "{}/{}\n",
265                            format_oid(&mask_gen.mgf),
266                            format_oid(&mask_gen.hash),
267                        ));
268                    } else {
269                        output.push_str("INVALID\n");
270                    }
271                    output.push_str(&format!(
272                        "{}Salt Length: {}\n",
273                        indent_s,
274                        params.salt_length()
275                    ));
276                }
277                SignatureAlgorithm::RSAAES_OAEP(params) => {
278                    output.push_str("RSAAES-OAEP\n");
279                    let indent_s = " ".repeat(indent + 2);
280                    output.push_str(&format!(
281                        "{}Hash Algorithm: {}\n",
282                        indent_s,
283                        format_oid(params.hash_algorithm_oid()),
284                    ));
285                    output.push_str(&format!("{}Mask Generation Function: ", indent_s));
286                    if let Ok(mask_gen) = params.mask_gen_algorithm() {
287                        output.push_str(&format!(
288                            "{}/{}\n",
289                            format_oid(&mask_gen.mgf),
290                            format_oid(&mask_gen.hash),
291                        ));
292                    } else {
293                        output.push_str("INVALID\n");
294                    }
295                    output.push_str(&format!(
296                        "{}pSourceFunc: {}\n",
297                        indent_s,
298                        format_oid(&params.p_source_alg().algorithm),
299                    ));
300                }
301            }
302        }
303        Err(e) => {
304            output.push_str(&format!("Could not parse signature algorithm: {e}\n"));
305            output.push_str("  Signature Algorithm:\n");
306            output.push_str(&format_x509_digest_algorithm(signature_algorithm, indent));
307        }
308    }
309    output
310}
311
312fn format_x509_ski(public_key: &SubjectPublicKeyInfo) -> String {
313    let mut output = String::new();
314    output.push_str("    Public Key Algorithm:\n");
315    output.push_str(&format_x509_digest_algorithm(&public_key.algorithm, 6));
316    output.push('\n');
317    match public_key.parsed() {
318        Ok(PublicKey::RSA(rsa)) => {
319            output.push_str(&format!("    RSA Public Key: ({} bit)\n", rsa.key_size()));
320            for l in format_number_to_hex_with_colon(rsa.modulus, 16) {
321                output.push_str(&format!("        {l}\n"));
322            }
323            if let Ok(e) = rsa.try_exponent() {
324                output.push_str(&format!("    exponent: 0x{e:x} ({e})\n"));
325            } else {
326                output.push_str("    exponent: <INVALID>:\n");
327                output.push_str(&format_hex_dump(rsa.exponent, 32));
328                output.push('\n');
329            }
330        }
331        Ok(PublicKey::EC(ec)) => {
332            output.push_str(&format!("    EC Public Key: ({} bit)\n", ec.key_size()));
333            for l in format_number_to_hex_with_colon(ec.data(), 16) {
334                output.push_str(&format!("        {l}\n"));
335            }
336        }
337        Ok(PublicKey::DSA(y)) => {
338            output.push_str(&format!("    DSA Public Key: ({} bit)\n", 8 * y.len()));
339            for l in format_number_to_hex_with_colon(y, 16) {
340                output.push_str(&format!("        {l}\n"));
341            }
342        }
343        Ok(PublicKey::GostR3410(y)) => {
344            output.push_str(&format!(
345                "    GOST R 34.10-94 Public Key: ({} bit)\n",
346                8 * y.len()
347            ));
348            for l in format_number_to_hex_with_colon(y, 16) {
349                output.push_str(&format!("        {l}\n"));
350            }
351        }
352        Ok(PublicKey::GostR3410_2012(y)) => {
353            output.push_str(&format!(
354                "    GOST R 34.10-2012 Public Key: ({} bit)\n",
355                8 * y.len()
356            ));
357            for l in format_number_to_hex_with_colon(y, 16) {
358                output.push_str(&format!("        {l}\n"));
359            }
360        }
361        Ok(PublicKey::Unknown(b)) => {
362            output.push_str("    Unknown key type\n");
363            output.push_str(&format_hex_dump(b, 256));
364            output.push('\n');
365        }
366        Err(_) => {
367            output.push_str("    INVALID PUBLIC KEY\n");
368        }
369    }
370    output
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use x509_parser::parse_x509_certificate;
377
378    #[test]
379    fn pretty_print_with_invalid_certificate_returns_error() {
380        let invalid_der = vec![0x30, 0x82, 0x01, 0xff];
381        let result = parse_x509_certificate(&invalid_der);
382        assert!(result.is_err());
383    }
384
385    #[test]
386    fn pretty_print_with_valid_certificate_succeeds() {
387        let der_cert = hex::decode("308204263082028ea003020102021100e3c99a0ad2a86c42bd9384e972d09952300d06092a864886f70d01010b05003073311e301c060355040a13156d6b6365727420646576656c6f706d656e7420434131243022060355040b0c1b74696c6c69407a757365202854696c6d616e204261756d616e6e29312b302906035504030c226d6b636572742074696c6c69407a757365202854696c6d616e204261756d616e6e29301e170d3236303330353131333234375a170d3238303630353130333234375a304f31273025060355040a131e6d6b6365727420646576656c6f706d656e7420636572746966696361746531243022060355040b0c1b74696c6c69407a757365202854696c6d616e204261756d616e6e2930820122300d06092a864886f70d01010105000382010f003082010a0282010100a36f6785351ad9fd76436b2ce650be1662849b8e21aa68b2c23f4f3bf38a1eb9abf983a653b0d807cc9d0d1b37d313aa1a90a815e531d1dfb2c790bc547090fb9c35bc77df437421a3a2c1ba18013aedfdb65b038acdb8a26719bd18111219db47f8ca6db4e090dad717722f2c0c59725562a3eae2ce6c3996232562ccda4f95c8b64e2da31526dc7f4bdca753a9023a6494a4b7dda11e278af6b917fe0fa6de568ebb03ab801b8272396e2ce4098b1ee8059c666e654237f4d470b9e53ba1c0fc46df54f9dd469cb415091b99186a10e9f378d5074d3fb4aa73bc561326e142e60eed93396ac0bb5040cc8df5c7f81e8bd0594c6e4d06b050664627c33fa3f90203010001a3593057300e0603551d0f0101ff0404030205a030130603551d25040c300a06082b06010505070301301f0603551d230418301680142e997fa48849b3d77b61d74d24c8b999ada1fa62300f0603551d1104083006820474657374300d06092a864886f70d01010b05000382018100b0e65c60ddbace88d4fbadfdef32165381555e83522036a46af625a28cdf331754c7e08a8603b019d5b09aa3b13629a5cd8c0784787b607d04b4c42e47f8983a35f69d082d8feba9c625de3f7ef793837fc045e3358a24249717ae393da23c516343ed3784edf82684e9f1c6394b0b28a9cf869260296ba893815488d19dae49cfa9557135b6450891601118996d03f3c90ba6409deb4ea7bd07e7288ff8575382f28e0837a13fb0fa860cd7880b80f7253c936955a803d5110f384163d4292c9f1e3c413ae2a3b018ae3daef136c1b9bd5888bf8702717e36f85d7bb7b79306bccd6aa4b0206ff903bd6e6d61eaad2d5c62b9417308176faf8a08bac86a5fc42009a418567d1fd0e26eb187a8c7b4208747b62beb81b0284868335d6ea95b2253c4f6203f4e1c2c8d3f0e0075ba7c5b0a92d961425d8c15f1112dc7df857d77904bbc850883c3404067a872a4bae988d954ba9001ac99b75ef7b917b527528fb1a9b54bd17ecd7537f70086ed070e9a2d8e0d83f19b148d1b941456f7283c9b").unwrap();
388
389        let (_, x509) = parse_x509_certificate(&der_cert).unwrap();
390        
391        let output = x509.pretty_print();
392        assert!(output.contains("Subject:"));
393        assert!(output.contains("Issuer:"));
394        assert!(output.contains("Serial:"));
395    }
396
397    #[test]
398    fn to_pem_with_valid_certificate_succeeds() {
399        let der_cert = hex::decode("308204263082028ea003020102021100e3c99a0ad2a86c42bd9384e972d09952300d06092a864886f70d01010b05003073311e301c060355040a13156d6b6365727420646576656c6f706d656e7420434131243022060355040b0c1b74696c6c69407a757365202854696c6d616e204261756d616e6e29312b302906035504030c226d6b636572742074696c6c69407a757365202854696c6d616e204261756d616e6e29301e170d3236303330353131333234375a170d3238303630353130333234375a304f31273025060355040a131e6d6b6365727420646576656c6f706d656e7420636572746966696361746531243022060355040b0c1b74696c6c69407a757365202854696c6d616e204261756d616e6e2930820122300d06092a864886f70d01010105000382010f003082010a0282010100a36f6785351ad9fd76436b2ce650be1662849b8e21aa68b2c23f4f3bf38a1eb9abf983a653b0d807cc9d0d1b37d313aa1a90a815e531d1dfb2c790bc547090fb9c35bc77df437421a3a2c1ba18013aedfdb65b038acdb8a26719bd18111219db47f8ca6db4e090dad717722f2c0c59725562a3eae2ce6c3996232562ccda4f95c8b64e2da31526dc7f4bdca753a9023a6494a4b7dda11e278af6b917fe0fa6de568ebb03ab801b8272396e2ce4098b1ee8059c666e654237f4d470b9e53ba1c0fc46df54f9dd469cb415091b99186a10e9f378d5074d3fb4aa73bc561326e142e60eed93396ac0bb5040cc8df5c7f81e8bd0594c6e4d06b050664627c33fa3f90203010001a3593057300e0603551d0f0101ff0404030205a030130603551d25040c300a06082b06010505070301301f0603551d230418301680142e997fa48849b3d77b61d74d24c8b999ada1fa62300f0603551d1104083006820474657374300d06092a864886f70d01010b05000382018100b0e65c60ddbace88d4fbadfdef32165381555e83522036a46af625a28cdf331754c7e08a8603b019d5b09aa3b13629a5cd8c0784787b607d04b4c42e47f8983a35f69d082d8feba9c625de3f7ef793837fc045e3358a24249717ae393da23c516343ed3784edf82684e9f1c6394b0b28a9cf869260296ba893815488d19dae49cfa9557135b6450891601118996d03f3c90ba6409deb4ea7bd07e7288ff8575382f28e0837a13fb0fa860cd7880b80f7253c936955a803d5110f384163d4292c9f1e3c413ae2a3b018ae3daef136c1b9bd5888bf8702717e36f85d7bb7b79306bccd6aa4b0206ff903bd6e6d61eaad2d5c62b9417308176faf8a08bac86a5fc42009a418567d1fd0e26eb187a8c7b4208747b62beb81b0284868335d6ea95b2253c4f6203f4e1c2c8d3f0e0075ba7c5b0a92d961425d8c15f1112dc7df857d77904bbc850883c3404067a872a4bae988d954ba9001ac99b75ef7b917b527528fb1a9b54bd17ecd7537f70086ed070e9a2d8e0d83f19b148d1b941456f7283c9b").unwrap();
400
401        let (_, x509) = parse_x509_certificate(&der_cert).unwrap();
402        let pem = x509.to_pem();
403
404        assert!(pem.contains("-----BEGIN CERTIFICATE-----"));
405        assert!(pem.contains("-----END CERTIFICATE-----"));
406    }
407}