diff --git a/.github/workflows/x509-cert.yml b/.github/workflows/x509-cert.yml index 3ea1ff2cc..0c52777c7 100644 --- a/.github/workflows/x509-cert.yml +++ b/.github/workflows/x509-cert.yml @@ -38,12 +38,13 @@ jobs: toolchain: ${{ matrix.rust }} targets: ${{ matrix.target }} - uses: RustCrypto/actions/cargo-hack-install@master - - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features arbitrary,default,std + - run: cargo hack build --target ${{ matrix.target }} --feature-powerset --exclude-features arbitrary,builder,default,std minimal-versions: uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master with: working-directory: ${{ github.workflow }} + install-zlint: true test: runs-on: ubuntu-latest @@ -59,6 +60,8 @@ jobs: with: toolchain: ${{ matrix.rust }} - uses: RustCrypto/actions/cargo-hack-install@master + - name: Install zlint + uses: RustCrypto/actions/zlint-install@master - run: cargo hack test --feature-powerset fuzz: diff --git a/Cargo.lock b/Cargo.lock index d653bc18a..91452c6d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,6 +58,12 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" name = "base16ct" version = "0.2.0" +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base32" version = "0.4.0" @@ -86,6 +92,12 @@ dependencies = [ "proptest", ] +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "basic-toml" version = "0.1.2" @@ -246,9 +258,9 @@ version = "0.2.0-pre.0" dependencies = [ "const-oid 0.9.2", "crmf", - "der", + "der 0.7.3", "hex-literal", - "spki", + "spki 0.7.1", "x509-cert", ] @@ -257,10 +269,10 @@ name = "cms" version = "0.2.0" dependencies = [ "const-oid 0.9.2", - "der", + "der 0.7.3", "hex-literal", "pkcs5", - "spki", + "spki 0.7.1", "x509-cert", ] @@ -332,8 +344,8 @@ version = "0.2.0-pre.0" dependencies = [ "cms", "const-oid 0.9.2", - "der", - "spki", + "der 0.7.3", + "spki 0.7.1", "x509-cert", ] @@ -380,6 +392,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2538c4e68e52548bacb3e83ac549f903d44f011ac9d5abb5e132e67d0808f7" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -399,12 +423,23 @@ dependencies = [ "der_derive", "flagset", "hex-literal", - "pem-rfc7468", + "pem-rfc7468 0.7.0", "proptest", "time", "zeroize", ] +[[package]] +name = "der" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b10af9f9f9f2134a42d3f8aa74658660f2e0234b0eb81bd171df8aa32779ed" +dependencies = [ + "const-oid 0.9.2", + "pem-rfc7468 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize", +] + [[package]] name = "der_derive" version = "0.7.0" @@ -442,16 +477,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" dependencies = [ "block-buffer", + "const-oid 0.9.2", "crypto-common", "subtle", ] +[[package]] +name = "ecdsa" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106401dadc137d05cb0d4ab4d42be089746aefdfe8992df4d0edcf351c16ddca" +dependencies = [ + "der 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + [[package]] name = "either" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "elliptic-curve" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cdacd4d6ed3f9b98680b679c0e52a823b8a2c7a97358d508fe247f2180c282" +dependencies = [ + "base16ct 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "pem-rfc7468 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "pkcs8 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core", + "sec1 0.7.1", + "subtle", + "zeroize", +] + [[package]] name = "errno" version = "0.3.0" @@ -482,6 +551,16 @@ dependencies = [ "instant", ] +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "flagset" version = "0.4.3" @@ -597,13 +676,14 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -616,6 +696,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "half" version = "1.8.2" @@ -736,6 +827,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -779,6 +873,44 @@ dependencies = [ "autocfg", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2399c9463abc5f909349d8aa9ba080e0b88b3ce2885389b60b993f39b1a56905" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" @@ -817,6 +949,18 @@ version = "6.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +[[package]] +name = "p256" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7270da3e5caa82afd3deb054cc237905853813aea3859544bc082c3fe55b8d47" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + [[package]] name = "pbkdf2" version = "0.12.1" @@ -831,7 +975,16 @@ dependencies = [ name = "pem-rfc7468" version = "0.7.0" dependencies = [ - "base64ct", + "base64ct 1.6.0", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -851,14 +1004,26 @@ name = "pkcs1" version = "0.7.2" dependencies = [ "const-oid 0.9.2", - "der", + "der 0.7.3", "hex-literal", - "pkcs8", - "spki", + "pkcs8 0.10.2", + "spki 0.7.1", "tempfile", "zeroize", ] +[[package]] +name = "pkcs1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "575fd6eebed721a2929faa1ee1383a49788378083bbbd7f299af75dd84195cee" +dependencies = [ + "der 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "pkcs8 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "spki 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "zeroize", +] + [[package]] name = "pkcs12" version = "0.0.0" @@ -869,23 +1034,23 @@ version = "0.7.1" dependencies = [ "aes", "cbc", - "der", + "der 0.7.3", "des", "hex-literal", "pbkdf2", "scrypt", "sha1", "sha2", - "spki", + "spki 0.7.1", ] [[package]] name = "pkcs7" version = "0.4.0" dependencies = [ - "der", + "der 0.7.3", "hex-literal", - "spki", + "spki 0.7.1", "x509-cert", ] @@ -893,15 +1058,25 @@ dependencies = [ name = "pkcs8" version = "0.10.2" dependencies = [ - "der", + "der 0.7.3", "hex-literal", "pkcs5", "rand_core", - "spki", + "spki 0.7.1", "subtle", "tempfile", ] +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "spki 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "plotters" version = "0.3.4" @@ -936,6 +1111,15 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "primeorder" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7613fdcc0831c10060fa69833ea8fa2caa94b6456f51e25356a885b530a2e3d0" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1098,6 +1282,38 @@ version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rsa" +version = "0.9.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16504cc31b04d2a5ec729f0c7e1b62e76634a9537f089df0ca1981dc8208a89" +dependencies = [ + "byteorder", + "const-oid 0.9.2", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)", + "pkcs8 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core", + "sha2", + "signature", + "subtle", + "zeroize", +] + [[package]] name = "rstest" version = "0.17.0" @@ -1135,16 +1351,16 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.7" +version = "0.37.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" +checksum = "1aef160324be24d31a62147fae491c14d2204a3865c7ca8c3b0d7f7bcb3ea635" dependencies = [ "bitflags", "errno", "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1200,15 +1416,29 @@ dependencies = [ "sha2", ] +[[package]] +name = "sec1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48518a2b5775ba8ca5b46596aae011caa431e6ce7e4a67ead66d92f08884220e" +dependencies = [ + "base16ct 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "der 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array", + "pkcs8 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", + "subtle", + "zeroize", +] + [[package]] name = "sec1" version = "0.7.2" dependencies = [ - "base16ct", - "der", + "base16ct 0.2.0", + "der 0.7.3", "generic-array", "hex-literal", - "pkcs8", + "pkcs8 0.10.2", "serdect", "subtle", "tempfile", @@ -1275,7 +1505,7 @@ dependencies = [ name = "serdect" version = "0.2.0" dependencies = [ - "base16ct", + "base16ct 0.2.0", "bincode", "ciborium", "hex-literal", @@ -1309,6 +1539,16 @@ dependencies = [ "digest", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest", + "rand_core", +] + [[package]] name = "slab" version = "0.4.8" @@ -1318,18 +1558,40 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spki" version = "0.7.1" dependencies = [ "arbitrary", - "base64ct", - "der", + "base64ct 1.6.0", + "der 0.7.3", "hex-literal", "sha2", "tempfile", ] +[[package]] +name = "spki" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37a5be806ab6f127c3da44b7378837ebf01dadca8510a0e572460216b228bd0e" +dependencies = [ + "base64ct 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", + "der 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "subtle" version = "2.4.1" @@ -1782,10 +2044,28 @@ version = "0.2.1" dependencies = [ "arbitrary", "const-oid 0.9.2", - "der", + "der 0.7.3", + "ecdsa", "hex-literal", + "p256", + "rand", + "rsa", "rstest", - "spki", + "sha1", + "sha2", + "signature", + "spki 0.7.1", + "tempfile", + "x509-cert-test-support", +] + +[[package]] +name = "x509-cert-test-support" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "tempfile", ] [[package]] @@ -1793,9 +2073,9 @@ name = "x509-ocsp" version = "0.2.0-pre" dependencies = [ "const-oid 0.9.2", - "der", + "der 0.7.3", "hex-literal", - "spki", + "spki 0.7.1", "x509-cert", ] diff --git a/Cargo.toml b/Cargo.toml index 8328d10f0..a2137acd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ members = [ "tls_codec", "tls_codec/derive", "x509-cert", + "x509-cert/test-support", "x509-ocsp" ] diff --git a/x509-cert/Cargo.toml b/x509-cert/Cargo.toml index d0202bec8..8d74686d6 100644 --- a/x509-cert/Cargo.toml +++ b/x509-cert/Cargo.toml @@ -16,21 +16,32 @@ rust-version = "1.65" [dependencies] const-oid = { version = "0.9.2", features = ["db"] } # TODO: path = "../const-oid" -der = { version = "0.7", features = ["alloc", "derive", "flagset", "oid"] } -spki = { version = "0.7", features = ["alloc"] } +der = { version = "0.7.3", features = ["alloc", "derive", "flagset", "oid"] } +spki = { version = "0.7.1", features = ["alloc"] } # optional dependencies arbitrary = { version = "1.3", features = ["derive"], optional = true } +sha1 = { version = "0.10.0", optional = true } +signature = { version = "2.1.0", features = ["digest"], optional = true } [dev-dependencies] hex-literal = "0.4" +rand = "0.8.5" +rsa = { version = "0.9.0-pre.1", features = ["sha2"] } +ecdsa = { version = "0.16.4", features = ["digest", "pem"] } +p256 = "0.13.0" rstest = "0.17" +sha2 = { version = "0.10", features = ["oid"] } +tempfile = "3.5.0" +x509-cert-test-support = { path = "./test-support" } [features] default = ["pem", "std"] std = ["const-oid/std", "der/std", "spki/std"] arbitrary = ["dep:arbitrary", "std", "der/arbitrary", "spki/arbitrary"] +builder = ["std", "sha1/default", "signature"] +hazmat = [] pem = ["der/pem", "spki/pem"] [package.metadata.docs.rs] diff --git a/x509-cert/src/builder.rs b/x509-cert/src/builder.rs new file mode 100644 index 000000000..8174678d7 --- /dev/null +++ b/x509-cert/src/builder.rs @@ -0,0 +1,336 @@ +//! X509 Certificate builder + +use alloc::vec; +use core::fmt; +use der::{asn1::BitString, referenced::OwnedToRef, Encode}; +use signature::{Keypair, SignatureEncoding, Signer}; +use spki::{ + DynSignatureAlgorithmIdentifier, EncodePublicKey, SubjectPublicKeyInfoOwned, + SubjectPublicKeyInfoRef, +}; + +use crate::{ + certificate::{Certificate, TbsCertificate, Version}, + ext::{ + pkix::{ + AuthorityKeyIdentifier, BasicConstraints, KeyUsage, KeyUsages, SubjectKeyIdentifier, + }, + AsExtension, Extension, + }, + name::Name, + serial_number::SerialNumber, + time::Validity, +}; + +/// Error type +#[derive(Debug)] +#[non_exhaustive] +pub enum Error { + /// ASN.1 DER-related errors. + Asn1(der::Error), + + /// Public key errors propagated from the [`spki::Error`] type. + PublicKey(spki::Error), + + /// Signing error propagated for the [`signature::Signer`] type. + Signature(signature::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Asn1(err) => write!(f, "ASN.1 error: {}", err), + Error::PublicKey(err) => write!(f, "public key error: {}", err), + Error::Signature(err) => write!(f, "signature error: {}", err), + } + } +} + +impl From for Error { + fn from(err: der::Error) -> Error { + Error::Asn1(err) + } +} + +impl From for Error { + fn from(err: spki::Error) -> Error { + Error::PublicKey(err) + } +} + +impl From for Error { + fn from(err: signature::Error) -> Error { + Error::Signature(err) + } +} + +type Result = core::result::Result; + +/// The type of certificate to build +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Profile { + /// Build a root CA certificate + Root, + /// Build an intermediate sub CA certificate + SubCA { + /// issuer Name, + /// represents the name signing the certificate + issuer: Name, + /// pathLenConstraint INTEGER (0..MAX) OPTIONAL + /// BasicConstraints as defined in [RFC 5280 Section 4.2.1.9]. + path_len_constraint: Option, + }, + /// Build an end certificate + Leaf { + /// issuer Name, + /// represents the name signing the certificate + issuer: Name, + /// should the key agreement flag of KeyUsage be enabled + enable_key_agreement: bool, + }, + #[cfg(feature = "hazmat")] + /// Opt-out of the default extensions + Manual { + /// issuer Name, + /// represents the name signing the certificate + /// A `None` will make it a self-signed certificate + issuer: Option, + }, +} + +impl Profile { + fn get_issuer(&self, subject: &Name) -> Name { + match self { + Profile::Root => subject.clone(), + Profile::SubCA { issuer, .. } => issuer.clone(), + Profile::Leaf { issuer, .. } => issuer.clone(), + #[cfg(feature = "hazmat")] + Profile::Manual { issuer, .. } => issuer.as_ref().unwrap_or(subject).clone(), + } + } + + fn build_extensions( + &self, + spk: SubjectPublicKeyInfoRef<'_>, + issuer_spk: SubjectPublicKeyInfoRef<'_>, + tbs: &TbsCertificate, + ) -> Result> { + #[cfg(feature = "hazmat")] + // User opted out of default extensions set. + if let Profile::Manual { .. } = self { + return Ok(vec::Vec::default()); + } + + let mut extensions: vec::Vec = vec::Vec::new(); + + extensions.push(SubjectKeyIdentifier::try_from(spk)?.to_extension(tbs)?); + + // Build Authority Key Identifier + match self { + Profile::Root => {} + _ => { + extensions + .push(AuthorityKeyIdentifier::try_from(issuer_spk.clone())?.to_extension(tbs)?); + } + } + + // Build Basic Contraints extensions + extensions.push(match self { + Profile::Root => BasicConstraints { + ca: true, + path_len_constraint: None, + } + .to_extension(tbs)?, + Profile::SubCA { + path_len_constraint, + .. + } => BasicConstraints { + ca: true, + path_len_constraint: *path_len_constraint, + } + .to_extension(tbs)?, + Profile::Leaf { .. } => BasicConstraints { + ca: false, + path_len_constraint: None, + } + .to_extension(tbs)?, + #[cfg(feature = "hazmat")] + Profile::Manual { .. } => unreachable!(), + }); + + // Build Key Usage extension + match self { + Profile::Root | Profile::SubCA { .. } => { + extensions.push( + KeyUsage( + KeyUsages::DigitalSignature | KeyUsages::KeyCertSign | KeyUsages::CRLSign, + ) + .to_extension(tbs)?, + ); + } + Profile::Leaf { + enable_key_agreement, + .. + } => { + let mut key_usage = KeyUsages::DigitalSignature + | KeyUsages::NonRepudiation + | KeyUsages::KeyEncipherment; + if *enable_key_agreement { + key_usage |= KeyUsages::KeyAgreement; + } + + extensions.push(KeyUsage(key_usage).to_extension(tbs)?); + } + #[cfg(feature = "hazmat")] + Profile::Manual { .. } => unreachable!(), + } + + Ok(extensions) + } +} + +/// X509 Certificate builder +/// +/// ``` +/// use der::Decode; +/// use x509_cert::spki::SubjectPublicKeyInfoOwned; +/// use x509_cert::certificate::Version; +/// use x509_cert::builder::{CertificateBuilder, Profile}; +/// use x509_cert::name::Name; +/// use x509_cert::serial_number::SerialNumber; +/// use x509_cert::time::Validity; +/// use std::str::FromStr; +/// +/// # const RSA_2048_DER: &[u8] = include_bytes!("../tests/examples/rsa2048-pub.der"); +/// # const RSA_2048_PRIV_DER: &[u8] = include_bytes!("../tests/examples/rsa2048-priv.der"); +/// # use rsa::{pkcs1v15::SigningKey, pkcs1::DecodeRsaPrivateKey}; +/// # use sha2::Sha256; +/// # use std::time::Duration; +/// # use der::referenced::RefToOwned; +/// # fn rsa_signer() -> SigningKey { +/// # let private_key = rsa::RsaPrivateKey::from_pkcs1_der(RSA_2048_PRIV_DER).unwrap(); +/// # let signing_key = SigningKey::::new_with_prefix(private_key); +/// # signing_key +/// # } +/// +/// let serial_number = SerialNumber::from(42u32); +/// let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); +/// let profile = Profile::Root; +/// let subject = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US").unwrap(); +/// +/// let pub_key = SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER).expect("get rsa pub key"); +/// +/// let mut signer = rsa_signer(); +/// let mut builder = CertificateBuilder::new( +/// profile, +/// Version::V3, +/// serial_number, +/// validity, +/// subject, +/// pub_key, +/// &mut signer, +/// ) +/// .expect("Create certificate"); +/// ``` +pub struct CertificateBuilder<'s, S> { + tbs: TbsCertificate, + signer: &'s mut S, +} + +impl<'s, S> CertificateBuilder<'s, S> +where + S: Keypair, + S::VerifyingKey: EncodePublicKey, + S::VerifyingKey: DynSignatureAlgorithmIdentifier, +{ + /// Creates a new certificate builder + pub fn new( + profile: Profile, + version: Version, + serial_number: SerialNumber, + mut validity: Validity, + subject: Name, + subject_public_key_info: SubjectPublicKeyInfoOwned, + signer: &'s mut S, + ) -> Result + where + S: Signer, + { + let verifying_key = signer.verifying_key(); + let signer_pub = verifying_key + .to_public_key_der()? + .decode_msg::()?; + + let signature_alg = verifying_key.signature_algorithm_identifier()?; + let issuer = profile.get_issuer(&subject); + + validity.not_before.rfc5280_adjust_utc_time()?; + validity.not_after.rfc5280_adjust_utc_time()?; + + let mut tbs = TbsCertificate { + version, + serial_number, + signature: signature_alg, + issuer, + validity, + subject, + subject_public_key_info, + extensions: None, + + // We will not generate unique identifier because as per RFC5280 Section 4.1.2.8: + // CAs conforming to this profile MUST NOT generate + // certificates with unique identifiers. + // + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.8 + issuer_unique_id: None, + subject_unique_id: None, + }; + + if tbs.version == Version::V3 { + let extensions = profile.build_extensions( + tbs.subject_public_key_info.owned_to_ref(), + signer_pub.owned_to_ref(), + &tbs, + )?; + if !extensions.is_empty() { + tbs.extensions = Some(extensions); + } + } + + Ok(Self { tbs, signer }) + } + + /// Add an extension to this certificate + pub fn add_extension(&mut self, extension: &E) -> Result<()> { + if self.tbs.version == Version::V3 { + let ext = extension.to_extension(&self.tbs)?; + + if let Some(extensions) = self.tbs.extensions.as_mut() { + extensions.push(ext); + } else { + let extensions = vec![ext]; + self.tbs.extensions = Some(extensions); + } + } + + Ok(()) + } + + /// Run the certificate through the signer and build the end certificate. + pub fn build(&mut self) -> Result + where + S: Signer, + Signature: SignatureEncoding, + { + let signature = self.signer.try_sign(&self.tbs.to_der()?)?; + let signature = BitString::from_bytes(signature.to_bytes().as_ref())?; + + let cert = Certificate { + tbs_certificate: self.tbs.clone(), + signature_algorithm: self.tbs.signature.clone(), + signature, + }; + + Ok(cert) + } +} diff --git a/x509-cert/src/ext.rs b/x509-cert/src/ext.rs index 238de1860..843998c6f 100644 --- a/x509-cert/src/ext.rs +++ b/x509-cert/src/ext.rs @@ -1,5 +1,7 @@ //! Standardized X.509 Certificate Extensions +use crate::certificate; +use const_oid::AssociatedOid; use der::{asn1::OctetString, Sequence, ValueOrd}; use spki::ObjectIdentifier; @@ -42,3 +44,21 @@ pub struct Extension { /// /// [RFC 5280 Section 4.1.2.9]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.9 pub type Extensions = alloc::vec::Vec; + +/// Trait to be implemented by extensions to allow them to be formated as x509 v3 extensions by +/// builder. +pub trait AsExtension: AssociatedOid + der::Encode { + /// Should the extension be marked critical + fn critical(&self, tbs: &certificate::TbsCertificate) -> bool; + + /// Returns the Extension with the content encoded. + fn to_extension(&self, tbs: &certificate::TbsCertificate) -> Result { + let content = OctetString::new(::to_der(self)?)?; + + Ok(Extension { + extn_id: ::OID, + critical: self.critical(tbs), + extn_value: content, + }) + } +} diff --git a/x509-cert/src/ext/pkix.rs b/x509-cert/src/ext/pkix.rs index 7443e1edf..7e5b19060 100644 --- a/x509-cert/src/ext/pkix.rs +++ b/x509-cert/src/ext/pkix.rs @@ -48,6 +48,11 @@ impl AssociatedOid for SubjectKeyIdentifier { } impl_newtype!(SubjectKeyIdentifier, OctetString); +impl_extension!(SubjectKeyIdentifier, critical = false); +impl_key_identifier!( + SubjectKeyIdentifier, + (|result: &[u8]| Ok(Self(OctetString::new(result)?))) +); /// SubjectAltName as defined in [RFC 5280 Section 4.2.1.6]. /// @@ -65,6 +70,23 @@ impl AssociatedOid for SubjectAltName { impl_newtype!(SubjectAltName, name::GeneralNames); +impl crate::ext::AsExtension for SubjectAltName { + fn critical(&self, tbs: &crate::certificate::TbsCertificate) -> bool { + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 + // Further, if the only subject identity included in the certificate is + // an alternative name form (e.g., an electronic mail address), then the + // subject distinguished name MUST be empty (an empty sequence), and the + // subjectAltName extension MUST be present. If the subject field + // contains an empty sequence, then the issuing CA MUST include a + // subjectAltName extension that is marked as critical. When including + // the subjectAltName extension in a certificate that has a non-empty + // subject distinguished name, conforming CAs SHOULD mark the + // subjectAltName extension as non-critical. + + tbs.subject.is_empty() + } +} + /// IssuerAltName as defined in [RFC 5280 Section 4.2.1.7]. /// /// ```text @@ -80,6 +102,7 @@ impl AssociatedOid for IssuerAltName { } impl_newtype!(IssuerAltName, name::GeneralNames); +impl_extension!(IssuerAltName, critical = false); /// SubjectDirectoryAttributes as defined in [RFC 5280 Section 4.2.1.8]. /// @@ -96,6 +119,7 @@ impl AssociatedOid for SubjectDirectoryAttributes { } impl_newtype!(SubjectDirectoryAttributes, Vec); +impl_extension!(SubjectDirectoryAttributes, critical = false); /// InhibitAnyPolicy as defined in [RFC 5280 Section 4.2.1.14]. /// @@ -112,3 +136,4 @@ impl AssociatedOid for InhibitAnyPolicy { } impl_newtype!(InhibitAnyPolicy, u32); +impl_extension!(InhibitAnyPolicy, critical = true); diff --git a/x509-cert/src/ext/pkix/access.rs b/x509-cert/src/ext/pkix/access.rs index 4cac6d028..4d2d9db17 100644 --- a/x509-cert/src/ext/pkix/access.rs +++ b/x509-cert/src/ext/pkix/access.rs @@ -23,6 +23,7 @@ impl AssociatedOid for AuthorityInfoAccessSyntax { } impl_newtype!(AuthorityInfoAccessSyntax, Vec); +impl_extension!(AuthorityInfoAccessSyntax, critical = false); /// SubjectInfoAccessSyntax as defined in [RFC 5280 Section 4.2.2.2]. /// @@ -39,6 +40,7 @@ impl AssociatedOid for SubjectInfoAccessSyntax { } impl_newtype!(SubjectInfoAccessSyntax, Vec); +impl_extension!(SubjectInfoAccessSyntax, critical = false); /// AccessDescription as defined in [RFC 5280 Section 4.2.2.1]. /// diff --git a/x509-cert/src/ext/pkix/authkeyid.rs b/x509-cert/src/ext/pkix/authkeyid.rs index 9e4ec50b7..60b61e5e0 100644 --- a/x509-cert/src/ext/pkix/authkeyid.rs +++ b/x509-cert/src/ext/pkix/authkeyid.rs @@ -19,7 +19,7 @@ use der::Sequence; /// ``` /// /// [RFC 5280 Section 4.2.1.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.1 -#[derive(Clone, Debug, Eq, PartialEq, Sequence)] +#[derive(Clone, Debug, Eq, PartialEq, Sequence, Default)] #[allow(missing_docs)] pub struct AuthorityKeyIdentifier { #[asn1(context_specific = "0", tag_mode = "IMPLICIT", optional = "true")] @@ -35,3 +35,12 @@ pub struct AuthorityKeyIdentifier { impl AssociatedOid for AuthorityKeyIdentifier { const OID: ObjectIdentifier = ID_CE_AUTHORITY_KEY_IDENTIFIER; } + +impl_extension!(AuthorityKeyIdentifier, critical = false); +impl_key_identifier!( + AuthorityKeyIdentifier, + (|result: &[u8]| Ok(Self { + key_identifier: Some(OctetString::new(result)?), + ..Default::default() + })) +); diff --git a/x509-cert/src/ext/pkix/certpolicy.rs b/x509-cert/src/ext/pkix/certpolicy.rs index 07c4e8cc9..85820c14d 100644 --- a/x509-cert/src/ext/pkix/certpolicy.rs +++ b/x509-cert/src/ext/pkix/certpolicy.rs @@ -14,6 +14,10 @@ use der::{Any, Choice, Sequence, ValueOrd}; /// ``` /// /// [RFC 5280 Section 4.2.1.4]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.4 +// If this extension is +// critical, the path validation software MUST be able to interpret this +// extension (including the optional qualifier), or MUST reject the +// certificate. #[derive(Clone, Debug, PartialEq, Eq)] pub struct CertificatePolicies(pub Vec); @@ -22,6 +26,7 @@ impl AssociatedOid for CertificatePolicies { } impl_newtype!(CertificatePolicies, Vec); +impl_extension!(CertificatePolicies); /// PolicyInformation as defined in [RFC 5280 Section 4.2.1.4]. /// diff --git a/x509-cert/src/ext/pkix/constraints/basic.rs b/x509-cert/src/ext/pkix/constraints/basic.rs index 5972cc8eb..d1ff02a0f 100644 --- a/x509-cert/src/ext/pkix/constraints/basic.rs +++ b/x509-cert/src/ext/pkix/constraints/basic.rs @@ -22,3 +22,24 @@ pub struct BasicConstraints { impl AssociatedOid for BasicConstraints { const OID: ObjectIdentifier = ID_CE_BASIC_CONSTRAINTS; } + +impl crate::ext::AsExtension for BasicConstraints { + fn critical(&self, _tbs: &crate::certificate::TbsCertificate) -> bool { + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 + // Conforming CAs MUST include this extension in all CA certificates + // that contain public keys used to validate digital signatures on + // certificates and MUST mark the extension as critical in such + // certificates. This extension MAY appear as a critical or non- + // critical extension in CA certificates that contain public keys used + // exclusively for purposes other than validating digital signatures on + // certificates. Such CA certificates include ones that contain public + // keys used exclusively for validating digital signatures on CRLs and + // ones that contain key management public keys used with certificate + // enrollment protocols. This extension MAY appear as a critical or + // non-critical extension in end entity certificates. + + // NOTE(baloo): from the spec, it doesn't appear to hurt if we force the extension + // to be critical. + true + } +} diff --git a/x509-cert/src/ext/pkix/constraints/name.rs b/x509-cert/src/ext/pkix/constraints/name.rs index ee6cd2895..c05d00564 100644 --- a/x509-cert/src/ext/pkix/constraints/name.rs +++ b/x509-cert/src/ext/pkix/constraints/name.rs @@ -31,6 +31,8 @@ impl AssociatedOid for NameConstraints { const OID: ObjectIdentifier = ID_CE_NAME_CONSTRAINTS; } +impl_extension!(NameConstraints, critical = true); + /// GeneralSubtrees as defined in [RFC 5280 Section 4.2.1.10]. /// /// ```text diff --git a/x509-cert/src/ext/pkix/constraints/policy.rs b/x509-cert/src/ext/pkix/constraints/policy.rs index dcb1a8cb9..c4097372a 100644 --- a/x509-cert/src/ext/pkix/constraints/policy.rs +++ b/x509-cert/src/ext/pkix/constraints/policy.rs @@ -24,3 +24,5 @@ pub struct PolicyConstraints { impl AssociatedOid for PolicyConstraints { const OID: ObjectIdentifier = ID_CE_POLICY_CONSTRAINTS; } + +impl_extension!(PolicyConstraints, critical = true); diff --git a/x509-cert/src/ext/pkix/crl.rs b/x509-cert/src/ext/pkix/crl.rs index 04dd20925..5516fbc9e 100644 --- a/x509-cert/src/ext/pkix/crl.rs +++ b/x509-cert/src/ext/pkix/crl.rs @@ -28,6 +28,7 @@ impl AssociatedOid for CrlNumber { } impl_newtype!(CrlNumber, Uint); +impl_extension!(CrlNumber, critical = false); /// BaseCRLNumber as defined in [RFC 5280 Section 5.2.4]. /// @@ -60,6 +61,7 @@ impl AssociatedOid for CrlDistributionPoints { } impl_newtype!(CrlDistributionPoints, Vec); +impl_extension!(CrlDistributionPoints, critical = false); /// FreshestCrl as defined in [RFC 5280 Section 5.2.6]. /// @@ -76,6 +78,7 @@ impl AssociatedOid for FreshestCrl { } impl_newtype!(FreshestCrl, Vec); +impl_extension!(FreshestCrl, critical = false); /// CRLReason as defined in [RFC 5280 Section 5.3.1]. /// diff --git a/x509-cert/src/ext/pkix/keyusage.rs b/x509-cert/src/ext/pkix/keyusage.rs index bb37e5021..e6087f482 100644 --- a/x509-cert/src/ext/pkix/keyusage.rs +++ b/x509-cert/src/ext/pkix/keyusage.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use const_oid::db::rfc5280::{ - ID_CE_EXT_KEY_USAGE, ID_CE_KEY_USAGE, ID_CE_PRIVATE_KEY_USAGE_PERIOD, + ANY_EXTENDED_KEY_USAGE, ID_CE_EXT_KEY_USAGE, ID_CE_KEY_USAGE, ID_CE_PRIVATE_KEY_USAGE_PERIOD, }; use const_oid::AssociatedOid; use der::asn1::{GeneralizedTime, ObjectIdentifier}; @@ -52,6 +52,7 @@ impl AssociatedOid for KeyUsage { } impl_newtype!(KeyUsage, FlagSet); +impl_extension!(KeyUsage, critical = true); impl KeyUsage { /// The subject public key is used for verifying digital signatures @@ -137,6 +138,26 @@ impl AssociatedOid for ExtendedKeyUsage { impl_newtype!(ExtendedKeyUsage, Vec); +impl crate::ext::AsExtension for ExtendedKeyUsage { + fn critical(&self, _tbs: &crate::certificate::TbsCertificate) -> bool { + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12 + // This extension MAY, at the option of the certificate issuer, be + // either critical or non-critical. + // + // If a CA includes extended key usages to satisfy such applications, + // but does not wish to restrict usages of the key, the CA can include + // the special KeyPurposeId anyExtendedKeyUsage in addition to the + // particular key purposes required by the applications. Conforming CAs + // SHOULD NOT mark this extension as critical if the anyExtendedKeyUsage + // KeyPurposeId is present. Applications that require the presence of a + // particular purpose MAY reject certificates that include the + // anyExtendedKeyUsage OID but not the particular OID expected for the + // application. + + !self.0.iter().any(|el| *el == ANY_EXTENDED_KEY_USAGE) + } +} + /// PrivateKeyUsagePeriod as defined in [RFC 3280 Section 4.2.1.4]. /// /// RFC 5280 states "use of this ISO standard extension is neither deprecated nor recommended for use in the Internet PKI." diff --git a/x509-cert/src/ext/pkix/policymap.rs b/x509-cert/src/ext/pkix/policymap.rs index e6b55bad5..1997f5493 100644 --- a/x509-cert/src/ext/pkix/policymap.rs +++ b/x509-cert/src/ext/pkix/policymap.rs @@ -20,6 +20,7 @@ impl AssociatedOid for PolicyMappings { } impl_newtype!(PolicyMappings, Vec); +impl_extension!(PolicyMappings, critical = true); /// PolicyMapping as defined in [RFC 5280 Section 4.2.1.5]. /// diff --git a/x509-cert/src/lib.rs b/x509-cert/src/lib.rs index a09c3bd32..11ef03ca9 100644 --- a/x509-cert/src/lib.rs +++ b/x509-cert/src/lib.rs @@ -33,6 +33,9 @@ pub mod request; pub mod serial_number; pub mod time; +#[cfg(feature = "builder")] +pub mod builder; + pub use certificate::{Certificate, PkiPath, TbsCertificate, Version}; pub use der; pub use spki; diff --git a/x509-cert/src/macros.rs b/x509-cert/src/macros.rs index f325fde63..d6ab9addc 100644 --- a/x509-cert/src/macros.rs +++ b/x509-cert/src/macros.rs @@ -78,3 +78,57 @@ macro_rules! impl_newtype { } }; } + +/// Implements the AsExtension traits for every defined Extension paylooad +macro_rules! impl_extension { + ($newtype:ty) => { + impl_extension!($newtype, critical = false); + }; + ($newtype:ty, critical = $critical:expr) => { + impl crate::ext::AsExtension for $newtype { + fn critical(&self, _tbs: &crate::certificate::TbsCertificate) -> bool { + $critical + } + } + }; +} + +/// Implements conversions between [`spki::SubjectPublicKeyInfo`] and [`SubjectKeyIdentifier`] or [`AuthorityKeyIdentifier`] +macro_rules! impl_key_identifier { + ($newtype:ty, $out:expr) => { + #[cfg(feature = "builder")] + mod builder_key_identifier { + use super::*; + use der::asn1::OctetString; + use sha1::{Digest, Sha1}; + use spki::SubjectPublicKeyInfoRef; + + impl<'a> TryFrom> for $newtype { + type Error = der::Error; + + fn try_from(issuer: SubjectPublicKeyInfoRef<'a>) -> Result { + // https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 + // + // For CA certificates, subject key identifiers SHOULD be derived from + // the public key or a method that generates unique values. Two common + // methods for generating key identifiers from the public key are: + + // (1) The keyIdentifier is composed of the 160-bit SHA-1 hash of the + // value of the BIT STRING subjectPublicKey (excluding the tag, + // length, and number of unused bits). + + // (2) The keyIdentifier is composed of a four-bit type field with + // the value 0100 followed by the least significant 60 bits of + // the SHA-1 hash of the value of the BIT STRING + // subjectPublicKey (excluding the tag, length, and number of + // unused bits). + + // Here we're using the first method + + let result = Sha1::digest(issuer.subject_public_key.raw_bytes()); + $out(result.as_slice()) + } + } + } + }; +} diff --git a/x509-cert/src/name.rs b/x509-cert/src/name.rs index f5755b806..dfceea112 100644 --- a/x509-cert/src/name.rs +++ b/x509-cert/src/name.rs @@ -31,6 +31,11 @@ impl RdnSequence { pub fn encode_from_string(s: &str) -> Result, der::Error> { Self::from_str(s)?.to_der() } + + /// Is this [`RdnSequence`] empty? + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } /// Parse an [`RdnSequence`] string. diff --git a/x509-cert/src/serial_number.rs b/x509-cert/src/serial_number.rs index 814c09629..d9cb50523 100644 --- a/x509-cert/src/serial_number.rs +++ b/x509-cert/src/serial_number.rs @@ -3,8 +3,9 @@ use core::fmt::Display; use der::{ - asn1::Int, asn1::Uint, DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, - Result, Tag, ValueOrd, Writer, + asn1::{self, Int}, + DecodeValue, EncodeValue, ErrorKind, FixedTag, Header, Length, Reader, Result, Tag, ValueOrd, + Writer, }; /// [RFC 5280 Section 4.1.2.2.] Serial Number @@ -39,7 +40,7 @@ impl SerialNumber { /// /// The byte slice **must** represent a positive integer. pub fn new(bytes: &[u8]) -> Result { - let inner = Uint::new(bytes)?; + let inner = asn1::Uint::new(bytes)?; // The user might give us a 20 byte unsigned integer with a high MSB, // which we'd then encode with 21 octets to preserve the sign bit. @@ -106,6 +107,27 @@ impl Display for SerialNumber { } } +macro_rules! impl_from { + ($source:ty) => { + impl From<$source> for SerialNumber { + fn from(inner: $source) -> SerialNumber { + let serial_number = &inner.to_be_bytes()[..]; + let serial_number = asn1::Uint::new(serial_number).unwrap(); + + // This could only fail if the big endian representation was to be more than 20 + // bytes long. Because it's only implemented for up to u64 / usize (8 bytes). + SerialNumber::new(serial_number.as_bytes()).unwrap() + } + } + }; +} + +impl_from!(u8); +impl_from!(u16); +impl_from!(u32); +impl_from!(u64); +impl_from!(usize); + // Implement by hand because the derive would create invalid values. // Use the constructor to create a valid value. #[cfg(feature = "arbitrary")] diff --git a/x509-cert/src/time.rs b/x509-cert/src/time.rs index 092874c4d..5948a7976 100644 --- a/x509-cert/src/time.rs +++ b/x509-cert/src/time.rs @@ -58,6 +58,20 @@ impl Time { Time::GeneralTime(t) => t.to_system_time(), } } + + /// Convert time to UTCTime representation + /// As per RFC 5280: 4.1.2.5, date through 2049 should be expressed as UTC Time. + #[cfg(feature = "builder")] + pub(crate) fn rfc5280_adjust_utc_time(&mut self) -> der::Result<()> { + if let Time::GeneralTime(t) = self { + let date = t.to_date_time(); + if date.year() <= UtcTime::MAX_YEAR { + *self = Time::UtcTime(UtcTime::from_date_time(date)?); + } + } + + Ok(()) + } } impl fmt::Display for Time { diff --git a/x509-cert/test-support/.gitignore b/x509-cert/test-support/.gitignore new file mode 100644 index 000000000..ea8c4bf7f --- /dev/null +++ b/x509-cert/test-support/.gitignore @@ -0,0 +1 @@ +/target diff --git a/x509-cert/test-support/Cargo.toml b/x509-cert/test-support/Cargo.toml new file mode 100644 index 000000000..344478360 --- /dev/null +++ b/x509-cert/test-support/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "x509-cert-test-support" +description = """ +Set of utils to run certificates through zlint or openssl. +""" +version = "0.1.0" +edition = "2021" +authors = ["RustCrypto Developers"] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/RustCrypto/formats/tree/master/x509-cert/test-support" +categories = ["cryptography"] +readme = "README.md" + +[dependencies] +serde = { version = "1", features = ["derive"] } +serde_json = "1.0.95" +tempfile = "3.5.0" diff --git a/x509-cert/test-support/src/lib.rs b/x509-cert/test-support/src/lib.rs new file mode 100644 index 000000000..da52a50e2 --- /dev/null +++ b/x509-cert/test-support/src/lib.rs @@ -0,0 +1,2 @@ +pub mod openssl; +pub mod zlint; diff --git a/x509-cert/test-support/src/openssl.rs b/x509-cert/test-support/src/openssl.rs new file mode 100644 index 000000000..daa4365d8 --- /dev/null +++ b/x509-cert/test-support/src/openssl.rs @@ -0,0 +1,35 @@ +use std::{ + fs::File, + io::{Read, Write}, + process::{Command, Stdio}, +}; +use tempfile::tempdir; + +pub fn check_certificate(pem: &[u8]) -> String { + let tmp_dir = tempdir().expect("create tempdir"); + let cert_path = tmp_dir.path().join("cert.pem"); + + let mut cert_file = File::create(&cert_path).expect("create pem file"); + cert_file.write_all(pem).expect("Create pem file"); + + let mut child = Command::new("openssl") + .arg("x509") + .arg("-in") + .arg(&cert_path) + .arg("-noout") + .arg("-text") + .stderr(Stdio::inherit()) + .stdout(Stdio::piped()) + .spawn() + .expect("zlint failed"); + let mut stdout = child.stdout.take().unwrap(); + let exit_status = child.wait().expect("get openssl x509 status"); + + assert!(exit_status.success(), "openssl failed"); + let mut output_buf = Vec::new(); + stdout + .read_to_end(&mut output_buf) + .expect("read openssl output"); + + String::from_utf8(output_buf.clone()).unwrap() +} diff --git a/x509-cert/test-support/src/zlint.rs b/x509-cert/test-support/src/zlint.rs new file mode 100644 index 000000000..f13c1dced --- /dev/null +++ b/x509-cert/test-support/src/zlint.rs @@ -0,0 +1,195 @@ +use serde::{ + de::{Error, MapAccess, Visitor}, + Deserialize, Deserializer, +}; +use std::{ + collections::HashMap, + fmt, + fs::File, + io::{Read, Write}, + process::{Command, Stdio}, +}; +use tempfile::tempdir; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Status { + NotApplicable, + NotEffective, + Pass, + Notice, + Info, + Warn, + Error, + Fatal, +} + +impl Status { + pub fn is_successful(&self) -> bool { + *self != Status::Warn && *self != Status::Error + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct LintStatus { + pub status: Status, + pub details: Option, +} + +impl LintStatus { + pub fn is_successful(&self) -> bool { + self.status.is_successful() + } +} + +impl<'de> Deserialize<'de> for LintStatus { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + struct StatusVisitor; + + impl<'de> Visitor<'de> for StatusVisitor { + type Value = LintStatus; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer between -2^31 and 2^31") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut status_output = None; + let mut details = None; + + while let Some((key, value)) = access.next_entry::<&str, &str>()? { + if key == "result" { + status_output = Some(match value { + "NA" => Status::NotApplicable, + "NE" => Status::NotEffective, + "pass" => Status::Pass, + "notice" => Status::Notice, + "fatal" => Status::Fatal, + "error" => Status::Error, + "warn" => Status::Warn, + "info" => Status::Info, + other => { + return Err(M::Error::custom(format!( + "unsupported value: {}", + other, + ))) + } + }); + } + if key == "details" { + details = Some(value.to_string()); + } + } + + if let Some(status) = status_output { + Ok(LintStatus { status, details }) + } else { + Err(M::Error::custom("no 'result' field found")) + } + } + } + + deserializer.deserialize_map(StatusVisitor) + } +} + +#[derive(Debug, Deserialize)] +pub struct LintResult(pub HashMap); + +impl LintResult { + pub fn check_lints(&self, ignored: &[&str]) -> bool { + let mut failed = HashMap::::new(); + + for (key, value) in &self.0 { + if !value.is_successful() && !ignored.contains(&key.as_str()) { + failed.insert(String::from(key), value.clone()); + } + } + + eprintln!("failed lints: {:?}", failed); + + failed.is_empty() + } +} + +const ZLINT_CONFIG: &str = " +[AppleRootStorePolicyConfig] + +[CABFBaselineRequirementsConfig] + +[CABFEVGuidelinesConfig] + +[CommunityConfig] + +[MozillaRootStorePolicyConfig] + +[RFC5280Config] + +[RFC5480Config] + +[RFC5891Config] + +[e_rsa_fermat_factorization] +Rounds = 100 +"; + +pub fn check_certificate(pem: &[u8], ignored: &[&str]) { + let tmp_dir = tempdir().expect("create tempdir"); + let config_path = tmp_dir.path().join("config.toml"); + let cert_path = tmp_dir.path().join("cert.pem"); + + let mut config_file = File::create(&config_path).expect("create config file"); + config_file + .write_all(ZLINT_CONFIG.as_bytes()) + .expect("Create config file"); + + let mut cert_file = File::create(&cert_path).expect("create pem file"); + cert_file.write_all(pem).expect("Create pem file"); + + let mut child = Command::new("zlint") + .arg("-pretty") + .arg("-config") + .arg(&config_path) + .arg(&cert_path) + .stderr(Stdio::inherit()) + .stdout(Stdio::piped()) + .spawn() + .expect("zlint failed"); + let mut stdout = child.stdout.take().unwrap(); + let exit_status = child.wait().expect("get zlint status"); + + assert!(exit_status.success(), "zlint failed"); + let mut output_buf = Vec::new(); + stdout + .read_to_end(&mut output_buf) + .expect("read zlint output"); + + let output: LintResult = serde_json::from_slice(&output_buf).expect("parse zlint output"); + + assert!(output.check_lints(ignored)); +} + +#[test] +fn parse_zlint_output() { + let demo_output = br#" + { + "e_algorithm_identifier_improper_encoding": {"result": "pass"}, + "e_basic_constraints_not_critical": {"result": "NA", "details": "foo"} + } + "#; + + let output: LintResult = serde_json::from_slice(demo_output).expect("parse output"); + + assert_eq!( + output.0.get("e_algorithm_identifier_improper_encoding"), + Some(&LintStatus { + status: Status::Pass, + details: None + }) + ); +} diff --git a/x509-cert/tests/builder.rs b/x509-cert/tests/builder.rs new file mode 100644 index 000000000..0a2622f8d --- /dev/null +++ b/x509-cert/tests/builder.rs @@ -0,0 +1,179 @@ +#![cfg(all(feature = "builder", feature = "pem"))] + +use der::{pem::LineEnding, Decode, Encode, EncodePem}; +use p256::{pkcs8::DecodePrivateKey, NistP256}; +use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::pkcs1v15::SigningKey; +use sha2::Sha256; +use spki::SubjectPublicKeyInfoOwned; +use std::{str::FromStr, time::Duration}; +use x509_cert::{ + builder::{CertificateBuilder, Profile}, + certificate::Version, + name::Name, + serial_number::SerialNumber, + time::Validity, +}; +use x509_cert_test_support::{openssl, zlint}; + +const RSA_2048_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-pub.der"); + +#[test] +fn root_ca_certificate() { + let serial_number = SerialNumber::from(42u32); + let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); + let profile = Profile::Root; + let subject = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US") + .unwrap() + .to_der() + .unwrap(); + let subject = Name::from_der(&subject).unwrap(); + let pub_key = + SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key"); + + let mut signer = rsa_signer(); + let mut builder = CertificateBuilder::new( + profile, + Version::V3, + serial_number, + validity, + subject, + pub_key, + &mut signer, + ) + .expect("Create certificate"); + + let certificate = builder.build().unwrap(); + + let pem = certificate.to_pem(LineEnding::LF).expect("generate pem"); + println!("{}", openssl::check_certificate(pem.as_bytes())); + + let ignored = &[]; + zlint::check_certificate(pem.as_bytes(), ignored); +} + +#[test] +fn sub_ca_certificate() { + let serial_number = SerialNumber::from(42u32); + let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); + + let issuer = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US") + .unwrap() + .to_der() + .unwrap(); + let issuer = Name::from_der(&issuer).unwrap(); + let profile = Profile::SubCA { + issuer, + path_len_constraint: Some(0), + }; + + let subject = Name::from_str("CN=World domination task force,O=World domination Inc,C=US") + .unwrap() + .to_der() + .unwrap(); + let subject = Name::from_der(&subject).unwrap(); + let pub_key = + SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key"); + + let mut signer = ecdsa_signer(); + let mut builder = CertificateBuilder::new::>( + profile, + Version::V3, + serial_number, + validity, + subject, + pub_key, + &mut signer, + ) + .expect("Create certificate"); + + let certificate = builder.build::>().unwrap(); + + let pem = certificate.to_pem(LineEnding::LF).expect("generate pem"); + println!("{}", openssl::check_certificate(pem.as_bytes())); + + // TODO(baloo): not too sure we should tackle those in this API. + let ignored = &[ + "w_sub_ca_aia_missing", + "e_sub_ca_crl_distribution_points_missing", + "e_sub_ca_certificate_policies_missing", + "w_sub_ca_aia_does_not_contain_issuing_ca_url", + ]; + + zlint::check_certificate(pem.as_bytes(), ignored); +} + +#[test] +fn leaf_certificate() { + let serial_number = SerialNumber::from(42u32); + let validity = Validity::from_now(Duration::new(5, 0)).unwrap(); + + let issuer = Name::from_str("CN=World domination corporation,O=World domination Inc,C=US") + .unwrap() + .to_der() + .unwrap(); + let issuer = Name::from_der(&issuer).unwrap(); + let profile = Profile::Leaf { + issuer, + enable_key_agreement: false, + }; + + let subject = Name::from_str("CN=service.domination.world") + .unwrap() + .to_der() + .unwrap(); + let subject = Name::from_der(&subject).unwrap(); + let pub_key = + SubjectPublicKeyInfoOwned::try_from(RSA_2048_DER_EXAMPLE).expect("get rsa pub key"); + + let mut signer = ecdsa_signer(); + let mut builder = CertificateBuilder::new::>( + profile, + Version::V3, + serial_number, + validity, + subject, + pub_key, + &mut signer, + ) + .expect("Create certificate"); + + let certificate = builder.build::>().unwrap(); + + let pem = certificate.to_pem(LineEnding::LF).expect("generate pem"); + println!("{}", openssl::check_certificate(pem.as_bytes())); + + // TODO(baloo): not too sure we should tackle those in this API. + let ignored = &[ + "e_sub_cert_aia_missing", + "e_sub_cert_crl_distribution_points_missing", + "w_sub_cert_aia_does_not_contain_issuing_ca_url", + // Missing policies + "e_sub_cert_certificate_policies_missing", + "e_sub_cert_cert_policy_empty", + // Needs to be added by the end-user + "e_sub_cert_aia_does_not_contain_ocsp_url", + // SAN needs to include DNS name (if used) + "e_ext_san_missing", + "e_subject_common_name_not_exactly_from_san", + // Extended key usage needs to be added by end-user and is use-case dependent + "e_sub_cert_eku_missing", + ]; + + zlint::check_certificate(pem.as_bytes(), ignored); +} + +const RSA_2048_PRIV_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-priv.der"); + +fn rsa_signer() -> SigningKey { + let private_key = rsa::RsaPrivateKey::from_pkcs1_der(RSA_2048_PRIV_DER_EXAMPLE).unwrap(); + let signing_key = SigningKey::::new_with_prefix(private_key); + signing_key +} + +const PKCS8_PRIVATE_KEY_DER: &[u8] = include_bytes!("examples/p256-priv.der"); + +fn ecdsa_signer() -> ecdsa::SigningKey { + let secret_key = p256::SecretKey::from_pkcs8_der(PKCS8_PRIVATE_KEY_DER).unwrap(); + ecdsa::SigningKey::from(secret_key) +} diff --git a/x509-cert/tests/examples/p256-priv.der b/x509-cert/tests/examples/p256-priv.der new file mode 100644 index 000000000..c0de45ef2 Binary files /dev/null and b/x509-cert/tests/examples/p256-priv.der differ diff --git a/x509-cert/tests/examples/rsa2048-priv.der b/x509-cert/tests/examples/rsa2048-priv.der new file mode 100644 index 000000000..bbf18768c Binary files /dev/null and b/x509-cert/tests/examples/rsa2048-priv.der differ diff --git a/x509-cert/tests/examples/rsa2048-pub.der b/x509-cert/tests/examples/rsa2048-pub.der new file mode 100644 index 000000000..4148aaaaa Binary files /dev/null and b/x509-cert/tests/examples/rsa2048-pub.der differ