diff --git a/.github/workflows/pkcs10.yml b/.github/workflows/pkcs10.yml new file mode 100644 index 000000000..0ed155f69 --- /dev/null +++ b/.github/workflows/pkcs10.yml @@ -0,0 +1,64 @@ +name: pkcs10 + +on: + pull_request: + paths: + - "base64ct/**" + - "const-oid/**" + - "der/**" + - "pem-rfc7468/**" + - "pkcs10/**" + - "spki/**" + - "x509/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: pkcs10 + +env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Dwarnings" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.57.0 # MSRV + - stable + target: + - thumbv7em-none-eabi + - wasm32-unknown-unknown + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + target: ${{ matrix.target }} + override: true + - run: cargo build --release --target ${{ matrix.target }} --no-default-features + - run: cargo build --release --target ${{ matrix.target }} --no-default-features --features pem + + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.57.0 # MSRV + - stable + steps: + - uses: actions/checkout@v1 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + - run: cargo test --release --no-default-features + - run: cargo test --release + - run: cargo test --release --features pem + - run: cargo test --release --all-features diff --git a/Cargo.lock b/Cargo.lock index 58987971d..574c08945 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -530,6 +530,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "pkcs10" +version = "0.1.0" +dependencies = [ + "der", + "hex-literal", + "spki", + "x509", +] + [[package]] name = "pkcs5" version = "0.5.0-pre" diff --git a/Cargo.toml b/Cargo.toml index fbc091107..5d945d8a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "pkcs5", "pkcs7", "pkcs8", + "pkcs10", "sec1", "spki", "ssh-key", diff --git a/pkcs10/CHANGELOG.md b/pkcs10/CHANGELOG.md new file mode 100644 index 000000000..d6637e049 --- /dev/null +++ b/pkcs10/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog +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). diff --git a/pkcs10/Cargo.toml b/pkcs10/Cargo.toml new file mode 100644 index 000000000..b262402e8 --- /dev/null +++ b/pkcs10/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "pkcs10" +version = "0.1.0" +description = """ +Pure Rust implementation of Public-Key Cryptography Standards (PKCS) #10: +Certification Request Syntax Specification (RFC 5208). +""" +authors = ["RustCrypto Developers"] +license = "Apache-2.0 OR MIT" +repository = "https://github.com/RustCrypto/formats/tree/master/pkcs10" +categories = ["cryptography", "data-structures", "encoding", "no-std", "parser-implementations"] +keywords = ["crypto", "pkcs", "certification", "request", "csr", "certificate"] +readme = "README.md" +edition = "2021" +rust-version = "1.56" + +[dev-dependencies] +hex-literal = "0.3" + +[dependencies] +der = { version = "0.6.0-pre.0", features = ["oid", "derive", "alloc"], path = "../der" } +spki = { version = "0.6.0-pre", path = "../spki" } +x509 = { version = "0.0.1", path = "../x509" } + +[features] +pem = ["der/pem", "spki/pem"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/pkcs10/LICENSE-APACHE b/pkcs10/LICENSE-APACHE new file mode 100644 index 000000000..78173fa2e --- /dev/null +++ b/pkcs10/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/pkcs10/LICENSE-MIT b/pkcs10/LICENSE-MIT new file mode 100644 index 000000000..b9d3eff60 --- /dev/null +++ b/pkcs10/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2020-2021 The RustCrypto Project Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/pkcs10/README.md b/pkcs10/README.md new file mode 100644 index 000000000..77e0e03d0 --- /dev/null +++ b/pkcs10/README.md @@ -0,0 +1,81 @@ +# [RustCrypto]: PKCS#10 (Certification Requests) + +[![crate][crate-image]][crate-link] +[![Docs][docs-image]][docs-link] +[![Build Status][build-image]][build-link] +![Apache2/MIT licensed][license-image] +![Rust Version][rustc-image] +[![Project Chat][chat-image]][chat-link] + +Pure Rust implementation of Public-Key Cryptography Standards (PKCS) #10: +Certification Request Syntax Specification ([RFC 2986]). + +[Documentation][docs-link] + +## About PKCS#10 + +PKCS#10 is a format for certification requests (sometimes called certificate +signing requests). This format usually contains a public key. + +You can identify a PKCS#10 request encoded as PEM (i.e. text) by the +following: + +```text +-----BEGIN CERTIFICATE REQUEST----- +``` + +PKCS#10 certification requests can also be serialized in an ASN.1-based binary +format. The PEM text encoding is a Base64 representation of this format. + +## Supported Algorithms + +This crate is implemented in an algorithm-agnostic manner with the goal of +enabling PKCS#10 support for any algorithm. + +That said, it has been tested for interoperability against keys generated by +OpenSSL for the following algorithms: + +- RSA (`id-rsaEncryption`) + +Please open an issue if you encounter trouble using it with a particular +algorithm, including the ones listed above or other algorithms. + +## Minimum Supported Rust Version + +This crate requires **Rust 1.56** at a minimum. + +We may change the MSRV in the future, but it will be accompanied by a minor +version bump. + +## License + +Licensed under either of: + + * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) + * [MIT license](http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[//]: # (badges) + +[crate-image]: https://img.shields.io/crates/v/pkcs10.svg +[crate-link]: https://crates.io/crates/pkcs10 +[docs-image]: https://docs.rs/pkcs10/badge.svg +[docs-link]: https://docs.rs/pkcs10/ +[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg +[rustc-image]: https://img.shields.io/badge/rustc-1.56+-blue.svg +[chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg +[chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/300570-formats +[build-image]: https://github.com/RustCrypto/formats/workflows/pkcs10/badge.svg?branch=master&event=push +[build-link]: https://github.com/RustCrypto/formats/actions + +[//]: # (links) + +[RustCrypto]: https://github.com/rustcrypto +[RFC 2986]: https://tools.ietf.org/html/rfc2986 diff --git a/pkcs10/src/attribute.rs b/pkcs10/src/attribute.rs new file mode 100644 index 000000000..afc9921a3 --- /dev/null +++ b/pkcs10/src/attribute.rs @@ -0,0 +1,54 @@ +use der::asn1::{Any, ObjectIdentifier, SetOfVec}; +use der::{Decodable, Sequence, ValueOrd}; + +/// PKCS#10 `Attribute` as defined in [RFC 5280 Appendix A.1]. +/// +/// ```text +/// Attribute ::= SEQUENCE { +/// type AttributeType, +/// values SET OF AttributeValue -- at least one value is required +/// } +/// +/// AttributeType ::= OBJECT IDENTIFIER +/// +/// AttributeValue ::= ANY -- DEFINED BY AttributeType +/// ``` +/// +/// Note that [RFC 2986 Section 4] defines a constrained version of this type: +/// +/// ```text +/// Attribute { ATTRIBUTE:IOSet } ::= SEQUENCE { +/// type ATTRIBUTE.&id({IOSet}), +/// values SET SIZE(1..MAX) OF ATTRIBUTE.&Type({IOSet}{@type}) +/// } +/// ``` +/// +/// The unconstrained version should be preferred. +/// +/// [RFC 2986 Section 4]: https://datatracker.ietf.org/doc/html/rfc2986#section-4 +/// [RFC 5280 Appendix A.1]: https://datatracker.ietf.org/doc/html/rfc5280#appendix-A.1 +#[derive(Clone, Debug, PartialEq, Eq, Sequence, ValueOrd)] +pub struct Attribute<'a> { + /// Attribute kind (OID). + pub oid: ObjectIdentifier, + + /// Attribute values. + pub values: SetOfVec>, +} + +impl<'a> TryFrom<&'a [u8]> for Attribute<'a> { + type Error = der::Error; + + fn try_from(bytes: &'a [u8]) -> Result { + Self::from_der(bytes) + } +} + +/// PKCS#10 `Attributes` as defined in [RFC 2986 Section 4]. +/// +/// ```text +/// Attributes { ATTRIBUTE:IOSet } ::= SET OF Attribute{{ IOSet }} +/// ``` +/// +/// [RFC 2986 Section 4]: https://datatracker.ietf.org/doc/html/rfc2986#section-4 +pub type Attributes<'a> = SetOfVec>; diff --git a/pkcs10/src/document.rs b/pkcs10/src/document.rs new file mode 100644 index 000000000..fd3823d96 --- /dev/null +++ b/pkcs10/src/document.rs @@ -0,0 +1,88 @@ +//! Certification request document. + +use super::CertReq; + +use alloc::vec::Vec; +use core::fmt; + +use der::{Decodable, Document}; + +#[cfg(feature = "pem")] +use {core::str::FromStr, der::pem}; + +/// Certification request document. +/// +/// This type provides storage for [`CertReq`] encoded as ASN.1 +/// DER with the invariant that the contained-document is "well-formed", i.e. +/// it will parse successfully according to this crate's parsing rules. +#[derive(Clone)] +pub struct CertReqDocument(Vec); + +impl<'a> Document<'a> for CertReqDocument { + type Message = CertReq<'a>; + const SENSITIVE: bool = false; +} + +impl AsRef<[u8]> for CertReqDocument { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl TryFrom<&[u8]> for CertReqDocument { + type Error = der::Error; + + fn try_from(bytes: &[u8]) -> Result { + bytes.to_vec().try_into() + } +} + +impl TryFrom> for CertReqDocument { + type Error = der::Error; + + fn try_from(cr: CertReq<'_>) -> Result { + Self::try_from(&cr) + } +} + +impl TryFrom<&CertReq<'_>> for CertReqDocument { + type Error = der::Error; + + fn try_from(cr: &CertReq<'_>) -> Result { + Self::from_msg(cr) + } +} + +impl TryFrom> for CertReqDocument { + type Error = der::Error; + + fn try_from(bytes: Vec) -> der::Result { + // Ensure document is well-formed + CertReq::from_der(bytes.as_slice())?; + Ok(Self(bytes)) + } +} + +impl fmt::Debug for CertReqDocument { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt.debug_tuple("CertReqDocument") + .field(&self.decode()) + .finish() + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl FromStr for CertReqDocument { + type Err = der::Error; + + fn from_str(s: &str) -> Result { + Self::from_pem(s) + } +} + +#[cfg(feature = "pem")] +#[cfg_attr(docsrs, doc(cfg(feature = "pem")))] +impl pem::PemLabel for CertReqDocument { + const TYPE_LABEL: &'static str = "CERTIFICATE REQUEST"; +} diff --git a/pkcs10/src/info.rs b/pkcs10/src/info.rs new file mode 100644 index 000000000..5a23bddb0 --- /dev/null +++ b/pkcs10/src/info.rs @@ -0,0 +1,38 @@ +use super::{Attributes, Version}; +use der::{Decodable, Sequence}; + +/// PKCS#10 `CertificationRequestInfo` as defined in [RFC 2986 Section 4]. +/// +/// ```text +/// CertificationRequestInfo ::= SEQUENCE { +/// version INTEGER { v1(0) } (v1,...), +/// subject Name, +/// subjectPKInfo SubjectPublicKeyInfo{{ PKInfoAlgorithms }}, +/// attributes [0] Attributes{{ CRIAttributes }} +/// } +/// ``` +/// +/// [RFC 2986 Section 4]: https://datatracker.ietf.org/doc/html/rfc2986#section-4 +#[derive(Clone, Debug, PartialEq, Eq, Sequence)] +pub struct CertReqInfo<'a> { + /// Certification request version. + pub version: Version, + + /// Subject name. + pub subject: x509::Name<'a>, + + /// Subject public key info. + pub public_key: spki::SubjectPublicKeyInfo<'a>, + + /// Request attributes. + #[asn1(context_specific = "0", tag_mode = "IMPLICIT")] + pub attributes: Attributes<'a>, +} + +impl<'a> TryFrom<&'a [u8]> for CertReqInfo<'a> { + type Error = der::Error; + + fn try_from(bytes: &'a [u8]) -> Result { + Self::from_der(bytes) + } +} diff --git a/pkcs10/src/lib.rs b/pkcs10/src/lib.rs new file mode 100644 index 000000000..dc07de1e2 --- /dev/null +++ b/pkcs10/src/lib.rs @@ -0,0 +1,57 @@ +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg", + html_root_url = "https://docs.rs/pkcs10/0.3.0" +)] +#![forbid(unsafe_code, clippy::unwrap_used)] +#![warn(missing_docs, rust_2018_idioms, unused_qualifications)] + +extern crate alloc; + +mod attribute; +mod document; +mod info; +mod version; + +pub use attribute::{Attribute, Attributes}; +pub use document::CertReqDocument; +pub use info::CertReqInfo; +pub use version::Version; + +use der::asn1::BitString; +use der::{Decodable, Sequence}; +use spki::AlgorithmIdentifier; + +/// PKCS#10 `CertificationRequest` as defined in [RFC 2986 Section 4]. +/// +/// ```text +/// CertificationRequest ::= SEQUENCE { +/// certificationRequestInfo CertificationRequestInfo, +/// signatureAlgorithm AlgorithmIdentifier{{ SignatureAlgorithms }}, +/// signature BIT STRING +/// } +/// ``` +/// +/// [RFC 2986 Section 4]: https://datatracker.ietf.org/doc/html/rfc2986#section-4 +#[derive(Clone, Debug, PartialEq, Eq, Sequence)] +pub struct CertReq<'a> { + /// Certification request information. + pub info: CertReqInfo<'a>, + + /// Signature algorithm identifier. + pub algorithm: AlgorithmIdentifier<'a>, + + /// Signature. + pub signature: BitString<'a>, +} + +impl<'a> TryFrom<&'a [u8]> for CertReq<'a> { + type Error = der::Error; + + fn try_from(bytes: &'a [u8]) -> Result { + Self::from_der(bytes) + } +} diff --git a/pkcs10/src/version.rs b/pkcs10/src/version.rs new file mode 100644 index 000000000..48bfad7c4 --- /dev/null +++ b/pkcs10/src/version.rs @@ -0,0 +1,49 @@ +//! Certification request information version identifier. + +use der::{Decodable, Decoder, Encodable, Encoder, FixedTag, Tag}; + +/// Version identifier for certification request information. +/// +/// (RFC 2986 designates `0` as the only valid version) +#[derive(Clone, Debug, Copy, PartialEq, Eq)] +pub enum Version { + /// Denotes PKCS#8 v1 + V1 = 0, +} + +impl Decodable<'_> for Version { + fn decode(decoder: &mut Decoder<'_>) -> der::Result { + Version::try_from(u8::decode(decoder)?).map_err(|_| Self::TAG.value_error()) + } +} + +impl Encodable for Version { + fn encoded_len(&self) -> der::Result { + der::Length::from(1u8).for_tlv() + } + + fn encode(&self, encoder: &mut Encoder<'_>) -> der::Result<()> { + u8::from(*self).encode(encoder) + } +} + +impl From for u8 { + fn from(version: Version) -> Self { + version as u8 + } +} + +impl TryFrom for Version { + type Error = der::Error; + + fn try_from(byte: u8) -> Result { + match byte { + 0 => Ok(Version::V1), + _ => Err(Self::TAG.value_error()), + } + } +} + +impl FixedTag for Version { + const TAG: Tag = Tag::Integer; +} diff --git a/pkcs10/tests/certreq.rs b/pkcs10/tests/certreq.rs new file mode 100644 index 000000000..d3ab85d8e --- /dev/null +++ b/pkcs10/tests/certreq.rs @@ -0,0 +1,118 @@ +//! Certification request (`CertReq`) tests + +use der::{Encodable, Tag, Tagged}; +use hex_literal::hex; +use pkcs10::{CertReq, Version}; + +#[cfg(feature = "pem")] +use der::Document; + +#[cfg(feature = "pem")] +use pkcs10::CertReqDocument; + +const RSA_KEY: &[u8] = &hex!("3082010A0282010100BF59F7FE716DDE47C73579CA846EFA8D30AB3612E0D6A524204A72CA8E50C9F459513DF0D73331BED3D7A2DA7A362719E471EE6A9D87827D1024ED44605AB9B48F3B808C5E173B9F3EC4003D57F1718489F5C7A0421C46FBD527A40AB4BA6B9DB16A545D1ECF6E2A5633BD80594EBA4AFEE71F63E1D357C64E9A3FF6B83746A885C373F3527987E4C2B4AF7FE4D4EA16405E5E15285DD938823AA18E2634BAFE847A761CAFABB0401D3FA03A07A9D097CBB0C77156CCFE36131DADF1C109C2823972F0AF21A35F358E788304C0C78B951739D91FABFFD07AA8CD4F69746B3D0EB4587469F9D39F4FBDC761200DFB27DAF69562311D8B191B7EEFAAE2F8D6F8EB0203010001"); +const RSA_SIG: &[u8] = &hex!("2B053CFE81C6542176BD70B373A5FC8DC1F1806A5AB10D25E36690EED1DF57AD5F18EC0CCF165F000245B14157141224B431EC6715EFE937F66B892D11EDF8858EDF67ACCAE9701A2244BECA80705D7CC292BAD9B02001E4572EE492B08473D5AF59CC83DDA1DE5C2BF470FD784495070A9C5AF8EA9A4060C1DBC5C4690CC8DF6D528C55D82EC9C0DF3046BBCAE7542025D7EE170788C9C234132703290A31AC2700E55339590226D5E582EC61869862769FD85B45F287FFDD6DB530995D31F94D7D2C26EF3F48A182C3026CC698F382A72F1A11E3C689953055DAC0DFEBE9CDB163CA3AF33FFC4DA0F6B84B9D7CDD4321CCECD4BAC528DEFF9715FFD9D4731E"); + +/// RSA-2048 `CertReq` encoded as ASN.1 DER +const RSA_2048_DER_EXAMPLE: &[u8] = include_bytes!("examples/rsa2048-csr.der"); + +/// RSA-2048 PKCS#8 public key encoded as PEM +#[cfg(feature = "pem")] +const RSA_2048_PEM_EXAMPLE: &str = include_str!("examples/rsa2048-csr.pem"); + +const NAMES: &[(&str, &str)] = &[ + ("2.5.4.3", "example.com"), + ("2.5.4.7", "Los Angeles"), + ("2.5.4.8", "California"), + ("2.5.4.10", "Example Inc"), + ("2.5.4.6", "US"), +]; + +#[rustfmt::skip] +const EXTENSIONS: &[(&str, &[u8])] = &[ + ("2.5.29.19", &hex!("3000")), // basicConstraints + ("2.5.29.15", &hex!("030205A0")), // keyUsage + ("2.5.29.37", &hex!("301406082B0601050507030106082B06010505070302")), // extKeyUsage + ("2.5.29.17", &hex!("300D820B6578616D706C652E636F6D")), // subjectAltNamec +]; + +#[test] +fn decode_rsa_2048_der() { + let cr = CertReq::try_from(RSA_2048_DER_EXAMPLE).unwrap(); + + // Check the version. + assert_eq!(cr.info.version, Version::V1); + + // Check all the RDNs. + assert_eq!(cr.info.subject.len(), NAMES.len()); + for (name, (oid, val)) in cr.info.subject.iter().zip(NAMES) { + let kind = name.get(0).unwrap(); + let value = match kind.value.tag() { + Tag::Utf8String => kind.value.utf8_string().unwrap().as_str(), + Tag::PrintableString => kind.value.printable_string().unwrap().as_str(), + _ => panic!("unexpected tag"), + }; + + assert_eq!(kind.oid, oid.parse().unwrap()); + assert_eq!(name.len(), 1); + assert_eq!(value, *val); + } + + // Check the public key. + let alg = cr.info.public_key.algorithm; + assert_eq!(alg.oid, "1.2.840.113549.1.1.1".parse().unwrap()); + assert!(alg.parameters.unwrap().is_null()); + assert_eq!(cr.info.public_key.subject_public_key, RSA_KEY); + + // Check the attributes (just one; contains extensions). + assert_eq!(cr.info.attributes.len(), 1); + let attribute = cr.info.attributes.get(0).unwrap(); + assert_eq!(attribute.oid, "1.2.840.113549.1.9.14".parse().unwrap()); // extensionRequest + assert_eq!(attribute.values.len(), 1); + + // Check the extensions. + let extensions: x509::Extensions = attribute.values.get(0).unwrap().decode_into().unwrap(); + for (ext, (oid, val)) in extensions.iter().zip(EXTENSIONS) { + assert_eq!(ext.extn_id, oid.parse().unwrap()); + assert_eq!(ext.extn_value, *val); + assert!(!ext.critical); + } + + // Check the signature value. + assert_eq!(cr.algorithm.oid, "1.2.840.113549.1.1.11".parse().unwrap()); + assert!(cr.algorithm.parameters.unwrap().is_null()); + assert_eq!(cr.signature.as_bytes().unwrap(), RSA_SIG); +} + +#[test] +#[cfg(feature = "pem")] +fn decode_rsa_2048_pem() { + let doc: CertReqDocument = RSA_2048_PEM_EXAMPLE.parse().unwrap(); + assert_eq!(doc.as_ref(), RSA_2048_DER_EXAMPLE); + + // Ensure `CertReqDocument` parses successfully + let cr = CertReq::try_from(RSA_2048_DER_EXAMPLE).unwrap(); + assert_eq!(doc.decode(), cr); +} + +// The following tests currently fail because of a bug in the `der` crate; +// specifically, the `IMPLICIT` tagging on `CertReqInfo::attributes`. + +#[test] +fn encode_rsa_2048_der() { + let cr = CertReq::try_from(RSA_2048_DER_EXAMPLE).unwrap(); + let cr_encoded = cr.to_vec().unwrap(); + assert_eq!(RSA_2048_DER_EXAMPLE, cr_encoded.as_slice()); +} + +#[test] +#[cfg(feature = "pem")] +fn encode_rsa_2048_pem() { + let cr = CertReq::try_from(RSA_2048_DER_EXAMPLE).unwrap(); + let cr_encoded = CertReqDocument::try_from(cr) + .unwrap() + .to_pem(Default::default()) + .unwrap(); + + assert_eq!(RSA_2048_PEM_EXAMPLE, cr_encoded); +} diff --git a/pkcs10/tests/examples/rsa2048-crt.der b/pkcs10/tests/examples/rsa2048-crt.der new file mode 100644 index 000000000..6f46ef1f0 Binary files /dev/null and b/pkcs10/tests/examples/rsa2048-crt.der differ diff --git a/pkcs10/tests/examples/rsa2048-crt.pem b/pkcs10/tests/examples/rsa2048-crt.pem new file mode 100644 index 000000000..d45142cfb --- /dev/null +++ b/pkcs10/tests/examples/rsa2048-crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDnDCCAoSgAwIBAgIJAKQzLo3paeO7MA0GCSqGSIb3DQEBCwUAMGQxFDASBgNV +BAMMC2V4YW1wbGUuY29tMRQwEgYDVQQHDAtMb3MgQW5nZWxlczETMBEGA1UECAwK +Q2FsaWZvcm5pYTEUMBIGA1UECgwLRXhhbXBsZSBJbmMxCzAJBgNVBAYTAlVTMB4X +DTIyMDEwODE4NDA1N1oXDTIzMDEwODE4NDA1N1owZDEUMBIGA1UEAwwLZXhhbXBs +ZS5jb20xFDASBgNVBAcMC0xvcyBBbmdlbGVzMRMwEQYDVQQIDApDYWxpZm9ybmlh +MRQwEgYDVQQKDAtFeGFtcGxlIEluYzELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC/Wff+cW3eR8c1ecqEbvqNMKs2EuDWpSQgSnLK +jlDJ9FlRPfDXMzG+09ei2no2Jxnkce5qnYeCfRAk7URgWrm0jzuAjF4XO58+xAA9 +V/FxhIn1x6BCHEb71SekCrS6a52xalRdHs9uKlYzvYBZTrpK/ucfY+HTV8ZOmj/2 +uDdGqIXDc/NSeYfkwrSvf+TU6hZAXl4VKF3ZOII6oY4mNLr+hHp2HK+rsEAdP6A6 +B6nQl8uwx3FWzP42Ex2t8cEJwoI5cvCvIaNfNY54gwTAx4uVFznZH6v/0HqozU9p +dGs9DrRYdGn5059PvcdhIA37J9r2lWIxHYsZG37vquL41vjrAgMBAAGjUTBPMAkG +A1UdEwQCMAAwCwYDVR0PBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF +BQcDAjAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEA +kqvA9M0WRVffA9Eb5h813vio3ceQ8JItVHWyvh9vNGOz3d3eywXIOAKMmzQRQUfY +7WMbjCM9ppTKRmfoFbMnDQb1aa93isuCoo5QRSpX6DmN/p4v3uz79p8m8in+xhKQ +1m6et1iwR9cbQxLsmsaVaVTn16xdsL+gq7V4IZXf8CVyxL0mH5FdRmj/nqiWTv6S +I9tIFiEhCqq1P5XGi6TJAg59M8Dlnd/j5eJHTIlADjG0O1LLvAcuc3rq+dYj0mOU +RX4MzusreyKRGdvr2IN2gYCDPOgOiqp3YKkOnXV8/pya1KSGrT51fEYTdUrjJ6dr +430thqsUED++/t+K76IRMw== +-----END CERTIFICATE----- diff --git a/pkcs10/tests/examples/rsa2048-csr.der b/pkcs10/tests/examples/rsa2048-csr.der new file mode 100644 index 000000000..31c9e225a Binary files /dev/null and b/pkcs10/tests/examples/rsa2048-csr.der differ diff --git a/pkcs10/tests/examples/rsa2048-csr.pem b/pkcs10/tests/examples/rsa2048-csr.pem new file mode 100644 index 000000000..15c7cc40f --- /dev/null +++ b/pkcs10/tests/examples/rsa2048-csr.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIDCTCCAfECAQAwZDEUMBIGA1UEAwwLZXhhbXBsZS5jb20xFDASBgNVBAcMC0xv +cyBBbmdlbGVzMRMwEQYDVQQIDApDYWxpZm9ybmlhMRQwEgYDVQQKDAtFeGFtcGxl +IEluYzELMAkGA1UEBhMCVVMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQC/Wff+cW3eR8c1ecqEbvqNMKs2EuDWpSQgSnLKjlDJ9FlRPfDXMzG+09ei2no2 +Jxnkce5qnYeCfRAk7URgWrm0jzuAjF4XO58+xAA9V/FxhIn1x6BCHEb71SekCrS6 +a52xalRdHs9uKlYzvYBZTrpK/ucfY+HTV8ZOmj/2uDdGqIXDc/NSeYfkwrSvf+TU +6hZAXl4VKF3ZOII6oY4mNLr+hHp2HK+rsEAdP6A6B6nQl8uwx3FWzP42Ex2t8cEJ +woI5cvCvIaNfNY54gwTAx4uVFznZH6v/0HqozU9pdGs9DrRYdGn5059PvcdhIA37 +J9r2lWIxHYsZG37vquL41vjrAgMBAAGgYDBeBgkqhkiG9w0BCQ4xUTBPMAkGA1Ud +EwQCMAAwCwYDVR0PBAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD +AjAWBgNVHREEDzANggtleGFtcGxlLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAKwU8 +/oHGVCF2vXCzc6X8jcHxgGpasQ0l42aQ7tHfV61fGOwMzxZfAAJFsUFXFBIktDHs +ZxXv6Tf2a4ktEe34hY7fZ6zK6XAaIkS+yoBwXXzCkrrZsCAB5Fcu5JKwhHPVr1nM +g92h3lwr9HD9eESVBwqcWvjqmkBgwdvFxGkMyN9tUoxV2C7JwN8wRrvK51QgJdfu +FweIycI0EycDKQoxrCcA5VM5WQIm1eWC7GGGmGJ2n9hbRfKH/91ttTCZXTH5TX0s +Ju8/SKGCwwJsxpjzgqcvGhHjxomVMFXawN/r6c2xY8o68z/8TaD2uEudfN1DIczs +1LrFKN7/lxX/2dRzHg== +-----END CERTIFICATE REQUEST----- diff --git a/pkcs10/tests/examples/rsa2048-prv.der b/pkcs10/tests/examples/rsa2048-prv.der new file mode 100644 index 000000000..f5c6adc7d Binary files /dev/null and b/pkcs10/tests/examples/rsa2048-prv.der differ diff --git a/pkcs10/tests/examples/rsa2048-prv.pem b/pkcs10/tests/examples/rsa2048-prv.pem new file mode 100644 index 000000000..26e7df39b --- /dev/null +++ b/pkcs10/tests/examples/rsa2048-prv.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAv1n3/nFt3kfHNXnKhG76jTCrNhLg1qUkIEpyyo5QyfRZUT3w +1zMxvtPXotp6NicZ5HHuap2Hgn0QJO1EYFq5tI87gIxeFzufPsQAPVfxcYSJ9ceg +QhxG+9UnpAq0umudsWpUXR7PbipWM72AWU66Sv7nH2Ph01fGTpo/9rg3RqiFw3Pz +UnmH5MK0r3/k1OoWQF5eFShd2TiCOqGOJjS6/oR6dhyvq7BAHT+gOgep0JfLsMdx +Vsz+NhMdrfHBCcKCOXLwryGjXzWOeIMEwMeLlRc52R+r/9B6qM1PaXRrPQ60WHRp ++dOfT73HYSAN+yfa9pViMR2LGRt+76ri+Nb46wIDAQABAoIBAQC0ZwMKrTATH4Lt +pLxM7UBkupzAJz44v4r2spnU5CXAsRFAKfCVQxvEOH8Vd3s+8NBVcyB+/bOTT4tX +9SXA3eg1FdDYWf4fU0PIbgt3yiDEkFttD97EVVqK9KQh4UIQe4M5j/CntnOD/oA0 +2ZVXHYU/TWDjVEzE7vz0gDKLzZO3lWrpmUiLd6bglfA0vxpbgQ901e/SWdXlqXzO +ejjRX/htEQ8tDyW9GrOdVs6NRM1KH3+WCUhNp6xBQtNcqCwKzpvmMcPQyVGxAy6P +fB7vqzjSDs1jEREFhWW7266tZgL+AieA+b86eSFDqMJ9LoLhYyHtflR2UAsgae/a ++sB3B4XRAoGBAN3E2HFJDewNSIniM+f1XVk+rvbOTsTcCFvmXd47HFz+ogJ+n6ME +/xwT5OYGsXAEhuduFndkbZAz/s+GpNGu8BBklmUagGXcYKUibt7nhQDRUC8pf9NQ +KPGQOyLsZXfFVs/QxwIQl/QXw8TS0vtqosirY//zPG9gm2jfqmkr34BDAoGBANzj +Lq85a1V4s2UVGj05VBZsP5P9XL9ew7oOthXvffgcwc5mIQDiGsU2rQRn957JES/g +hoPG5LgOjep129mT7h1Ws0+4dMO0uKXbMvOHkhUbdXiumFFYCxfQWiV1AVOMxyeE +jWPZiXKCuaTLQs0VFnQG0dT5cWIfBnOBhUx8lU45AoGAAvNjjd5S+RkUJgGEf0mc +fFuBKHeGRMhItDBUf2h58CLTNQVKSnj+i/kXype8NKlawimM0vnbG1gVw90exEt3 +lkBAYAgCPVi5UHks0Hp0IpamYnpC4STn5o7suoI6t2VAynMUsspVu0G1sSC8/etl +TxY4tmceHr1CVBrlwZB74NECgYByAStaMteMELT+ieq2CL22qP4TgqP4/Y8lm2wt +XCN3CFibD6kfDJPmj7ay3Ho4UOx2+npSzzfDK3fhuBzVan1uVQ5NKhXR4JegusbM +XH9wN3Dk7bAd48Qt8VJlnMMnfTRY2BglneRL3t60CFidArJJBjAMrQXxL7Qjr4i+ +Flr1OQKBgCHey67JrCzv3VqnnCB1iScVMi8ABv11Mw8fwXIdhpgzYeFFAQ4YROuB +YgmIjTW2H6sjZHKVuyUq0wWm+SOByeVmNnh6dNhD6MLWnaGOC2ZSzaM3w0QizJZP +lo2n+xr6iC0aRBV5PkbIxv1eCLwDBO7Rij0NMaljeFUIXdMNb8NT +-----END RSA PRIVATE KEY-----