diff --git a/x509/Cargo.toml b/x509/Cargo.toml index 3f35e6927..6c8611766 100644 --- a/x509/Cargo.toml +++ b/x509/Cargo.toml @@ -25,7 +25,9 @@ hex-literal = "0.3" rstest = "0.12.0" [features] +alloc = ["der/alloc"] std = ["der/std", "spki/std"] +pem = ["alloc", "der/pem"] [package.metadata.docs.rs] all-features = true diff --git a/x509/src/certificate.rs b/x509/src/certificate.rs index f93ab6faf..755a79895 100644 --- a/x509/src/certificate.rs +++ b/x509/src/certificate.rs @@ -1,3 +1,5 @@ +//! Certificate types + use crate::{name::Name, time::Validity}; use alloc::vec::Vec; @@ -7,6 +9,8 @@ use der::asn1::{BitString, UIntBytes}; use der::{Decodable, Enumerated, Error, ErrorKind, Newtype, Sequence}; use spki::{AlgorithmIdentifier, SubjectPublicKeyInfo}; +pub mod document; + /// Certificate `Version` as defined in [RFC 5280 Section 4.1]. /// /// ```text diff --git a/x509/src/certificate/document.rs b/x509/src/certificate/document.rs new file mode 100644 index 000000000..f8aabd445 --- /dev/null +++ b/x509/src/certificate/document.rs @@ -0,0 +1,97 @@ +//! CertificateDocument implementation + +use crate::Certificate; +use der::{Error, Result}; + +use alloc::vec::Vec; +use core::fmt; +use der::{Decodable, Document}; + +#[cfg(feature = "pem")] +use {core::str::FromStr, der::pem}; + +/// Certificate document. +/// +/// This type provides storage for [`Certificate`] encoded as ASN.1 +/// DER with the invariant that the contained-document is "well-formed", i.e. +/// it will parse successfully according to this crate's parsing rules. +#[derive(Clone)] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +pub struct CertificateDocument(Vec); + +impl<'a> TryFrom<&'a [u8]> for Certificate<'a> { + type Error = Error; + + fn try_from(bytes: &'a [u8]) -> Result { + Self::from_der(bytes) + } +} + +impl<'a> Document<'a> for CertificateDocument { + type Message = Certificate<'a>; + const SENSITIVE: bool = false; +} + +impl AsRef<[u8]> for CertificateDocument { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl TryFrom<&[u8]> for CertificateDocument { + type Error = Error; + + fn try_from(bytes: &[u8]) -> Result { + Self::from_der(bytes) + } +} + +impl TryFrom> for CertificateDocument { + type Error = Error; + + fn try_from(cert: Certificate<'_>) -> Result { + Self::try_from(&cert) + } +} + +impl TryFrom<&Certificate<'_>> for CertificateDocument { + type Error = Error; + + fn try_from(cert: &Certificate<'_>) -> Result { + Self::from_msg(cert) + } +} + +impl TryFrom> for CertificateDocument { + type Error = Error; + + fn try_from(bytes: Vec) -> Result { + // Ensure document is well-formed + Certificate::from_der(bytes.as_slice())?; + Ok(Self(bytes)) + } +} + +impl fmt::Debug for CertificateDocument { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_tuple("CertificateDocument") + .field(&self.decode()) + .finish() + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl FromStr for CertificateDocument { + type Err = Error; + + fn from_str(s: &str) -> Result { + Self::from_pem(s) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl pem::PemLabel for CertificateDocument { + const TYPE_LABEL: &'static str = "CERTIFICATE"; +} diff --git a/x509/src/lib.rs b/x509/src/lib.rs index 645b656a1..7b3031564 100644 --- a/x509/src/lib.rs +++ b/x509/src/lib.rs @@ -23,12 +23,11 @@ pub use der; pub mod anchor; pub mod attr; +pub mod certificate; pub mod crl; pub mod ext; pub mod name; pub mod request; pub mod time; -mod certificate; - pub use certificate::{Certificate, PkiPath, TbsCertificate, Version}; diff --git a/x509/tests/document.rs b/x509/tests/document.rs new file mode 100644 index 000000000..c50d657b5 --- /dev/null +++ b/x509/tests/document.rs @@ -0,0 +1,131 @@ +//! Certificate document tests +use der::Document; +use x509_cert::certificate::document::CertificateDocument; + +#[cfg(all(feature = "pem", any(feature = "alloc", feature = "std")))] +use der::Encodable; + +use x509_cert::*; + +#[cfg(feature = "std")] +use std::path::Path; + +#[cfg(feature = "pem")] +use der::pem::LineEnding; + +/// `Certificate` encoded as ASN.1 DER +const CERT_DER_EXAMPLE: &[u8] = include_bytes!("examples/amazon.der"); + +/// `Certificate` encoded as PEM +#[cfg(all(feature = "pem"))] +const CERT_PEM_EXAMPLE: &str = include_str!("examples/amazon.pem"); + +#[test] +#[cfg(all(feature = "pem", feature = "std"))] +fn decode_cert_pem_file() { + let doc: CertificateDocument = + CertificateDocument::read_pem_file(Path::new("tests/examples/amazon.pem")).unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); +} + +#[test] +#[cfg(all(feature = "std", feature = "alloc"))] +fn decode_cert_der_file() { + use x509_cert::certificate::document::CertificateDocument; + let doc: CertificateDocument = + CertificateDocument::read_der_file(Path::new("tests/examples/amazon.der")).unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); +} + +#[test] +#[cfg(all(feature = "pem", any(feature = "alloc", feature = "std")))] +fn decode_cert_pem() { + let doc: CertificateDocument = CERT_PEM_EXAMPLE.parse().unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); + + // Ensure `CertificateDocument` parses successfully + let cert = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); + assert_eq!(doc.decode(), cert); + assert_eq!(doc.to_pem(LineEnding::default()).unwrap(), CERT_PEM_EXAMPLE); + + let doc: CertificateDocument = CertificateDocument::from_pem(CERT_PEM_EXAMPLE).unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); + + // Ensure `CertificateDocument` parses successfully + let cert = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); + assert_eq!(doc.decode(), cert); + assert_eq!(doc.to_pem(LineEnding::default()).unwrap(), CERT_PEM_EXAMPLE); +} + +#[test] +fn decode_cert_der() { + let doc: CertificateDocument = CertificateDocument::from_der(CERT_DER_EXAMPLE).unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); + + // Ensure `CertificateDocument` parses successfully + let cert = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); + assert_eq!(doc.decode(), cert); +} + +#[test] +#[cfg(all(feature = "pem", any(feature = "alloc", feature = "std")))] +fn encode_cert_der() { + let pk = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); + let pk_encoded = pk.to_vec().unwrap(); + assert_eq!(CERT_DER_EXAMPLE, pk_encoded.as_slice()); +} + +#[test] +#[cfg(feature = "std")] +fn write_cert_der() { + let doc: CertificateDocument = CertificateDocument::from_der(CERT_DER_EXAMPLE).unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); + assert_eq!(doc.to_der().as_ref(), CERT_DER_EXAMPLE); + + let r = doc.write_der_file(Path::new("tests/examples/amazon.der.regen")); + if r.is_err() { + panic!("Failed to write file") + } + + let doc: CertificateDocument = + CertificateDocument::read_der_file(Path::new("tests/examples/amazon.der.regen")).unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); + assert_eq!(doc.to_der().as_ref(), CERT_DER_EXAMPLE); + let r = std::fs::remove_file("tests/examples/amazon.der.regen"); + if r.is_err() {} +} + +#[test] +#[cfg(all(feature = "pem", any(feature = "alloc", feature = "std")))] +fn encode_cert_pem() { + let pk = Certificate::try_from(CERT_DER_EXAMPLE).unwrap(); + let pk_encoded = CertificateDocument::try_from(pk) + .unwrap() + .to_pem(Default::default()) + .unwrap(); + + assert_eq!(CERT_PEM_EXAMPLE, pk_encoded); +} + +#[test] +#[cfg(all(feature = "std", feature = "pem"))] +fn write_cert_pem() { + let doc: CertificateDocument = CertificateDocument::from_der(CERT_DER_EXAMPLE).unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); + assert_eq!(doc.to_der().as_ref(), CERT_DER_EXAMPLE); + + let r = doc.write_pem_file( + Path::new("tests/examples/amazon.pem.regen"), + LineEnding::default(), + ); + if r.is_err() { + panic!("Failed to write file") + } + + let doc: CertificateDocument = + CertificateDocument::read_pem_file(Path::new("tests/examples/amazon.pem.regen")).unwrap(); + assert_eq!(doc.as_ref(), CERT_DER_EXAMPLE); + assert_eq!(doc.to_der().as_ref(), CERT_DER_EXAMPLE); + let r = std::fs::remove_file("tests/examples/amazon.pem.regen"); + if r.is_err() {} +} diff --git a/x509/tests/examples/amazon.der b/x509/tests/examples/amazon.der new file mode 100644 index 000000000..cc30bd69b Binary files /dev/null and b/x509/tests/examples/amazon.der differ diff --git a/x509/tests/examples/amazon.pem b/x509/tests/examples/amazon.pem new file mode 100644 index 000000000..8b41dea1f --- /dev/null +++ b/x509/tests/examples/amazon.pem @@ -0,0 +1,49 @@ +-----BEGIN CERTIFICATE----- +MIIIwTCCB6mgAwIBAgIQDkI5q4Xi5qJ8Usbem5B42TANBgkqhkiG9w0BAQsFADBE +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMR4wHAYDVQQDExVE +aWdpQ2VydCBHbG9iYWwgQ0EgRzIwHhcNMjExMDA2MDAwMDAwWhcNMjIwOTE5MjM1 +OTU5WjAYMRYwFAYDVQQDDA0qLnBlZy5hMnouY29tMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAv+/uM8Nke+pDU6lWoZALXfrNwH6/B3+8FEfNewD6mN8u +ntzCMH8fPU5Gb1/odQWS7GiBPowoU6smFj4F0kD3Qh4OUpAVbcYS2ad5nVBnwmh8 +Tm/U3DO34FZgxtjz3qxBVKr1ryYO3+1/H2xBuit8W1TSd/s+2joupzAAQq0zn8B3 +6kpi14dYIaZgBw0WuQHaybTlC0cko9x2t43RXxNZgRxqYyh+I+MK19ZOAZgCPAoo +JXyQiUIohTdOw8gnDY5gI4zhHyzvuhifSBeII1WA6MfGdiKV+K0R9LLr0oHYV3F7 +yCQoDN29ZVgXj4UZu64IxIQnuHRN6X2otR3qgUjAoQIDAQABo4IF2TCCBdUwHwYD +VR0jBBgwFoAUJG4rLdBqklFRJWkBqppHponnQCAwHQYDVR0OBBYEFJVFFD46QB6V +FvCCrEVzglhtm/B0MIICowYDVR0RBIICmjCCApaCDGFtYXpvbi5jby51a4ITdWVk +YXRhLmFtYXpvbi5jby51a4IQd3d3LmFtYXpvbi5jby51a4IXb3JpZ2luLXd3dy5h +bWF6b24uY28udWuCDSoucGVnLmEyei5jb22CCmFtYXpvbi5jb22CCGFtem4uY29t +ghF1ZWRhdGEuYW1hem9uLmNvbYINdXMuYW1hem9uLmNvbYIOd3d3LmFtYXpvbi5j +b22CDHd3dy5hbXpuLmNvbYIUY29ycG9yYXRlLmFtYXpvbi5jb22CEWJ1eWJveC5h +bWF6b24uY29tghFpcGhvbmUuYW1hem9uLmNvbYINeXAuYW1hem9uLmNvbYIPaG9t +ZS5hbWF6b24uY29tghVvcmlnaW4td3d3LmFtYXpvbi5jb22CFm9yaWdpbjItd3d3 +LmFtYXpvbi5jb22CIWJ1Y2tleWUtcmV0YWlsLXdlYnNpdGUuYW1hem9uLmNvbYIS +aHVkZGxlcy5hbWF6b24uY29tgglhbWF6b24uZGWCDXd3dy5hbWF6b24uZGWCFG9y +aWdpbi13d3cuYW1hem9uLmRlggxhbWF6b24uY28uanCCCWFtYXpvbi5qcIINd3d3 +LmFtYXpvbi5qcIIQd3d3LmFtYXpvbi5jby5qcIIXb3JpZ2luLXd3dy5hbWF6b24u +Y28uanCCECouYWEucGVnLmEyei5jb22CECouYWIucGVnLmEyei5jb22CECouYWMu +cGVnLmEyei5jb22CGG9yaWdpbi13d3cuYW1hem9uLmNvbS5hdYIRd3d3LmFtYXpv +bi5jb20uYXWCECouYnoucGVnLmEyei5jb22CDWFtYXpvbi5jb20uYXWCGG9yaWdp +bjItd3d3LmFtYXpvbi5jby5qcDAOBgNVHQ8BAf8EBAMCBaAwHQYDVR0lBBYwFAYI +KwYBBQUHAwEGCCsGAQUFBwMCMHcGA1UdHwRwMG4wNaAzoDGGL2h0dHA6Ly9jcmwz +LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbENBRzIuY3JsMDWgM6Axhi9odHRw +Oi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxDQUcyLmNybDA+BgNV +HSAENzA1MDMGBmeBDAECATApMCcGCCsGAQUFBwIBFhtodHRwOi8vd3d3LmRpZ2lj +ZXJ0LmNvbS9DUFMwdAYIKwYBBQUHAQEEaDBmMCQGCCsGAQUFBzABhhhodHRwOi8v +b2NzcC5kaWdpY2VydC5jb20wPgYIKwYBBQUHMAKGMmh0dHA6Ly9jYWNlcnRzLmRp +Z2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbENBRzIuY3J0MAwGA1UdEwEB/wQCMAAw +ggF+BgorBgEEAdZ5AgQCBIIBbgSCAWoBaAB2ACl5vvCeOTkh8FZzn2Old+W+V32c +YAr4+U1dJlwlXceEAAABfFOKWLsAAAQDAEcwRQIhAOIjvEg/ozDhjhiV0fbaYc83 +oPb2I08md/bU7nhfQufgAiAyA6CaJgpUYYQjchPqiS3DzHIIQL0FIkohEcluqPJV +oAB3AFGjsPX9AXmcVm24N3iPDKR6zBsny/eeiEKaDf7UiwXlAAABfFOKWOQAAAQD +AEgwRgIhAJA7QR62SZNgoU0SCfXaPUriW4FrPlVUZbSLl7s+q3W4AiEAg/iCIUET +pZEw8IAvQGC4ggNwRgm97pma3Ug8FhJp0KgAdQBByMqx3yJGShDGoToJQodeTjGL +GwPr60vHaPCQYpYG9gAAAXxTilifAAAEAwBGMEQCIFcuV+EsdfqUxhSV4+pSF5I/ +EVBVin/kOpaTBcSTlHccAiA7IdRwyN09v9bnXKJ2XsMDGfe9RHwWwVoXA/NJMx5A +3DANBgkqhkiG9w0BAQsFAAOCAQEAyLJluG6AFZ5fVgxdS574SZd7dInEumnQUQmt ++D/vbUb4NfiO4aRdPGkGotGHptFW9wxYjvYlSmtWbns5v+0CmpiXadXLNPWZtNjJ +gFR3WTLbHzBtD+C8yL1AQ/L2kOk9PVpq50leD7+dZimurjX3k7CbxaItjHnKAq3V +klINM2LLV//ZqnQxBlHeUiTfnUjKR/0FRDM4zr247HX0BH5CUl3wT5/e6mBqkXkK +Vvl+zzVfQjkeL40KjJJjQ4+N0gsPS8+rBPGHEVXFGPtlbI6pyS3Wo2rTkaIDwd+i +7ckVQviFKPooe4SVfp+kX4xVu9BEI4hlVa2y7qc/mMecBpXT+Q== +-----END CERTIFICATE-----