From d2a24d5102ef49e4e18ee2beaf3a1cef139df382 Mon Sep 17 00:00:00 2001 From: Andrew Walbran Date: Fri, 15 Dec 2023 16:04:54 +0000 Subject: Upgrade x509-cert to 0.2.4 This project was upgraded with external_updater. Usage: tools/external_updater/updater.sh update rust/crates/x509-cert For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md Test: TreeHugger Change-Id: Ia6bd8174dee938fcdc202d0ad165eae5901ba486 --- .cargo_vcs_info.json | 2 +- Android.bp | 8 +- CHANGELOG.md | 125 +++++ Cargo.toml | 77 ++- Cargo.toml.orig | 41 +- METADATA | 21 +- README.md | 4 +- cargo_embargo.json | 4 +- src/anchor.rs | 48 +- src/attr.rs | 134 ++++-- src/builder.rs | 526 +++++++++++++++++++++ src/certificate.rs | 164 +++++-- src/crl.rs | 29 +- src/ext.rs | 33 +- src/ext/pkix.rs | 56 ++- src/ext/pkix/access.rs | 18 +- src/ext/pkix/authkeyid.rs | 24 +- src/ext/pkix/certpolicy.rs | 43 +- src/ext/pkix/constraints/basic.rs | 25 + src/ext/pkix/constraints/name.rs | 16 +- src/ext/pkix/constraints/policy.rs | 2 + src/ext/pkix/crl.rs | 36 +- src/ext/pkix/crl/dp.rs | 16 +- src/ext/pkix/keyusage.rs | 119 ++++- src/ext/pkix/name/dirstr.rs | 11 +- src/ext/pkix/name/dp.rs | 6 +- src/ext/pkix/name/ediparty.rs | 6 +- src/ext/pkix/name/general.rs | 66 ++- src/ext/pkix/name/other.rs | 12 +- src/ext/pkix/policymap.rs | 1 + src/lib.rs | 8 +- src/macros.rs | 60 ++- src/name.rs | 110 +++-- src/request.rs | 139 ++++-- src/serial_number.rs | 195 ++++++++ src/time.rs | 48 +- tests/builder.rs | 315 ++++++++++++ tests/certificate.rs | 196 ++++++-- tests/certreq.rs | 19 +- ...6EDA6FA1EDFA8C253936C75B5EEBD954BFF452.fake.pem | 61 +++ .../28903a635b5280fae6774c0b6da7d6baa64af2e8.der | Bin 0 -> 1370 bytes tests/examples/crates.io-chain.pem | 122 +++++ tests/examples/p256-priv.der | Bin 0 -> 138 bytes tests/examples/p256-pub.der | Bin 0 -> 91 bytes tests/examples/qualcomm.pem | 16 + tests/examples/rsa2048-priv.der | Bin 0 -> 1191 bytes tests/examples/rsa2048-pub.der | Bin 0 -> 294 bytes tests/general_name.rs | 4 +- tests/name.rs | 138 ++++-- tests/pkix_extensions.rs | 239 +++++++--- tests/trust_anchor_format.rs | 126 +++-- tests/validity.rs | 22 +- 52 files changed, 2901 insertions(+), 590 deletions(-) create mode 100644 src/builder.rs create mode 100644 src/serial_number.rs create mode 100644 tests/builder.rs create mode 100644 tests/examples/026EDA6FA1EDFA8C253936C75B5EEBD954BFF452.fake.pem create mode 100644 tests/examples/28903a635b5280fae6774c0b6da7d6baa64af2e8.der create mode 100644 tests/examples/crates.io-chain.pem create mode 100644 tests/examples/p256-priv.der create mode 100644 tests/examples/p256-pub.der create mode 100644 tests/examples/qualcomm.pem create mode 100644 tests/examples/rsa2048-priv.der create mode 100644 tests/examples/rsa2048-pub.der diff --git a/.cargo_vcs_info.json b/.cargo_vcs_info.json index 271baca..ec6a5a4 100644 --- a/.cargo_vcs_info.json +++ b/.cargo_vcs_info.json @@ -1,6 +1,6 @@ { "git": { - "sha1": "5747b144d21463fe471a2aaa34a5a1bd67b611f2" + "sha1": "dc5a611afb6d3aa7b9b9bfae3587f996bb25fdc6" }, "path_in_vcs": "x509-cert" } \ No newline at end of file diff --git a/Android.bp b/Android.bp index 0715dd3..01da6f9 100644 --- a/Android.bp +++ b/Android.bp @@ -35,14 +35,12 @@ rust_library_host { name: "libx509_cert", crate_name: "x509_cert", cargo_env_compat: true, - cargo_pkg_version: "0.1.1", + cargo_pkg_version: "0.2.4", srcs: ["src/lib.rs"], edition: "2021", - features: ["alloc"], rustlibs: [ "libconst_oid", "libder", - "libflagset", "libspki", ], } @@ -51,14 +49,12 @@ rust_library_rlib { name: "libx509_cert_nostd", crate_name: "x509_cert", cargo_env_compat: true, - cargo_pkg_version: "0.1.1", + cargo_pkg_version: "0.2.4", srcs: ["src/lib.rs"], edition: "2021", - features: ["alloc"], rustlibs: [ "libconst_oid_nostd", "libder_nostd", - "libflagset_nostd", "libspki_nostd", ], apex_available: [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 83406c9..9922da4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,131 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.2.4 (2023-07-25) + +### Added +- `add_attribute` to `RequestBuilder` ([#1137]) + +### Changed +- bump `serde_json` from 1.0.97 to 1.0.99 ([#1122]) +- use the shortest name when looking attr OID ([#1130]) +- bump `serde_json` from 1.0.100 to 1.0.103 ([#1158]) + +### Fixed +- RDN string representation ([#1126]) +- `Arbitrary` for certificates ([#1150]) + +[#1122]: https://github.com/RustCrypto/formats/pull/1122 +[#1126]: https://github.com/RustCrypto/formats/pull/1126 +[#1130]: https://github.com/RustCrypto/formats/pull/1130 +[#1137]: https://github.com/RustCrypto/formats/pull/1137 +[#1150]: https://github.com/RustCrypto/formats/pull/1150 +[#1158]: https://github.com/RustCrypto/formats/pull/1158 + +## 0.2.3 (2023-05-30) + +### Added +- Added `TryFrom` for `RelativeDistinguishedName` ([#1092]) +- Load a chain of certificates from a slice ([#1081]) + +[#1092]: https://github.com/RustCrypto/formats/pull/1092 +[#1081]: https://github.com/RustCrypto/formats/pull/1081 + +## 0.2.2 (2023-05-19) + +### Added +- Certificate builder ([#764]) +- Support for `RandomizedSigner` in builder ([#1007]) +- Provide parsing profiles ([#987]) +- Support for `Time::INFINITY` ([#1024]) +- Conversion from `std::net::IpAddr` ([#1035]) +- `CertReq` builder ([#1034]) +- missing extension implementations ([#1050]) +- notes about `UTCTime` range being 1970-2049 ([#1052]) +- consume the `SignatureBitStringEncoding` trait ([#1048]) + +### Changed +- use `ErrorKind::Value` for overlength serial ([#988]) +- Bump `hex-literal` to v0.4.1 ([#999]) +- Builder updates ([#1001]) +- better debug info when `zlint` isn't installed ([#1018]) +- make SKI optional in leaf certificate ([#1028]) +- bump rsa from 0.9.0-pre.2 to 0.9.0 ([#1033]) +- bump rsa from 0.9.1 to 0.9.2 ([#1056]) + +### Fixed +- fix `KeyUsage` bit tests ([#993]) +- extraneous PhantomData in `TbsCertificate` ([#1017]) +- CI flakiness ([#1042]) +- usage of ecdsa signer ([#1043]) + +[#764]: https://github.com/RustCrypto/formats/pull/764 +[#987]: https://github.com/RustCrypto/formats/pull/987 +[#988]: https://github.com/RustCrypto/formats/pull/988 +[#993]: https://github.com/RustCrypto/formats/pull/993 +[#999]: https://github.com/RustCrypto/formats/pull/999 +[#1001]: https://github.com/RustCrypto/formats/pull/1001 +[#1007]: https://github.com/RustCrypto/formats/pull/1007 +[#1017]: https://github.com/RustCrypto/formats/pull/1017 +[#1018]: https://github.com/RustCrypto/formats/pull/1018 +[#1024]: https://github.com/RustCrypto/formats/pull/1024 +[#1028]: https://github.com/RustCrypto/formats/pull/1028 +[#1033]: https://github.com/RustCrypto/formats/pull/1033 +[#1034]: https://github.com/RustCrypto/formats/pull/1034 +[#1035]: https://github.com/RustCrypto/formats/pull/1035 +[#1042]: https://github.com/RustCrypto/formats/pull/1042 +[#1043]: https://github.com/RustCrypto/formats/pull/1043 +[#1048]: https://github.com/RustCrypto/formats/pull/1048 +[#1050]: https://github.com/RustCrypto/formats/pull/1050 +[#1052]: https://github.com/RustCrypto/formats/pull/1052 +[#1056]: https://github.com/RustCrypto/formats/pull/1056 + +## 0.2.1 (2023-03-26) +### Added +- `FromStr` impls for `RdnSequence` (`Name`), `RelativeDistinguishedName`, and + `AttributeTypeAndValue` ([#949]) + +### Changed +- Deprecate `encode_from_string` functions ([#951]) + +[#949]: https://github.com/RustCrypto/formats/pull/949 +[#951]: https://github.com/RustCrypto/formats/pull/951 + +## 0.2.0 (2023-03-18) +### Added +- Feature-gated `Arbitrary` impl for `Certificate` ([#761]) +- Allow request to be serialized to PEM ([#819]) +- `Display` impl for `SerialNumber` ([#820]) +- `std` feature implies `const-oid/std` ([#874]) + +### Changed +- Serial numbers are formatted as `PrintableString` ([#794]) +- `SerialNumber` is now a specialized object ([#795]) +- MSRV 1.65 ([#805]) +- Make types owned instead of reference-based ([#806], [#841]) +- Bump `der` to v0.7 ([#899]) +- Bump `spki` to v0.7 ([#900]) + +### Fixed +- Handling of negative serial numbers ([#823], [#831]) + +### Removed +- `alloc` feature: now unconditionally required ([#841]) + +[#761]: https://github.com/RustCrypto/formats/pull/761 +[#794]: https://github.com/RustCrypto/formats/pull/794 +[#795]: https://github.com/RustCrypto/formats/pull/795 +[#805]: https://github.com/RustCrypto/formats/pull/805 +[#806]: https://github.com/RustCrypto/formats/pull/806 +[#819]: https://github.com/RustCrypto/formats/pull/819 +[#820]: https://github.com/RustCrypto/formats/pull/820 +[#823]: https://github.com/RustCrypto/formats/pull/823 +[#831]: https://github.com/RustCrypto/formats/pull/831 +[#841]: https://github.com/RustCrypto/formats/pull/841 +[#874]: https://github.com/RustCrypto/formats/pull/874 +[#899]: https://github.com/RustCrypto/formats/pull/899 +[#900]: https://github.com/RustCrypto/formats/pull/900 + ## 0.1.1 (2022-12-10) ### Added - Support `TeletexString` in `DirectoryString` ([#692]) diff --git a/Cargo.toml b/Cargo.toml index 43ad5a1..793721f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,9 @@ [package] edition = "2021" -rust-version = "1.56" +rust-version = "1.65" name = "x509-cert" -version = "0.1.1" +version = "0.2.4" authors = ["RustCrypto Developers"] description = """ Pure Rust implementation of the X.509 Public Key Infrastructure Certificate @@ -37,37 +37,90 @@ rustdoc-args = [ "docsrs", ] +[dependencies.arbitrary] +version = "1.3" +features = ["derive"] +optional = true + [dependencies.const-oid] -version = "0.9" +version = "0.9.3" features = ["db"] [dependencies.der] -version = "0.6" +version = "0.7.6" features = [ - "derive", "alloc", + "derive", "flagset", + "oid", ] -[dependencies.flagset] -version = "0.4.3" +[dependencies.sha1] +version = "0.10.0" +optional = true + +[dependencies.signature] +version = "2.1.0" +features = ["rand_core"] +optional = true [dependencies.spki] -version = "0.6" +version = "0.7.2" +features = ["alloc"] + +[dev-dependencies.ecdsa] +version = "0.16.7" +features = [ + "digest", + "pem", +] [dev-dependencies.hex-literal] -version = "0.3" +version = "0.4" + +[dev-dependencies.p256] +version = "0.13.0" + +[dev-dependencies.rand] +version = "0.8.5" + +[dev-dependencies.rsa] +version = "0.9.2" +features = ["sha2"] [dev-dependencies.rstest] -version = "0.12" +version = "0.17" + +[dev-dependencies.sha2] +version = "0.10" +features = ["oid"] + +[dev-dependencies.tempfile] +version = "3.5.0" [features] -alloc = ["der/alloc"] +arbitrary = [ + "dep:arbitrary", + "std", + "der/arbitrary", + "spki/arbitrary", +] +builder = [ + "std", + "sha1/default", + "signature", +] +default = [ + "pem", + "std", +] +hazmat = [] pem = [ - "alloc", "der/pem", + "spki/pem", ] std = [ + "const-oid/std", "der/std", "spki/std", ] diff --git a/Cargo.toml.orig b/Cargo.toml.orig index 723504f..717379f 100644 --- a/Cargo.toml.orig +++ b/Cargo.toml.orig @@ -1,6 +1,6 @@ [package] name = "x509-cert" -version = "0.1.1" +version = "0.2.4" description = """ Pure Rust implementation of the X.509 Public Key Infrastructure Certificate format as described in RFC 5280 @@ -12,26 +12,37 @@ categories = ["cryptography", "data-structures", "encoding", "no-std"] keywords = ["crypto"] readme = "README.md" edition = "2021" -rust-version = "1.56" +rust-version = "1.65" [dependencies] -const-oid = { version = "0.9", features = ["db"], path = "../const-oid" } -der = { version = "0.6", features = ["derive", "alloc", "flagset"], path = "../der" } -flagset = { version = "0.4.3" } -spki = { version = "0.6", path = "../spki" } +const-oid = { version = "0.9.3", features = ["db"] } +der = { version = "0.7.6", features = ["alloc", "derive", "flagset", "oid"] } +spki = { version = "0.7.2", features = ["alloc"] } -[dev-dependencies] -hex-literal = "0.3" +# optional dependencies +arbitrary = { version = "1.3", features = ["derive"], optional = true } +sha1 = { version = "0.10.0", optional = true } +signature = { version = "2.1.0", features = ["rand_core"], optional = true } -# NOTE: upgrading requires MSRV bumps -# - rstest v0.13 requires 1.59 (for `async-global-executor`) -# - rstest v0.14 requires a workspace-wide 1.60 MSRV (for namespaced features) -rstest = "0.12" +[dev-dependencies] +hex-literal = "0.4" +rand = "0.8.5" +rsa = { version = "0.9.2", features = ["sha2"] } +ecdsa = { version = "0.16.7", 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] -alloc = ["der/alloc"] -std = ["der/std", "spki/std"] -pem = ["alloc", "der/pem"] +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] all-features = true diff --git a/METADATA b/METADATA index c38c1c0..fb2e5f3 100644 --- a/METADATA +++ b/METADATA @@ -1,23 +1,20 @@ # This project was upgraded with external_updater. # Usage: tools/external_updater/updater.sh update rust/crates/x509-cert -# For more info, check https://cs.android.com/android/platform/superproject/+/master:tools/external_updater/README.md +# For more info, check https://cs.android.com/android/platform/superproject/+/main:tools/external_updater/README.md name: "x509-cert" description: "Pure Rust implementation of the X.509 Public Key Infrastructure Certificate format as described in RFC 5280." third_party { - url { - type: HOMEPAGE - value: "https://crates.io/crates/x509-cert" - } - url { - type: ARCHIVE - value: "https://static.crates.io/crates/x509-cert/x509-cert-0.1.1.crate" - } - version: "0.1.1" license_type: NOTICE last_upgrade_date { - year: 2022 + year: 2023 month: 12 - day: 19 + day: 15 + } + homepage: "https://crates.io/crates/x509-cert" + identifier { + type: "Archive" + value: "https://static.crates.io/crates/x509-cert/x509-cert-0.2.4.crate" + version: "0.2.4" } } diff --git a/README.md b/README.md index e439c07..e131c7d 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ format as described in [RFC 5280]. ## Minimum Supported Rust Version -This crate requires **Rust 1.57** at a minimum. +This crate requires **Rust 1.65** at a minimum. We may change the MSRV in the future, but it will be accompanied by a minor version bump. @@ -43,7 +43,7 @@ dual licensed as above, without any additional terms or conditions. [build-image]: https://github.com/RustCrypto/formats/actions/workflows/x509-cert.yml/badge.svg [build-link]: https://github.com/RustCrypto/formats/actions/workflows/x509-cert.yml [license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg -[rustc-image]: https://img.shields.io/badge/rustc-1.57+-blue.svg +[rustc-image]: https://img.shields.io/badge/rustc-1.65+-blue.svg [chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg [chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/300570-formats diff --git a/cargo_embargo.json b/cargo_embargo.json index 9351aed..e6f2c7a 100644 --- a/cargo_embargo.json +++ b/cargo_embargo.json @@ -3,9 +3,7 @@ "//apex_available:platform", "com.android.virt" ], - "features": [ - "alloc" - ], + "features": [], "run_cargo": false, "variants": [ { diff --git a/src/anchor.rs b/src/anchor.rs index ad7ef08..bc3a2fb 100644 --- a/src/anchor.rs +++ b/src/anchor.rs @@ -4,26 +4,22 @@ use crate::ext::pkix::{certpolicy::CertificatePolicies, NameConstraints}; use crate::{ext::Extensions, name::Name}; use crate::{Certificate, TbsCertificate}; -use der::asn1::{OctetStringRef, Utf8StringRef}; +use alloc::string::String; +use der::asn1::OctetString; +use der::flagset::{flags, FlagSet}; use der::{Choice, Enumerated, Sequence}; -use flagset::{flags, FlagSet}; -use spki::SubjectPublicKeyInfo; +use spki::SubjectPublicKeyInfoOwned; /// Version identifier for TrustAnchorInfo -#[derive(Clone, Debug, Copy, PartialEq, Eq, Enumerated)] +#[derive(Clone, Debug, Default, Copy, PartialEq, Eq, Enumerated)] #[asn1(type = "INTEGER")] #[repr(u8)] pub enum Version { /// Version 1 (default) + #[default] V1 = 0, } -impl Default for Version { - fn default() -> Self { - Version::V1 - } -} - /// ```text /// TrustAnchorInfo ::= SEQUENCE { /// version TrustAnchorInfoVersion DEFAULT v1, @@ -41,25 +37,25 @@ impl Default for Version { /// ``` #[derive(Clone, Debug, PartialEq, Eq, Sequence)] #[allow(missing_docs)] -pub struct TrustAnchorInfo<'a> { +pub struct TrustAnchorInfo { #[asn1(default = "Default::default")] pub version: Version, - pub pub_key: SubjectPublicKeyInfo<'a>, + pub pub_key: SubjectPublicKeyInfoOwned, - pub key_id: OctetStringRef<'a>, + pub key_id: OctetString, #[asn1(optional = "true")] - pub ta_title: Option>, + pub ta_title: Option, #[asn1(optional = "true")] - pub cert_path: Option>, + pub cert_path: Option, #[asn1(context_specific = "1", tag_mode = "EXPLICIT", optional = "true")] - pub extensions: Option>, + pub extensions: Option, #[asn1(context_specific = "2", tag_mode = "IMPLICIT", optional = "true")] - pub ta_title_lang_tag: Option>, + pub ta_title_lang_tag: Option, } /// ```text @@ -74,20 +70,20 @@ pub struct TrustAnchorInfo<'a> { /// ``` #[derive(Clone, Debug, Eq, PartialEq, Sequence)] #[allow(missing_docs)] -pub struct CertPathControls<'a> { - pub ta_name: Name<'a>, +pub struct CertPathControls { + pub ta_name: Name, #[asn1(context_specific = "0", tag_mode = "IMPLICIT", optional = "true")] - pub certificate: Option>, + pub certificate: Option, #[asn1(context_specific = "1", tag_mode = "IMPLICIT", optional = "true")] - pub policy_set: Option>, + pub policy_set: Option, #[asn1(context_specific = "2", tag_mode = "IMPLICIT", optional = "true")] pub policy_flags: Option, #[asn1(context_specific = "3", tag_mode = "IMPLICIT", optional = "true")] - pub name_constr: Option>, + pub name_constr: Option, #[asn1(context_specific = "4", tag_mode = "IMPLICIT", optional = "true")] pub path_len_constraint: Option, @@ -128,12 +124,12 @@ pub type CertPolicyFlags = FlagSet; #[derive(Clone, Debug, PartialEq, Eq, Choice)] #[allow(clippy::large_enum_variant)] #[allow(missing_docs)] -pub enum TrustAnchorChoice<'a> { - Certificate(Certificate<'a>), +pub enum TrustAnchorChoice { + Certificate(Certificate), #[asn1(context_specific = "1", tag_mode = "EXPLICIT", constructed = "true")] - TbsCertificate(TbsCertificate<'a>), + TbsCertificate(TbsCertificate), #[asn1(context_specific = "2", tag_mode = "EXPLICIT", constructed = "true")] - TaInfo(TrustAnchorInfo<'a>), + TaInfo(TrustAnchorInfo), } diff --git a/src/attr.rs b/src/attr.rs index 2b4336a..d4cccc3 100644 --- a/src/attr.rs +++ b/src/attr.rs @@ -1,12 +1,21 @@ //! Attribute-related definitions as defined in X.501 (and updated by RFC 5280). use alloc::vec::Vec; -use const_oid::db::rfc4519::{COUNTRY_NAME, DOMAIN_COMPONENT}; -use core::fmt::{self, Write}; - -use const_oid::db::DB; -use der::asn1::{AnyRef, ObjectIdentifier, SetOfVec}; -use der::{Decode, Encode, Error, ErrorKind, Sequence, Tag, Tagged, ValueOrd}; +use const_oid::db::{ + rfc4519::{COUNTRY_NAME, DOMAIN_COMPONENT, SERIAL_NUMBER}, + Database, DB, +}; +use core::{ + fmt::{self, Write}, + str::FromStr, +}; +use der::{ + asn1::{ + Any, Ia5StringRef, ObjectIdentifier, PrintableStringRef, SetOfVec, TeletexStringRef, + Utf8StringRef, + }, + Decode, Encode, Error, ErrorKind, Sequence, Tag, Tagged, ValueOrd, +}; /// X.501 `AttributeType` as defined in [RFC 5280 Appendix A.1]. /// @@ -24,7 +33,7 @@ pub type AttributeType = ObjectIdentifier; /// ``` /// /// [RFC 5280 Appendix A.1]: https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1 -pub type AttributeValue<'a> = AnyRef<'a>; +pub type AttributeValue = Any; /// X.501 `Attribute` as defined in [RFC 5280 Appendix A.1]. /// @@ -50,17 +59,9 @@ pub type AttributeValue<'a> = AnyRef<'a>; /// [RFC 5280 Appendix A.1]: https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1 #[derive(Clone, Debug, PartialEq, Eq, Sequence, ValueOrd)] #[allow(missing_docs)] -pub struct Attribute<'a> { +pub struct Attribute { pub oid: AttributeType, - pub values: SetOfVec>, -} - -impl<'a> TryFrom<&'a [u8]> for Attribute<'a> { - type Error = Error; - - fn try_from(bytes: &'a [u8]) -> Result { - Self::from_der(bytes) - } + pub values: SetOfVec, } /// X.501 `Attributes` as defined in [RFC 2986 Section 4]. @@ -70,7 +71,7 @@ impl<'a> TryFrom<&'a [u8]> for Attribute<'a> { /// ``` /// /// [RFC 2986 Section 4]: https://datatracker.ietf.org/doc/html/rfc2986#section-4 -pub type Attributes<'a> = SetOfVec>; +pub type Attributes = SetOfVec; /// X.501 `AttributeTypeAndValue` as defined in [RFC 5280 Appendix A.1]. /// @@ -82,11 +83,12 @@ pub type Attributes<'a> = SetOfVec>; /// ``` /// /// [RFC 5280 Appendix A.1]: https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1 -#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Sequence, ValueOrd)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Sequence, ValueOrd)] #[allow(missing_docs)] -pub struct AttributeTypeAndValue<'a> { +pub struct AttributeTypeAndValue { pub oid: AttributeType, - pub value: AnyRef<'a>, + pub value: AttributeValue, } #[derive(Copy, Clone)] @@ -141,9 +143,9 @@ impl Parser { } } -impl AttributeTypeAndValue<'_> { +impl AttributeTypeAndValue { /// Parses the hex value in the `OID=#HEX` format. - fn encode_hex(oid: ObjectIdentifier, val: &str) -> Result, Error> { + fn from_hex(oid: ObjectIdentifier, val: &str) -> Result { // Ensure an even number of hex bytes. let mut iter = match val.len() % 2 { 0 => [].iter().cloned().chain(val.bytes()), @@ -153,6 +155,7 @@ impl AttributeTypeAndValue<'_> { // Decode der bytes from hex. let mut bytes = Vec::with_capacity((val.len() + 1) / 2); + while let (Some(h), Some(l)) = (iter.next(), iter.next()) { let mut byte = 0u8; @@ -168,14 +171,14 @@ impl AttributeTypeAndValue<'_> { bytes.push(byte); } - // Serialize. - let value = AnyRef::from_der(&bytes)?; - let atv = AttributeTypeAndValue { oid, value }; - atv.to_vec() + Ok(Self { + oid, + value: Any::from_der(&bytes)?, + }) } /// Parses the string value in the `NAME=STRING` format. - fn encode_str(oid: ObjectIdentifier, val: &str) -> Result, Error> { + fn from_delimited_str(oid: ObjectIdentifier, val: &str) -> Result { // Undo escaping. let mut parser = Parser::new(); for c in val.bytes() { @@ -185,13 +188,16 @@ impl AttributeTypeAndValue<'_> { let tag = match oid { COUNTRY_NAME => Tag::PrintableString, DOMAIN_COMPONENT => Tag::Ia5String, + // Serial numbers are formatted as Printable String as per RFC 5280 Appendix A.1: + // https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1 + SERIAL_NUMBER => Tag::PrintableString, _ => Tag::Utf8String, }; - // Serialize. - let value = AnyRef::new(tag, parser.as_bytes())?; - let atv = AttributeTypeAndValue { oid, value }; - atv.to_vec() + Ok(Self { + oid, + value: Any::new(tag, parser.as_bytes())?, + }) } /// Converts an AttributeTypeAndValue string into an encoded AttributeTypeAndValue @@ -199,7 +205,24 @@ impl AttributeTypeAndValue<'_> { /// This function follows the rules in [RFC 4514]. /// /// [RFC 4514]: https://datatracker.ietf.org/doc/html/rfc4514 + #[deprecated( + since = "0.2.1", + note = "use AttributeTypeAndValue::from_str(...)?.to_der()" + )] pub fn encode_from_string(s: &str) -> Result, Error> { + Self::from_str(s)?.to_der() + } +} + +/// Parse an [`AttributeTypeAndValue`] string. +/// +/// This function follows the rules in [RFC 4514]. +/// +/// [RFC 4514]: https://datatracker.ietf.org/doc/html/rfc4514 +impl FromStr for AttributeTypeAndValue { + type Err = Error; + + fn from_str(s: &str) -> der::Result { let idx = s.find('=').ok_or_else(|| Error::from(ErrorKind::Failed))?; let (key, val) = s.split_at(idx); let val = &val[1..]; @@ -212,8 +235,8 @@ impl AttributeTypeAndValue<'_> { // If the value is hex-encoded DER... match val.strip_prefix('#') { - Some(val) => Self::encode_hex(oid, val), - None => Self::encode_str(oid, val), + Some(val) => Self::from_hex(oid, val), + None => Self::from_delimited_str(oid, val), } } } @@ -221,17 +244,23 @@ impl AttributeTypeAndValue<'_> { /// Serializes the structure according to the rules in [RFC 4514]. /// /// [RFC 4514]: https://datatracker.ietf.org/doc/html/rfc4514 -impl fmt::Display for AttributeTypeAndValue<'_> { +impl fmt::Display for AttributeTypeAndValue { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let val = match self.value.tag() { - Tag::PrintableString => self.value.printable_string().ok().map(|s| s.as_str()), - Tag::Utf8String => self.value.utf8_string().ok().map(|s| s.as_str()), - Tag::Ia5String => self.value.ia5_string().ok().map(|s| s.as_str()), - Tag::TeletexString => self.value.teletex_string().ok().map(|s| s.as_str()), + Tag::PrintableString => PrintableStringRef::try_from(&self.value) + .ok() + .map(|s| s.as_str()), + Tag::Utf8String => Utf8StringRef::try_from(&self.value) + .ok() + .map(|s| s.as_str()), + Tag::Ia5String => Ia5StringRef::try_from(&self.value).ok().map(|s| s.as_str()), + Tag::TeletexString => TeletexStringRef::try_from(&self.value) + .ok() + .map(|s| s.as_str()), _ => None, }; - if let (Some(key), Some(val)) = (DB.by_oid(&self.oid), val) { + if let (Some(key), Some(val)) = (DB.shortest_name_by_oid(&self.oid), val) { write!(f, "{}=", key.to_ascii_uppercase())?; let mut iter = val.char_indices().peekable(); @@ -245,7 +274,7 @@ impl fmt::Display for AttributeTypeAndValue<'_> { } } } else { - let value = self.value.to_vec().or(Err(fmt::Error))?; + let value = self.value.to_der().or(Err(fmt::Error))?; write!(f, "{}=#", self.oid)?; for c in value { @@ -256,3 +285,26 @@ impl fmt::Display for AttributeTypeAndValue<'_> { Ok(()) } } + +/// Helper trait to bring shortest name by oid lookups to Database +trait ShortestName { + fn shortest_name_by_oid(&self, oid: &ObjectIdentifier) -> Option<&str>; +} + +impl<'a> ShortestName for Database<'a> { + fn shortest_name_by_oid(&self, oid: &ObjectIdentifier) -> Option<&'a str> { + let mut best_match: Option<&'a str> = None; + + for m in self.find_names_for_oid(*oid) { + if let Some(previous) = best_match { + if m.len() < previous.len() { + best_match = Some(m); + } + } else { + best_match = Some(m); + } + } + + best_match + } +} diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..ec51a7e --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,526 @@ +//! X509 Certificate builder + +use alloc::vec; +use core::fmt; +use der::{asn1::BitString, referenced::OwnedToRef, Encode}; +use signature::{rand_core::CryptoRngCore, Keypair, RandomizedSigner, Signer}; +use spki::{ + DynSignatureAlgorithmIdentifier, EncodePublicKey, SignatureBitStringEncoding, + SubjectPublicKeyInfoOwned, SubjectPublicKeyInfoRef, +}; + +use crate::{ + certificate::{Certificate, TbsCertificate, Version}, + ext::{ + pkix::{ + AuthorityKeyIdentifier, BasicConstraints, KeyUsage, KeyUsages, SubjectKeyIdentifier, + }, + AsExtension, Extension, Extensions, + }, + name::Name, + request::{attributes::AsAttribute, CertReq, CertReqInfo, ExtensionReq}, + 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::Error`] type. + Signature(signature::Error), +} + +#[cfg(feature = "std")] +impl std::error::Error for 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, + /// should the key encipherment flag of KeyUsage be enabled + enable_key_encipherment: bool, + /// should the subject key identifier extension be included + /// + /// From [RFC 5280 Section 4.2.1.2]: + /// For end entity certificates, subject key identifiers SHOULD be + /// derived from the public key. Two common methods for generating key + /// identifiers from the public key are identified above. + #[cfg(feature = "hazmat")] + include_subject_key_identifier: 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(); + + match self { + #[cfg(feature = "hazmat")] + Profile::Leaf { + include_subject_key_identifier: false, + .. + } => {} + _ => extensions.push( + SubjectKeyIdentifier::try_from(spk)?.to_extension(&tbs.subject, &extensions)?, + ), + } + + // Build Authority Key Identifier + match self { + Profile::Root => {} + _ => { + extensions.push( + AuthorityKeyIdentifier::try_from(issuer_spk.clone())? + .to_extension(&tbs.subject, &extensions)?, + ); + } + } + + // Build Basic Contraints extensions + extensions.push(match self { + Profile::Root => BasicConstraints { + ca: true, + path_len_constraint: None, + } + .to_extension(&tbs.subject, &extensions)?, + Profile::SubCA { + path_len_constraint, + .. + } => BasicConstraints { + ca: true, + path_len_constraint: *path_len_constraint, + } + .to_extension(&tbs.subject, &extensions)?, + Profile::Leaf { .. } => BasicConstraints { + ca: false, + path_len_constraint: None, + } + .to_extension(&tbs.subject, &extensions)?, + #[cfg(feature = "hazmat")] + Profile::Manual { .. } => unreachable!(), + }); + + // Build Key Usage extension + match self { + Profile::Root | Profile::SubCA { .. } => { + extensions.push( + KeyUsage(KeyUsages::KeyCertSign | KeyUsages::CRLSign) + .to_extension(&tbs.subject, &extensions)?, + ); + } + Profile::Leaf { + enable_key_agreement, + enable_key_encipherment, + .. + } => { + let mut key_usage = KeyUsages::DigitalSignature | KeyUsages::NonRepudiation; + if *enable_key_encipherment { + key_usage |= KeyUsages::KeyEncipherment; + } + if *enable_key_agreement { + key_usage |= KeyUsages::KeyAgreement; + } + + extensions.push(KeyUsage(key_usage).to_extension(&tbs.subject, &extensions)?); + } + #[cfg(feature = "hazmat")] + Profile::Manual { .. } => unreachable!(), + } + + Ok(extensions) + } +} + +/// X509 Certificate builder +/// +/// ``` +/// use der::Decode; +/// use x509_cert::spki::SubjectPublicKeyInfoOwned; +/// 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, +/// serial_number, +/// validity, +/// subject, +/// pub_key, +/// &signer, +/// ) +/// .expect("Create certificate"); +/// ``` +pub struct CertificateBuilder<'s, S> { + tbs: TbsCertificate, + extensions: Extensions, + cert_signer: &'s S, +} + +impl<'s, S> CertificateBuilder<'s, S> +where + S: Keypair + DynSignatureAlgorithmIdentifier, + S::VerifyingKey: EncodePublicKey, +{ + /// Creates a new certificate builder + pub fn new( + profile: Profile, + serial_number: SerialNumber, + mut validity: Validity, + subject: Name, + subject_public_key_info: SubjectPublicKeyInfoOwned, + cert_signer: &'s S, + ) -> Result { + let verifying_key = cert_signer.verifying_key(); + let signer_pub = verifying_key + .to_public_key_der()? + .decode_msg::()?; + + let signature_alg = cert_signer.signature_algorithm_identifier()?; + let issuer = profile.get_issuer(&subject); + + validity.not_before.rfc5280_adjust_utc_time()?; + validity.not_after.rfc5280_adjust_utc_time()?; + + let tbs = TbsCertificate { + version: Version::V3, + 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, + }; + + let extensions = profile.build_extensions( + tbs.subject_public_key_info.owned_to_ref(), + signer_pub.owned_to_ref(), + &tbs, + )?; + Ok(Self { + tbs, + extensions, + cert_signer, + }) + } + + /// Add an extension to this certificate + pub fn add_extension(&mut self, extension: &E) -> Result<()> { + let ext = extension.to_extension(&self.tbs.subject, &self.extensions)?; + self.extensions.push(ext); + + Ok(()) + } +} + +/// Builder for X509 Certificate Requests +/// +/// ``` +/// # use p256::{pkcs8::DecodePrivateKey, NistP256, ecdsa::DerSignature}; +/// # const PKCS8_PRIVATE_KEY_DER: &[u8] = include_bytes!("../tests/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) +/// # } +/// use x509_cert::{ +/// builder::{Builder, RequestBuilder}, +/// ext::pkix::{name::GeneralName, SubjectAltName}, +/// name::Name, +/// }; +/// use std::str::FromStr; +/// +/// use std::net::{IpAddr, Ipv4Addr}; +/// let subject = Name::from_str("CN=service.domination.world").unwrap(); +/// +/// let signer = ecdsa_signer(); +/// let mut builder = RequestBuilder::new(subject, &signer).expect("Create certificate request"); +/// builder +/// .add_extension(&SubjectAltName(vec![GeneralName::from(IpAddr::V4( +/// Ipv4Addr::new(192, 0, 2, 0), +/// ))])) +/// .unwrap(); +/// +/// let cert_req = builder.build::().unwrap(); +/// ``` +pub struct RequestBuilder<'s, S> { + info: CertReqInfo, + extension_req: ExtensionReq, + req_signer: &'s S, +} + +impl<'s, S> RequestBuilder<'s, S> +where + S: Keypair + DynSignatureAlgorithmIdentifier, + S::VerifyingKey: EncodePublicKey, +{ + /// Creates a new certificate request builder + pub fn new(subject: Name, req_signer: &'s S) -> Result { + let version = Default::default(); + let verifying_key = req_signer.verifying_key(); + let public_key = verifying_key + .to_public_key_der()? + .decode_msg::()?; + let attributes = Default::default(); + let extension_req = Default::default(); + + Ok(Self { + info: CertReqInfo { + version, + subject, + public_key, + attributes, + }, + extension_req, + req_signer, + }) + } + + /// Add an extension to this certificate request + pub fn add_extension(&mut self, extension: &E) -> Result<()> { + let ext = extension.to_extension(&self.info.subject, &self.extension_req.0)?; + + self.extension_req.0.push(ext); + + Ok(()) + } + + /// Add an attribute to this certificate request + pub fn add_attribute(&mut self, attribute: &A) -> Result<()> { + let attr = attribute.to_attribute()?; + + self.info.attributes.insert(attr)?; + Ok(()) + } +} + +/// Trait for X509 builders +/// +/// This trait defines the interface between builder and the signers. +pub trait Builder: Sized { + /// The builder's object signer + type Signer; + + /// Type built by this builder + type Output: Sized; + + /// Return a reference to the signer. + fn signer(&self) -> &Self::Signer; + + /// Assemble the final object from signature. + fn assemble(self, signature: BitString) -> Result; + + /// Finalize and return a serialization of the object for signature. + fn finalize(&mut self) -> der::Result>; + + /// Run the object through the signer and build it. + fn build(mut self) -> Result + where + Self::Signer: Signer, + Signature: SignatureBitStringEncoding, + { + let blob = self.finalize()?; + + let signature = self.signer().try_sign(&blob)?.to_bitstring()?; + + self.assemble(signature) + } + + /// Run the object through the signer and build it. + fn build_with_rng(mut self, rng: &mut impl CryptoRngCore) -> Result + where + Self::Signer: RandomizedSigner, + Signature: SignatureBitStringEncoding, + { + let blob = self.finalize()?; + + let signature = self + .signer() + .try_sign_with_rng(rng, &blob)? + .to_bitstring()?; + + self.assemble(signature) + } +} + +impl<'s, S> Builder for CertificateBuilder<'s, S> +where + S: Keypair + DynSignatureAlgorithmIdentifier, + S::VerifyingKey: EncodePublicKey, +{ + type Signer = S; + type Output = Certificate; + + fn signer(&self) -> &Self::Signer { + self.cert_signer + } + + fn finalize(&mut self) -> der::Result> { + if !self.extensions.is_empty() { + self.tbs.extensions = Some(self.extensions.clone()); + } + + if self.tbs.extensions.is_none() { + if self.tbs.issuer_unique_id.is_some() || self.tbs.subject_unique_id.is_some() { + self.tbs.version = Version::V2; + } else { + self.tbs.version = Version::V1; + } + } + + self.tbs.to_der() + } + + fn assemble(self, signature: BitString) -> Result { + let signature_algorithm = self.tbs.signature.clone(); + + Ok(Certificate { + tbs_certificate: self.tbs, + signature_algorithm, + signature, + }) + } +} + +impl<'s, S> Builder for RequestBuilder<'s, S> +where + S: Keypair + DynSignatureAlgorithmIdentifier, + S::VerifyingKey: EncodePublicKey, +{ + type Signer = S; + type Output = CertReq; + + fn signer(&self) -> &Self::Signer { + self.req_signer + } + + fn finalize(&mut self) -> der::Result> { + self.info + .attributes + .insert(self.extension_req.clone().try_into()?)?; + + self.info.to_der() + } + + fn assemble(self, signature: BitString) -> Result { + let algorithm = self.req_signer.signature_algorithm_identifier()?; + + Ok(CertReq { + info: self.info, + algorithm, + signature, + }) + } +} diff --git a/src/certificate.rs b/src/certificate.rs index cf71948..c5a105d 100644 --- a/src/certificate.rs +++ b/src/certificate.rs @@ -1,17 +1,55 @@ //! Certificate types -use crate::{name::Name, time::Validity}; - +use crate::{name::Name, serial_number::SerialNumber, time::Validity}; use alloc::vec::Vec; -use core::cmp::Ordering; - use const_oid::AssociatedOid; -use der::asn1::{BitStringRef, UIntRef}; -use der::{Decode, Enumerated, Error, ErrorKind, Sequence, ValueOrd}; -use spki::{AlgorithmIdentifier, SubjectPublicKeyInfo}; +use core::{cmp::Ordering, fmt::Debug}; +use der::asn1::BitString; +use der::{Decode, Enumerated, Error, ErrorKind, Sequence, Tag, ValueOrd}; +use spki::{AlgorithmIdentifierOwned, SubjectPublicKeyInfoOwned}; #[cfg(feature = "pem")] -use der::pem::PemLabel; +use der::{ + pem::{self, PemLabel}, + DecodePem, +}; + +/// [`Profile`] allows the consumer of this crate to customize the behavior when parsing +/// certificates. +/// By default, parsing will be made in a rfc5280-compliant manner. +pub trait Profile: PartialEq + Debug + Eq + Clone { + /// Checks to run when parsing serial numbers + fn check_serial_number(serial: &SerialNumber) -> der::Result<()> { + // See the note in `SerialNumber::new`: we permit lengths of 21 bytes here, + // since some X.509 implementations interpret the limit of 20 bytes to refer + // to the pre-encoded value. + if serial.inner.len() > SerialNumber::::MAX_DECODE_LEN { + Err(Tag::Integer.value_error()) + } else { + Ok(()) + } + } +} + +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Eq, Clone)] +/// Parse certificates with rfc5280-compliant checks +pub struct Rfc5280; + +impl Profile for Rfc5280 {} + +#[cfg(feature = "hazmat")] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] +#[derive(Debug, PartialEq, Eq, Clone)] +/// Parse raw x509 certificate and disable all the checks +pub struct Raw; + +#[cfg(feature = "hazmat")] +impl Profile for Raw { + fn check_serial_number(_serial: &SerialNumber) -> der::Result<()> { + Ok(()) + } +} /// Certificate `Version` as defined in [RFC 5280 Section 4.1]. /// @@ -20,6 +58,7 @@ use der::pem::PemLabel; /// ``` /// /// [RFC 5280 Section 4.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Clone, Debug, Copy, PartialEq, Eq, Enumerated)] #[asn1(type = "INTEGER")] #[repr(u8)] @@ -36,7 +75,7 @@ pub enum Version { impl ValueOrd for Version { fn value_cmp(&self, other: &Self) -> der::Result { - (&(*self as u8)).value_cmp(&(*other as u8)) + (*self as u8).value_cmp(&(*other as u8)) } } @@ -46,6 +85,9 @@ impl Default for Version { } } +/// X.509 `TbsCertificate` as defined in [RFC 5280 Section 4.1] +pub type TbsCertificate = TbsCertificateInner; + /// X.509 `TbsCertificate` as defined in [RFC 5280 Section 4.1] /// /// ASN.1 structure containing the names of the subject and issuer, a public @@ -71,9 +113,10 @@ impl Default for Version { /// ``` /// /// [RFC 5280 Section 4.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)] #[allow(missing_docs)] -pub struct TbsCertificate<'a> { +pub struct TbsCertificateInner { /// The certificate version /// /// Note that this value defaults to Version 1 per the RFC. However, @@ -83,32 +126,30 @@ pub struct TbsCertificate<'a> { #[asn1(context_specific = "0", default = "Default::default")] pub version: Version, - pub serial_number: UIntRef<'a>, - pub signature: AlgorithmIdentifier<'a>, - pub issuer: Name<'a>, + pub serial_number: SerialNumber

, + pub signature: AlgorithmIdentifierOwned, + pub issuer: Name, pub validity: Validity, - pub subject: Name<'a>, - pub subject_public_key_info: SubjectPublicKeyInfo<'a>, + pub subject: Name, + pub subject_public_key_info: SubjectPublicKeyInfoOwned, #[asn1(context_specific = "1", tag_mode = "IMPLICIT", optional = "true")] - pub issuer_unique_id: Option>, + pub issuer_unique_id: Option, #[asn1(context_specific = "2", tag_mode = "IMPLICIT", optional = "true")] - pub subject_unique_id: Option>, + pub subject_unique_id: Option, #[asn1(context_specific = "3", tag_mode = "EXPLICIT", optional = "true")] - pub extensions: Option>, + pub extensions: Option, } -impl<'a> TbsCertificate<'a> { +impl TbsCertificateInner

{ /// Decodes a single extension /// /// Returns an error if multiple of these extensions is present. Returns /// `Ok(None)` if the extension is not present. Returns a decoding error /// if decoding failed. Otherwise returns the extension. - pub fn get<'b: 'a, T: Decode<'a> + AssociatedOid>( - &'b self, - ) -> Result, Error> { + pub fn get<'a, T: Decode<'a> + AssociatedOid>(&'a self) -> Result, Error> { let mut iter = self.filter::().peekable(); match iter.next() { None => Ok(None), @@ -122,18 +163,23 @@ impl<'a> TbsCertificate<'a> { /// Filters extensions by an associated OID /// /// Returns a filtered iterator over all the extensions with the OID. - pub fn filter<'b: 'a, T: Decode<'a> + AssociatedOid>( - &'b self, - ) -> impl 'b + Iterator> { + pub fn filter<'a, T: Decode<'a> + AssociatedOid>( + &'a self, + ) -> impl 'a + Iterator> { self.extensions .as_deref() .unwrap_or(&[]) .iter() .filter(|e| e.extn_id == T::OID) - .map(|e| Ok((e.critical, T::from_der(e.extn_value)?))) + .map(|e| Ok((e.critical, T::from_der(e.extn_value.as_bytes())?))) } } +/// X.509 certificates are defined in [RFC 5280 Section 4.1]. +/// +/// [RFC 5280 Section 4.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1 +pub type Certificate = CertificateInner; + /// X.509 certificates are defined in [RFC 5280 Section 4.1]. /// /// ```text @@ -145,17 +191,17 @@ impl<'a> TbsCertificate<'a> { /// ``` /// /// [RFC 5280 Section 4.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)] #[allow(missing_docs)] -pub struct Certificate<'a> { - pub tbs_certificate: TbsCertificate<'a>, - pub signature_algorithm: AlgorithmIdentifier<'a>, - pub signature: BitStringRef<'a>, +pub struct CertificateInner { + pub tbs_certificate: TbsCertificateInner

, + pub signature_algorithm: AlgorithmIdentifierOwned, + pub signature: BitString, } #[cfg(feature = "pem")] -#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] -impl PemLabel for Certificate<'_> { +impl PemLabel for CertificateInner

{ const PEM_LABEL: &'static str = "CERTIFICATE"; } @@ -170,4 +216,56 @@ impl PemLabel for Certificate<'_> { /// ``` /// /// [RFC 6066]: https://datatracker.ietf.org/doc/html/rfc6066#section-10.1 -pub type PkiPath<'a> = Vec>; +pub type PkiPath = Vec; + +#[cfg(feature = "pem")] +impl CertificateInner

{ + /// Parse a chain of pem-encoded certificates from a slice. + /// + /// Returns the list of certificates. + pub fn load_pem_chain(mut input: &[u8]) -> Result, Error> { + fn find_boundary(haystack: &[T], needle: &[T]) -> Option + where + for<'a> &'a [T]: PartialEq, + { + haystack + .windows(needle.len()) + .position(|window| window == needle) + } + + let mut certs = Vec::new(); + let mut position: usize = 0; + + let end_boundary = &b"-----END CERTIFICATE-----"[..]; + + // Strip the trailing whitespaces + loop { + if input.is_empty() { + break; + } + let last_pos = input.len() - 1; + + match input.get(last_pos) { + Some(b'\r') | Some(b'\n') => { + input = &input[..last_pos]; + } + _ => break, + } + } + + while position < input.len() - 1 { + let rest = &input[position..]; + let end_pos = find_boundary(rest, end_boundary) + .ok_or(pem::Error::PostEncapsulationBoundary)? + + end_boundary.len(); + + let cert_buf = &rest[..end_pos]; + let cert = Self::from_pem(cert_buf)?; + certs.push(cert); + + position += end_pos; + } + + Ok(certs) + } +} diff --git a/src/crl.rs b/src/crl.rs index 09256f2..6916783 100644 --- a/src/crl.rs +++ b/src/crl.rs @@ -2,14 +2,15 @@ use crate::ext::Extensions; use crate::name::Name; +use crate::serial_number::SerialNumber; use crate::time::Time; use crate::Version; use alloc::vec::Vec; -use der::asn1::{BitStringRef, UIntRef}; +use der::asn1::BitString; use der::{Sequence, ValueOrd}; -use spki::AlgorithmIdentifier; +use spki::AlgorithmIdentifierOwned; /// `CertificateList` as defined in [RFC 5280 Section 5.1]. /// @@ -24,10 +25,10 @@ use spki::AlgorithmIdentifier; /// [RFC 5280 Section 5.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-5.1 #[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)] #[allow(missing_docs)] -pub struct CertificateList<'a> { - pub tbs_cert_list: TbsCertList<'a>, - pub signature_algorithm: AlgorithmIdentifier<'a>, - pub signature: BitStringRef<'a>, +pub struct CertificateList { + pub tbs_cert_list: TbsCertList, + pub signature_algorithm: AlgorithmIdentifierOwned, + pub signature: BitString, } /// Implicit intermediate structure from the ASN.1 definition of `TBSCertList`. @@ -46,10 +47,10 @@ pub struct CertificateList<'a> { /// [RFC 5280 Section 5.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-5.1 #[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)] #[allow(missing_docs)] -pub struct RevokedCert<'a> { - pub serial_number: UIntRef<'a>, +pub struct RevokedCert { + pub serial_number: SerialNumber, pub revocation_date: Time, - pub crl_entry_extensions: Option>, + pub crl_entry_extensions: Option, } /// `TbsCertList` as defined in [RFC 5280 Section 5.1]. @@ -73,14 +74,14 @@ pub struct RevokedCert<'a> { /// [RFC 5280 Section 5.1]: https://datatracker.ietf.org/doc/html/rfc5280#section-5.1 #[derive(Clone, Debug, Eq, PartialEq, Sequence, ValueOrd)] #[allow(missing_docs)] -pub struct TbsCertList<'a> { +pub struct TbsCertList { pub version: Version, - pub signature: AlgorithmIdentifier<'a>, - pub issuer: Name<'a>, + pub signature: AlgorithmIdentifierOwned, + pub issuer: Name, pub this_update: Time, pub next_update: Option

, +} + +impl SerialNumber

{ + /// Maximum length in bytes for a [`SerialNumber`] + pub const MAX_LEN: Length = Length::new(20); + + /// See notes in `SerialNumber::new` and `SerialNumber::decode_value`. + pub(crate) const MAX_DECODE_LEN: Length = Length::new(21); + + /// Create a new [`SerialNumber`] from a byte slice. + /// + /// The byte slice **must** represent a positive integer. + pub fn new(bytes: &[u8]) -> Result { + 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. + // RFC 5280 is ambiguous about whether this is valid, so we limit + // `SerialNumber` *encodings* to 20 bytes or fewer while permitting + // `SerialNumber` *decodings* to have up to 21 bytes below. + if inner.value_len()? > Self::MAX_LEN { + return Err(ErrorKind::Overlength.into()); + } + + Ok(Self { + inner: inner.into(), + _profile: PhantomData, + }) + } + + /// Borrow the inner byte slice which contains the least significant bytes + /// of a big endian integer value with all leading zeros stripped. + pub fn as_bytes(&self) -> &[u8] { + self.inner.as_bytes() + } +} + +impl EncodeValue for SerialNumber

{ + fn value_len(&self) -> Result { + self.inner.value_len() + } + + fn encode_value(&self, writer: &mut impl Writer) -> Result<()> { + self.inner.encode_value(writer) + } +} + +impl<'a, P: Profile> DecodeValue<'a> for SerialNumber

{ + fn decode_value>(reader: &mut R, header: Header) -> Result { + let inner = Int::decode_value(reader, header)?; + let serial = Self { + inner, + _profile: PhantomData, + }; + + P::check_serial_number(&serial)?; + + Ok(serial) + } +} + +impl FixedTag for SerialNumber

{ + const TAG: Tag = ::TAG; +} + +impl Display for SerialNumber { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + let mut iter = self.as_bytes().iter().peekable(); + + while let Some(byte) = iter.next() { + match iter.peek() { + Some(_) => write!(f, "{:02X}:", byte)?, + None => write!(f, "{:02X}", byte)?, + } + } + + Ok(()) + } +} + +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")] +impl<'a, P: Profile> arbitrary::Arbitrary<'a> for SerialNumber

{ + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + let len = u.int_in_range(0u32..=Self::MAX_LEN.into())?; + + Self::new(u.bytes(len as usize)?).map_err(|_| arbitrary::Error::IncorrectFormat) + } + + fn size_hint(depth: usize) -> (usize, Option) { + arbitrary::size_hint::and(u32::size_hint(depth), (0, None)) + } +} + +#[cfg(test)] +mod tests { + use alloc::string::ToString; + + use super::*; + + #[test] + fn serial_number_invariants() { + // Creating a new serial with an oversized encoding (due to high MSB) fails. + { + let too_big = [0x80; 20]; + assert!(SerialNumber::::new(&too_big).is_err()); + } + + // Creating a new serial with the maximum encoding succeeds. + { + let just_enough = [ + 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + ]; + assert!(SerialNumber::::new(&just_enough).is_ok()); + } + } + + #[test] + fn serial_number_display() { + { + let sn = SerialNumber::new(&[0x11, 0x22, 0x33]).unwrap(); + + assert_eq!(sn.to_string(), "11:22:33") + } + + { + let sn = SerialNumber::new(&[0xAA, 0xBB, 0xCC, 0x01, 0x10, 0x00, 0x11]).unwrap(); + + // We force the user's serial to be positive if they give us a negative one. + assert_eq!(sn.to_string(), "00:AA:BB:CC:01:10:00:11") + } + + { + let sn = SerialNumber::new(&[0x00, 0x00, 0x01]).unwrap(); + + // Leading zeroes are ignored, due to canonicalization. + assert_eq!(sn.to_string(), "01") + } + } +} diff --git a/src/time.rs b/src/time.rs index d1f2ec5..c0ef8bf 100644 --- a/src/time.rs +++ b/src/time.rs @@ -3,7 +3,7 @@ use core::fmt; use core::time::Duration; use der::asn1::{GeneralizedTime, UtcTime}; -use der::{Choice, DateTime, Decode, Error, Result, Sequence, ValueOrd}; +use der::{Choice, DateTime, Sequence, ValueOrd}; #[cfg(feature = "std")] use std::time::SystemTime; @@ -21,9 +21,13 @@ use std::time::SystemTime; /// /// [RFC 5280 Section 4.1.2.5]: https://tools.ietf.org/html/rfc5280#section-4.1.2.5 /// [RFC 5280 Appendix A]: https://tools.ietf.org/html/rfc5280#page-117 +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] #[derive(Choice, Copy, Clone, Debug, Eq, PartialEq, ValueOrd)] pub enum Time { - /// Legacy UTC time (has 2-digit year, valid only through 2050). + /// Legacy UTC time (has 2-digit year, valid from 1970 to 2049). + /// + /// Note: RFC 5280 specifies 1950-2049, however due to common operations working on + /// `UNIX_EPOCH` this implementation's lower bound is 1970. #[asn1(type = "UTCTime")] UtcTime(UtcTime), @@ -33,6 +37,10 @@ pub enum Time { } impl Time { + /// Time used for Certificate who do not expire. + pub const INFINITY: Time = + Time::GeneralTime(GeneralizedTime::from_date_time(DateTime::INFINITY)); + /// Get duration since `UNIX_EPOCH`. pub fn to_unix_duration(self) -> Duration { match self { @@ -51,17 +59,30 @@ impl Time { /// Convert to [`SystemTime`]. #[cfg(feature = "std")] - #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn to_system_time(&self) -> SystemTime { match self { Time::UtcTime(t) => t.to_system_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 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_date_time()) } } @@ -79,7 +100,6 @@ impl From for Time { } #[cfg(feature = "std")] -#[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl From