diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..231e075 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,196 @@ +name: rustls + +permissions: + contents: read + +on: + push: + pull_request: + merge_group: + schedule: + - cron: '0 21 * * *' + +jobs: + build: + name: Build + test + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + # test a bunch of toolchains on ubuntu + rust: + - stable + - beta + - nightly + os: [ubuntu-latest] + # but only stable on macos/windows (slower platforms) + include: + - os: macos-latest + rust: stable + - os: windows-latest + rust: stable + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install ${{ matrix.rust }} toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + + - name: Install valgrind + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y valgrind + + - name: cargo test (debug; default features) + run: cargo test + env: + RUST_BACKTRACE: 1 + + - name: cargo test (debug; all features) + run: cargo test --all-features + env: + RUST_BACKTRACE: 1 + + - name: cargo test (debug; no default features; no run) + run: cargo test --no-default-features + env: + RUST_BACKTRACE: 1 + + wasm_build: + name: Build wasm32 + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Add wasm target + run: rustup target add wasm32-unknown-unknown + + - name: wasm32 build (debug; default features) + run: cargo build --target wasm32-unknown-unknown --lib + env: + RUST_BACKTRACE: 1 + + - name: wasm32 build (debug; all features) + run: cargo build --target wasm32-unknown-unknown --lib --all-features + env: + RUST_BACKTRACE: 1 + + - name: wasm32 build (debug; no default features) + run: cargo build --target wasm32-unknown-unknown --lib --no-default-features + env: + RUST_BACKTRACE: 1 + + msrv: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install MSRV toolchain + uses: dtolnay/rust-toolchain@master + with: + toolchain: "1.60" + + - run: cargo check --lib --all-features + + format: + name: Format + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Check formatting + run: cargo fmt --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Install rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - run: cargo clippy --all-features -- --deny warnings + + semver: + name: Check semver compatibility + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Check semver + uses: obi1kenobi/cargo-semver-checks-action@v2 + + fuzz: + name: Smoke-test fuzzing targets + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install nightly toolchain + uses: dtolnay/rust-toolchain@nightly + + - name: Install cargo fuzz + run: cargo install cargo-fuzz + + - name: Smoke-test fuzz targets + run: | + cargo fuzz build + for target in $(cargo fuzz list) ; do + cargo fuzz run $target -- -max_total_time=10 + done + + valgrind: + name: Check side-channels on base64 decoder + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Install stable toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install valgrind + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y valgrind + + - name: Build and run test + run: | + cargo test --all-features --lib + exe=$(cargo test --all-features --no-run --message-format json | \ + jq --slurp --raw-output '.[] | select(.reason == "compiler-artifact") | select(.target.name == "rustls_pki_types") | select(.profile.test) | .executable') + valgrind --error-exitcode=99 --exit-on-first-error=yes $exe + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8eb581d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +/.idea diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..051e55f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "rustls-pki-types" +version = "1.10.1" +edition = "2021" +rust-version = "1.60" +license = "MIT OR Apache-2.0" +description = "Shared types for the rustls PKI ecosystem" +documentation = "https://docs.rs/rustls-pki-types" +homepage = "https://github.com/rustls/pki-types" +repository = "https://github.com/rustls/pki-types" +categories = ["network-programming", "data-structures", "cryptography"] + +[features] +default = ["alloc"] +alloc = [] +std = ["alloc"] +web = ["web-time"] + +[target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dev-dependencies] +crabgrind = "=0.1.9" # compatible with valgrind package on GHA ubuntu-latest + +[target.'cfg(all(target_family = "wasm", target_os = "unknown"))'.dependencies] +web-time = { version = "1", optional = true } + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/LICENSE-APACHE b/LICENSE-APACHE new file mode 100644 index 0000000..5449729 --- /dev/null +++ b/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 2023 Dirkjan Ochtman + +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/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..f8cdbfd --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2023 Dirkjan Ochtman + +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/README.md b/README.md new file mode 100644 index 0000000..8bda0c7 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# rustls-pki-types + +[![Build Status](https://github.com/rustls/pki-types/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/rustls/pki-types/actions/workflows/ci.yml?query=branch%3Amain) +[![Documentation](https://docs.rs/rustls-pki-types/badge.svg)](https://docs.rs/rustls-pki-types/) +[![Chat](https://img.shields.io/discord/976380008299917365?logo=discord)](https://discord.gg/MCSB76RU96) + +This crate provides types for representing X.509 certificates, keys and other types as commonly +used in the rustls ecosystem. It is intended to be used by crates that need to work with such X.509 +types, such as [rustls](https://crates.io/crates/rustls), +[rustls-webpki](https://crates.io/crates/rustls-webpki), +and others. + +Some of these crates used to define their own trivial wrappers around DER-encoded bytes. +However, in order to avoid inconvenient dependency edges, these were all disconnected. By +using a common low-level crate of types with long-term stable API, we hope to avoid the +downsides of unnecessary dependency edges while providing interoperability between crates. + +## Features + +- Interoperability between different crates in the rustls ecosystem +- Long-term stable API +- No dependencies +- Support for `no_std` contexts, with optional support for `alloc` + +## DER and PEM + +Many of the types defined in this crate represent DER-encoded data. DER is a binary encoding of +the ASN.1 format commonly used in web PKI specifications. It is a binary encoding, so it is +relatively compact when stored in memory. However, as a binary format, it is not very easy to +work with for humans and in contexts where binary data is inconvenient. For this reason, +many tools and protocols use a ASCII-based encoding of DER, called PEM. In addition to the +base64-encoded DER, PEM objects are delimited by header and footer lines which indicate the type +of object contained in the PEM blob. + +This crate's types can be created from both DER and PEM encodings. + +## Creating new certificates and keys + +This crate does not provide any functionality for creating new certificates or keys. However, +the [rcgen](https://docs.rs/rcgen) crate can be used to create new certificates and keys. + +## Cloning private keys + +This crate intentionally **does not** implement `Clone` on private key types in +order to minimize the exposure of private key data in memory. + +If you want to extend the lifetime of a `PrivateKeyDer<'_>`, consider [`PrivateKeyDer::clone_key()`]. +Alternatively since these types are immutable, consider wrapping the `PrivateKeyDer<'_>` in a [`Rc`] +or an [`Arc`]. + +[`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html +[`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html +[`PrivateKeyDer::clone_key()`]: https://docs.rs/rustls-pki-types/latest/rustls_pki_types/enum.PrivateKeyDer.html#method.clone_key diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..2730088 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,63 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" + +[[package]] +name = "rustls-pki-types-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "rustls-pki-types", +] diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..4bfa301 --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "rustls-pki-types-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.rustls-pki-types] +path = ".." +features = ["std"] + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "private_key" +path = "fuzz_targets/private_key.rs" +test = false +doc = false + +[[bin]] +name = "pem" +path = "fuzz_targets/pem.rs" +test = false +doc = false diff --git a/fuzz/corpus/pem/zen.pem b/fuzz/corpus/pem/zen.pem new file mode 120000 index 0000000..644365f --- /dev/null +++ b/fuzz/corpus/pem/zen.pem @@ -0,0 +1 @@ +../../../tests/data/zen.pem \ No newline at end of file diff --git a/fuzz/fuzz_targets/pem.rs b/fuzz/fuzz_targets/pem.rs new file mode 100644 index 0000000..6033012 --- /dev/null +++ b/fuzz/fuzz_targets/pem.rs @@ -0,0 +1,26 @@ +#![no_main] + +use std::io::Cursor; + +use libfuzzer_sys::fuzz_target; + +use rustls_pki_types::pem::PemObject; +use rustls_pki_types::{CertificateDer, PrivateKeyDer}; + +fuzz_target!(|data: &[u8]| { + // cover the code paths that use std::io + for x in CertificateDer::pem_reader_iter(&mut Cursor::new(data)) { + match x { + Ok(_item) => (), + Err(_err) => break, + } + } + + // cover the code paths that use slices + for x in PrivateKeyDer::pem_slice_iter(data) { + match x { + Ok(_item) => (), + Err(_err) => break, + } + } +}); diff --git a/fuzz/fuzz_targets/private_key.rs b/fuzz/fuzz_targets/private_key.rs new file mode 100644 index 0000000..184d13a --- /dev/null +++ b/fuzz/fuzz_targets/private_key.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + let _ = rustls_pki_types::PrivateKeyDer::try_from(data); +}); diff --git a/src/base64.rs b/src/base64.rs new file mode 100644 index 0000000..c76e6da --- /dev/null +++ b/src/base64.rs @@ -0,0 +1,748 @@ +/// Decode base64 `input`, writing the result into `output`. +/// +/// `input` is treated as secret, so efforts are made to avoid +/// leaking its value via side channels, such as timing, +/// memory accesses, and execution trace. +/// +/// The following is deemed non-secret information: +/// +/// - Appearance of whitespace in `input` +/// - Erroneous characters in `input` (indeed, the first illegal +/// character is quoted in the error type) +/// - The length of `input` +/// - The length of `output` +/// +/// Returns the prefix of `output` that was written to. +pub(crate) fn decode_secret<'a>(input: &[u8], output: &'a mut [u8]) -> Result<&'a [u8], Error> { + decode(input, output, CodePoint::decode_secret) +} + +/// Decode base64 `input`, writing the result into `output`. +/// +/// `input` is treated as public information, so its value may +/// be leaked via side channels. +/// +/// Returns the prefix of `output` that was written to. +pub(crate) fn decode_public<'a>(input: &[u8], output: &'a mut [u8]) -> Result<&'a [u8], Error> { + decode(input, output, CodePoint::decode_public) +} + +/// Provide an upper limit on how much space could be required +/// to decode a base64 encoding of len `base64_len`. +pub(crate) const fn decoded_length(base64_len: usize) -> usize { + ((base64_len + 3) / 4) * 3 +} + +fn decode<'a>( + input: &[u8], + output: &'a mut [u8], + decode_byte: impl Fn(u8) -> CodePoint, +) -> Result<&'a [u8], Error> { + let mut buffer = 0u64; + let mut used = 0; + let mut shift = SHIFT_INITIAL; + let mut pad_mask = 0; + + let mut output_offset = 0; + + const SHIFT_INITIAL: i32 = (8 - 1) * 6; + + for byte in input.iter().copied() { + let (item, pad) = match decode_byte(byte) { + CodePoint::WHITESPACE => continue, + CodePoint::INVALID => return Err(Error::InvalidCharacter(byte)), + CodePoint::PAD => (0, 1), + CodePoint(n) => (n, 0), + }; + + // we collect 8 code points (therefore: 6 output bytes) into + // `buffer`. this keeps this loop as tight as possible. + if used == 8 { + if pad_mask != 0b0000_0000 { + return Err(Error::PrematurePadding); + } + + let chunk = output + .get_mut(output_offset..output_offset + 6) + .ok_or(Error::InsufficientOutputSpace)?; + + chunk[0] = (buffer >> 40) as u8; + chunk[1] = (buffer >> 32) as u8; + chunk[2] = (buffer >> 24) as u8; + chunk[3] = (buffer >> 16) as u8; + chunk[4] = (buffer >> 8) as u8; + chunk[5] = buffer as u8; + + output_offset += 6; + buffer = 0; + used = 0; + pad_mask = 0; + shift = SHIFT_INITIAL; + } + + buffer |= (item as u64) << shift; + shift -= 6; + pad_mask |= pad << used; + used += 1; + } + + // reduce to final block + if used > 4 { + if pad_mask & 0b0000_1111 != 0 { + return Err(Error::PrematurePadding); + } + let chunk = output + .get_mut(output_offset..output_offset + 3) + .ok_or(Error::InsufficientOutputSpace)?; + chunk[0] = (buffer >> 40) as u8; + chunk[1] = (buffer >> 32) as u8; + chunk[2] = (buffer >> 24) as u8; + + buffer <<= 24; + pad_mask >>= 4; + used -= 4; + output_offset += 3; + } + + match (used, pad_mask) { + // no trailing bytes + (0, 0b0000) => {} + + // 4 trailing bytes, no padding + (4, 0b0000) => { + let chunk = output + .get_mut(output_offset..output_offset + 3) + .ok_or(Error::InsufficientOutputSpace)?; + chunk[0] = (buffer >> 40) as u8; + chunk[1] = (buffer >> 32) as u8; + chunk[2] = (buffer >> 24) as u8; + output_offset += 3; + } + + // 4 trailing bytes with one padding char, or 3 trailing bytes + (4, 0b1000) | (3, 0b0000) => { + let chunk = output + .get_mut(output_offset..output_offset + 2) + .ok_or(Error::InsufficientOutputSpace)?; + + chunk[0] = (buffer >> 40) as u8; + chunk[1] = (buffer >> 32) as u8; + output_offset += 2; + } + + // 4 trailing bytes with two padding char, or 2 trailing bytes + (4, 0b1100) | (2, 0b0000) => { + let chunk = output + .get_mut(output_offset..output_offset + 1) + .ok_or(Error::InsufficientOutputSpace)?; + chunk[0] = (buffer >> 40) as u8; + output_offset += 1; + } + + // everything else is illegal + _ => return Err(Error::InvalidTrailingPadding), + } + + Ok(&output[..output_offset]) +} + +#[derive(Debug, PartialEq)] +pub(crate) enum Error { + /// Given character is not valid in base64 alphabet. + InvalidCharacter(u8), + + /// A padding character (`=`) appeared outside the final + /// block of 4 characters. + PrematurePadding, + + /// The padding characters at the end of the input were invalid. + InvalidTrailingPadding, + + /// Not enough space in output buffer. + /// + /// Use `decoded_length` to get an upper bound. + InsufficientOutputSpace, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +struct CodePoint(u8); + +impl CodePoint { + const WHITESPACE: Self = Self(0xf0); + const PAD: Self = Self(0xf1); + const INVALID: Self = Self(0xf2); +} + +impl CodePoint { + /// Side-channel rules: + /// + /// - code paths that produce `CodePoint(n)` must not make + /// `n` observable via a side channel. + /// - other code paths -- whitespace, padding or invalid -- need not, + /// these are not considered secret conditions. + fn decode_secret(b: u8) -> Self { + let is_upper = u8_in_range(b, b'A', b'Z'); + let is_lower = u8_in_range(b, b'a', b'z'); + let is_digit = u8_in_range(b, b'0', b'9'); + let is_plus = u8_equals(b, b'+'); + let is_slash = u8_equals(b, b'/'); + let is_pad = u8_equals(b, b'='); + let is_space = u8_in_range(b, b'\t', b'\r') | u8_equals(b, b' '); + + let is_invalid = !(is_lower | is_upper | is_digit | is_plus | is_slash | is_pad | is_space); + + Self( + (is_upper & b.wrapping_sub(b'A')) + | (is_lower & (b.wrapping_sub(b'a').wrapping_add(26))) + | (is_digit & (b.wrapping_sub(b'0').wrapping_add(52))) + | (is_plus & 62) + | (is_slash & 63) + | (is_space & Self::WHITESPACE.0) + | (is_pad & Self::PAD.0) + | (is_invalid & Self::INVALID.0), + ) + } + + fn decode_public(a: u8) -> Self { + const TABLE: [CodePoint; 256] = [ + // 0x00..0x0f + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::WHITESPACE, + CodePoint::WHITESPACE, + CodePoint::WHITESPACE, + CodePoint::WHITESPACE, + CodePoint::WHITESPACE, + CodePoint::INVALID, + CodePoint::INVALID, + // 0x10..0x1f + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0x20..0x2f + CodePoint::WHITESPACE, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint(62), + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint(63), + // 0x30..0x3f + CodePoint(52), + CodePoint(53), + CodePoint(54), + CodePoint(55), + CodePoint(56), + CodePoint(57), + CodePoint(58), + CodePoint(59), + CodePoint(60), + CodePoint(61), + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::PAD, + CodePoint::INVALID, + CodePoint::INVALID, + // 0x40..0x4f + CodePoint::INVALID, + CodePoint(0), + CodePoint(1), + CodePoint(2), + CodePoint(3), + CodePoint(4), + CodePoint(5), + CodePoint(6), + CodePoint(7), + CodePoint(8), + CodePoint(9), + CodePoint(10), + CodePoint(11), + CodePoint(12), + CodePoint(13), + CodePoint(14), + // 0x50..0x5f + CodePoint(15), + CodePoint(16), + CodePoint(17), + CodePoint(18), + CodePoint(19), + CodePoint(20), + CodePoint(21), + CodePoint(22), + CodePoint(23), + CodePoint(24), + CodePoint(25), + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0x60..0x6f + CodePoint::INVALID, + CodePoint(26), + CodePoint(27), + CodePoint(28), + CodePoint(29), + CodePoint(30), + CodePoint(31), + CodePoint(32), + CodePoint(33), + CodePoint(34), + CodePoint(35), + CodePoint(36), + CodePoint(37), + CodePoint(38), + CodePoint(39), + CodePoint(40), + // 0x70..0x7f + CodePoint(41), + CodePoint(42), + CodePoint(43), + CodePoint(44), + CodePoint(45), + CodePoint(46), + CodePoint(47), + CodePoint(48), + CodePoint(49), + CodePoint(50), + CodePoint(51), + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0x80..0x8f + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0x90..0x9f + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0xa0..0xaf + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0xb0..0xbf + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0xc0..0xcf + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0xd0..0xdf + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0xe0..0xef + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + // 0xf0..0xff + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + CodePoint::INVALID, + ]; + + TABLE[a as usize] + } +} + +/// Returns 0xff if `a` in `lo..=hi`. +/// +/// lo..=hi must not be 0..=255. Callers in this file have constant +/// `lo` and `hi`, and this function is private to this file. +fn u8_in_range(a: u8, lo: u8, hi: u8) -> u8 { + debug_assert!(lo <= hi); + debug_assert!(hi - lo != 255); + let a = a.wrapping_sub(lo); + u8_less_than(a, (hi - lo).wrapping_add(1)) +} + +/// Returns 0xff if a < b, 0 otherwise. +fn u8_less_than(a: u8, b: u8) -> u8 { + let a = u16::from(a); + let b = u16::from(b); + u8_broadcast16(a.wrapping_sub(b)) +} + +/// Returns 0xff if a == b, 0 otherwise. +fn u8_equals(a: u8, b: u8) -> u8 { + let diff = a ^ b; + u8_nonzero(diff) +} + +/// Returns 0xff if a != 0, 0 otherwise. +fn u8_nonzero(x: u8) -> u8 { + u8_broadcast8(!x & x.wrapping_sub(1)) +} + +/// Broadcasts the top bit of `x` +/// +/// In other words, if the top bit of `x` is set, +/// returns 0xff else 0x00. +fn u8_broadcast8(x: u8) -> u8 { + let msb = x >> 7; + 0u8.wrapping_sub(msb) +} + +/// Broadcasts the top bit of `x` +/// +/// In other words, if the top bit of `x` is set, +/// returns 0xff else 0x00. +fn u8_broadcast16(x: u16) -> u8 { + let msb = x >> 15; + 0u8.wrapping_sub(msb as u8) +} + +#[cfg(all(test, feature = "alloc"))] +mod tests { + use super::*; + + #[test] + fn decode_test() { + assert_eq!( + decode(b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"), + b"\x00\x10\x83\x10\x51\x87\x20\x92\x8b\x30\xd3\x8f\x41\x14\x93\x51\x55\x97\ + \x61\x96\x9b\x71\xd7\x9f\x82\x18\xa3\x92\x59\xa7\xa2\x9a\xab\xb2\xdb\xaf\ + \xc3\x1c\xb3\xd3\x5d\xb7\xe3\x9e\xbb\xf3\xdf\xbf" + ); + assert_eq!(decode(b"aGVsbG8="), b"hello"); + assert_eq!(decode(b"aGVsbG8gd29ybGQ="), b"hello world"); + assert_eq!(decode(b"aGVsbG8gd29ybGQh"), b"hello world!"); + assert_eq!(decode(b"////"), b"\xff\xff\xff"); + assert_eq!(decode(b"++++"), b"\xfb\xef\xbe"); + assert_eq!(decode(b"AAAA"), b"\x00\x00\x00"); + assert_eq!(decode(b"AAA="), b"\x00\x00"); + assert_eq!(decode(b"AA=="), b"\x00"); + + // like our previous use of rust-base64, we don't require padding + // if the encoding is otherwise valid given the length + assert_eq!(decode(b"AAA"), b"\x00\x00"); + assert_eq!(decode(b"AA"), b"\x00"); + + assert_eq!(decode(b""), b""); + } + + #[test] + fn decode_errors() { + let mut buf = [0u8; 6]; + + // illegal trailing padding + assert_eq!( + decode_both(b"A===", &mut buf), + Err(Error::InvalidTrailingPadding) + ); + assert_eq!( + decode_both(b"====", &mut buf), + Err(Error::InvalidTrailingPadding) + ); + assert_eq!( + decode_both(b"A==", &mut buf), + Err(Error::InvalidTrailingPadding) + ); + assert_eq!( + decode_both(b"AA=", &mut buf), + Err(Error::InvalidTrailingPadding) + ); + assert_eq!( + decode_both(b"A", &mut buf), + Err(Error::InvalidTrailingPadding) + ); + + // padding before final block + assert_eq!( + decode_both(b"=AAAAA==", &mut buf), + Err(Error::PrematurePadding) + ); + assert_eq!( + decode_both(b"A=AAAA==", &mut buf), + Err(Error::PrematurePadding) + ); + assert_eq!( + decode_both(b"AA=AAA==", &mut buf), + Err(Error::PrematurePadding) + ); + assert_eq!( + decode_both(b"AAA=AA==", &mut buf), + Err(Error::PrematurePadding) + ); + + // illegal inputs + assert_eq!( + decode_both(b"%AAA", &mut buf), + Err(Error::InvalidCharacter(b'%')) + ); + assert_eq!( + decode_both(b"A%AA", &mut buf), + Err(Error::InvalidCharacter(b'%')) + ); + assert_eq!( + decode_both(b"AA%A", &mut buf), + Err(Error::InvalidCharacter(b'%')) + ); + assert_eq!( + decode_both(b"AAA%", &mut buf), + Err(Error::InvalidCharacter(b'%')) + ); + + // output sizing + assert_eq!(decode_both(b"am9lIGJw", &mut [0u8; 7]), Ok(&b"joe bp"[..])); + assert_eq!(decode_both(b"am9lIGJw", &mut [0u8; 6]), Ok(&b"joe bp"[..])); + assert_eq!( + decode_both(b"am9lIGJw", &mut [0u8; 5]), + Err(Error::InsufficientOutputSpace) + ); + assert_eq!( + decode_both(b"am9lIGJw", &mut [0u8; 4]), + Err(Error::InsufficientOutputSpace) + ); + assert_eq!( + decode_both(b"am9lIGJw", &mut [0u8; 3]), + Err(Error::InsufficientOutputSpace) + ); + + // output sizing is not pessimistic when padding is valid + assert_eq!(decode_both(b"am9=", &mut [0u8; 2]), Ok(&b"jo"[..])); + assert_eq!(decode_both(b"am==", &mut [0u8; 1]), Ok(&b"j"[..])); + assert_eq!(decode_both(b"am9", &mut [0u8; 2]), Ok(&b"jo"[..])); + assert_eq!(decode_both(b"am", &mut [0u8; 1]), Ok(&b"j"[..])); + } + + #[test] + fn check_models() { + fn u8_broadcast8_model(x: u8) -> u8 { + match x & 0x80 { + 0x80 => 0xff, + _ => 0x00, + } + } + + fn u8_broadcast16_model(x: u16) -> u8 { + match x & 0x8000 { + 0x8000 => 0xff, + _ => 0x00, + } + } + + fn u8_nonzero_model(x: u8) -> u8 { + match x { + 0 => 0xff, + _ => 0x00, + } + } + + fn u8_equals_model(x: u8, y: u8) -> u8 { + match x == y { + true => 0xff, + false => 0x00, + } + } + + fn u8_in_range_model(x: u8, y: u8, z: u8) -> u8 { + match (y..=z).contains(&x) { + true => 0xff, + false => 0x00, + } + } + + for x in u8::MIN..=u8::MAX { + assert_eq!(u8_broadcast8(x), u8_broadcast8_model(x)); + assert_eq!(u8_nonzero(x), u8_nonzero_model(x)); + assert_eq!(CodePoint::decode_secret(x), CodePoint::decode_public(x)); + + for y in u8::MIN..=u8::MAX { + assert_eq!(u8_equals(x, y), u8_equals_model(x, y)); + + let v = (x as u16) | ((y as u16) << 8); + assert_eq!(u8_broadcast16(v), u8_broadcast16_model(v)); + + for z in y..=u8::MAX { + if z - y == 255 { + continue; + } + assert_eq!(u8_in_range(x, y, z), u8_in_range_model(x, y, z)); + } + } + } + } + + #[cfg(all(feature = "std", target_os = "linux", target_arch = "x86_64"))] + #[test] + fn codepoint_decode_secret_does_not_branch_or_index_on_secret_input() { + // this is using the same theory as + use crabgrind as cg; + + if matches!(cg::run_mode(), cg::RunMode::Native) { + std::println!("SKIPPED: must be run under valgrind"); + return; + } + + let input = [b'a']; + cg::monitor_command(format!( + "make_memory undefined {:p} {}", + input.as_ptr(), + input.len() + )) + .unwrap(); + + core::hint::black_box(CodePoint::decode_secret(input[0])); + } + + #[track_caller] + fn decode(input: &[u8]) -> alloc::vec::Vec { + let length = decoded_length(input.len()); + + let mut v = alloc::vec![0u8; length]; + let used = decode_both(input, &mut v).unwrap().len(); + v.truncate(used); + + v + } + + fn decode_both<'a>(input: &'_ [u8], output: &'a mut [u8]) -> Result<&'a [u8], Error> { + let mut output_copy = output.to_vec(); + let r_pub = decode_public(input, &mut output_copy); + + let r_sec = decode_secret(input, output); + + assert_eq!(r_pub, r_sec); + + r_sec + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..20dc4b9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,1136 @@ +//! This crate provides types for representing X.509 certificates, keys and other types as +//! commonly used in the rustls ecosystem. It is intended to be used by crates that need to work +//! with such X.509 types, such as [rustls](https://crates.io/crates/rustls), +//! [rustls-webpki](https://crates.io/crates/rustls-webpki), +//! [rustls-pemfile](https://crates.io/crates/rustls-pemfile), and others. +//! +//! Some of these crates used to define their own trivial wrappers around DER-encoded bytes. +//! However, in order to avoid inconvenient dependency edges, these were all disconnected. By +//! using a common low-level crate of types with long-term stable API, we hope to avoid the +//! downsides of unnecessary dependency edges while providing good interoperability between crates. +//! +//! ## DER and PEM +//! +//! Many of the types defined in this crate represent DER-encoded data. DER is a binary encoding of +//! the ASN.1 format commonly used in web PKI specifications. It is a binary encoding, so it is +//! relatively compact when stored in memory. However, as a binary format, it is not very easy to +//! work with for humans and in contexts where binary data is inconvenient. For this reason, +//! many tools and protocols use a ASCII-based encoding of DER, called PEM. In addition to the +//! base64-encoded DER, PEM objects are delimited by header and footer lines which indicate the type +//! of object contained in the PEM blob. +//! +//! Types here can be created from: +//! +//! - DER using (for example) [`PrivatePkcs8KeyDer::from()`]. +//! - PEM using (for example) [`pem::PemObject::from_pem_slice()`]. +//! +//! The [`pem::PemObject`] trait contains the full selection of ways to construct +//! these types from PEM encodings. That includes ways to open and read from a file, +//! from a slice, or from an `std::io` stream. +//! +//! There is also a lower-level API that allows a given PEM file to be fully consumed +//! in one pass, even if it contains different data types: see the implementation of +//! the [`pem::PemObject`] trait on the `(pem::SectionKind, Vec)` tuple. +//! +//! ## Creating new certificates and keys +//! +//! This crate does not provide any functionality for creating new certificates or keys. However, +//! the [rcgen](https://docs.rs/rcgen) crate can be used to create new certificates and keys. +//! +//! ## Cloning private keys +//! +//! This crate intentionally **does not** implement `Clone` on private key types in +//! order to minimize the exposure of private key data in memory. +//! +//! If you want to extend the lifetime of a `PrivateKeyDer<'_>`, consider [`PrivateKeyDer::clone_key()`]. +//! Alternatively since these types are immutable, consider wrapping the `PrivateKeyDer<'_>` in a [`Rc`] +//! or an [`Arc`]. +//! +//! [`Rc`]: https://doc.rust-lang.org/std/rc/struct.Rc.html +//! [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html +//! [`PrivateKeyDer::clone_key()`]: https://docs.rs/rustls-pki-types/latest/rustls_pki_types/enum.PrivateKeyDer.html#method.clone_key +//! +//! ## Target `wasm32-unknown-unknown` with the `web` feature +//! +//! [`std::time::SystemTime`](https://doc.rust-lang.org/std/time/struct.SystemTime.html) +//! is unavailable in `wasm32-unknown-unknown` targets, so calls to +//! [`UnixTime::now()`](https://docs.rs/rustls-pki-types/latest/rustls_pki_types/struct.UnixTime.html#method.now), +//! otherwise enabled by the [`std`](https://docs.rs/crate/rustls-pki-types/latest/features#std) feature, +//! require building instead with the [`web`](https://docs.rs/crate/rustls-pki-types/latest/features#web) +//! feature. It gets time by calling [`Date.now()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now) +//! in the browser. + +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(unreachable_pub, clippy::use_self)] +#![deny(missing_docs)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(feature = "alloc")] +use alloc::vec::Vec; +use core::fmt; +use core::ops::Deref; +use core::time::Duration; +#[cfg(feature = "alloc")] +use pem::{PemObject, PemObjectFilter, SectionKind}; +#[cfg(all( + feature = "std", + not(all(target_family = "wasm", target_os = "unknown")) +))] +use std::time::SystemTime; +#[cfg(all(target_family = "wasm", target_os = "unknown", feature = "web"))] +use web_time::SystemTime; + +mod base64; +mod server_name; + +/// Low-level PEM decoding APIs. +/// +/// These APIs allow decoding PEM format in an iterator, which means you +/// can load multiple different types of PEM section from a file in a single +/// pass. +#[cfg(feature = "alloc")] +pub mod pem; + +pub use server_name::{ + AddrParseError, DnsName, InvalidDnsNameError, IpAddr, Ipv4Addr, Ipv6Addr, ServerName, +}; + +/// A DER-encoded X.509 private key, in one of several formats +/// +/// See variant inner types for more detailed information. +/// +/// This can load several types of PEM-encoded private key, and then reveal +/// which types were found: +/// +/// ```rust +/// # #[cfg(all(feature = "alloc", feature = "std"))] { +/// use rustls_pki_types::{PrivateKeyDer, pem::PemObject}; +/// +/// // load from a PEM file +/// let pkcs8 = PrivateKeyDer::from_pem_file("tests/data/nistp256key.pkcs8.pem").unwrap(); +/// let pkcs1 = PrivateKeyDer::from_pem_file("tests/data/rsa1024.pkcs1.pem").unwrap(); +/// let sec1 = PrivateKeyDer::from_pem_file("tests/data/nistp256key.pem").unwrap(); +/// assert!(matches!(pkcs8, PrivateKeyDer::Pkcs8(_))); +/// assert!(matches!(pkcs1, PrivateKeyDer::Pkcs1(_))); +/// assert!(matches!(sec1, PrivateKeyDer::Sec1(_))); +/// # } +/// ``` +#[non_exhaustive] +#[derive(Debug, PartialEq, Eq)] +pub enum PrivateKeyDer<'a> { + /// An RSA private key + Pkcs1(PrivatePkcs1KeyDer<'a>), + /// A Sec1 private key + Sec1(PrivateSec1KeyDer<'a>), + /// A PKCS#8 private key + Pkcs8(PrivatePkcs8KeyDer<'a>), +} + +impl PrivateKeyDer<'_> { + /// Clone the private key to a `'static` value + #[cfg(feature = "alloc")] + pub fn clone_key(&self) -> PrivateKeyDer<'static> { + use PrivateKeyDer::*; + match self { + Pkcs1(key) => Pkcs1(key.clone_key()), + Sec1(key) => Sec1(key.clone_key()), + Pkcs8(key) => Pkcs8(key.clone_key()), + } + } + + /// Yield the DER-encoded bytes of the private key + pub fn secret_der(&self) -> &[u8] { + match self { + PrivateKeyDer::Pkcs1(key) => key.secret_pkcs1_der(), + PrivateKeyDer::Sec1(key) => key.secret_sec1_der(), + PrivateKeyDer::Pkcs8(key) => key.secret_pkcs8_der(), + } + } +} + +#[cfg(feature = "alloc")] +impl PemObject for PrivateKeyDer<'static> { + fn from_pem(kind: SectionKind, value: Vec) -> Option { + match kind { + SectionKind::RsaPrivateKey => Some(Self::Pkcs1(value.into())), + SectionKind::EcPrivateKey => Some(Self::Sec1(value.into())), + SectionKind::PrivateKey => Some(Self::Pkcs8(value.into())), + _ => None, + } + } +} + +impl<'a> From> for PrivateKeyDer<'a> { + fn from(key: PrivatePkcs1KeyDer<'a>) -> Self { + Self::Pkcs1(key) + } +} + +impl<'a> From> for PrivateKeyDer<'a> { + fn from(key: PrivateSec1KeyDer<'a>) -> Self { + Self::Sec1(key) + } +} + +impl<'a> From> for PrivateKeyDer<'a> { + fn from(key: PrivatePkcs8KeyDer<'a>) -> Self { + Self::Pkcs8(key) + } +} + +impl<'a> TryFrom<&'a [u8]> for PrivateKeyDer<'a> { + type Error = &'static str; + + fn try_from(key: &'a [u8]) -> Result { + const SHORT_FORM_LEN_MAX: u8 = 128; + const TAG_SEQUENCE: u8 = 0x30; + const TAG_INTEGER: u8 = 0x02; + + // We expect all key formats to begin with a SEQUENCE, which requires at least 2 bytes + // in the short length encoding. + if key.first() != Some(&TAG_SEQUENCE) || key.len() < 2 { + return Err(INVALID_KEY_DER_ERR); + } + + // The length of the SEQUENCE is encoded in the second byte. We must skip this many bytes. + let skip_len = match key[1] >= SHORT_FORM_LEN_MAX { + // 1 byte for SEQUENCE tag, 1 byte for short-form len + false => 2, + // 1 byte for SEQUENCE tag, 1 byte for start of len, remaining bytes encoded + // in key[1]. + true => 2 + (key[1] - SHORT_FORM_LEN_MAX) as usize, + }; + let key_bytes = key.get(skip_len..).ok_or(INVALID_KEY_DER_ERR)?; + + // PKCS#8 (https://www.rfc-editor.org/rfc/rfc5208) describes the PrivateKeyInfo + // structure as: + // PrivateKeyInfo ::= SEQUENCE { + // version Version, + // privateKeyAlgorithm AlgorithmIdentifier {{PrivateKeyAlgorithms}}, + // privateKey PrivateKey, + // attributes [0] Attributes OPTIONAL + // } + // PKCS#5 (https://www.rfc-editor.org/rfc/rfc8018) describes the AlgorithmIdentifier + // as a SEQUENCE. + // + // Therefore, we consider the outer SEQUENCE, a version number, and the start of + // an AlgorithmIdentifier to be enough to identify a PKCS#8 key. If it were PKCS#1 or SEC1 + // the version would not be followed by a SEQUENCE. + if matches!(key_bytes, [TAG_INTEGER, 0x01, _, TAG_SEQUENCE, ..]) { + return Ok(Self::Pkcs8(key.into())); + } + + // PKCS#1 (https://www.rfc-editor.org/rfc/rfc8017) describes the RSAPrivateKey structure + // as: + // RSAPrivateKey ::= SEQUENCE { + // version Version, + // modulus INTEGER, -- n + // publicExponent INTEGER, -- e + // privateExponent INTEGER, -- d + // prime1 INTEGER, -- p + // prime2 INTEGER, -- q + // exponent1 INTEGER, -- d mod (p-1) + // exponent2 INTEGER, -- d mod (q-1) + // coefficient INTEGER, -- (inverse of q) mod p + // otherPrimeInfos OtherPrimeInfos OPTIONAL + // } + // + // Therefore, we consider the outer SEQUENCE and a Version of 0 to be enough to identify + // a PKCS#1 key. If it were PKCS#8, the version would be followed by a SEQUENCE. If it + // were SEC1, the VERSION would have been 1. + if key_bytes.starts_with(&[TAG_INTEGER, 0x01, 0x00]) { + return Ok(Self::Pkcs1(key.into())); + } + + // SEC1 (https://www.rfc-editor.org/rfc/rfc5915) describes the ECPrivateKey structure as: + // ECPrivateKey ::= SEQUENCE { + // version INTEGER { ecPrivkeyVer1(1) } (ecPrivkeyVer1), + // privateKey OCTET STRING, + // parameters [0] ECParameters {{ NamedCurve }} OPTIONAL, + // publicKey [1] BIT STRING OPTIONAL + // } + // + // Therefore, we consider the outer SEQUENCE and an INTEGER of 1 to be enough to + // identify a SEC1 key. If it were PKCS#8 or PKCS#1, the version would have been 0. + if key_bytes.starts_with(&[TAG_INTEGER, 0x01, 0x01]) { + return Ok(Self::Sec1(key.into())); + } + + Err(INVALID_KEY_DER_ERR) + } +} + +static INVALID_KEY_DER_ERR: &str = "unknown or invalid key format"; + +#[cfg(feature = "alloc")] +impl TryFrom> for PrivateKeyDer<'_> { + type Error = &'static str; + + fn try_from(key: Vec) -> Result { + Ok(match PrivateKeyDer::try_from(&key[..])? { + PrivateKeyDer::Pkcs1(_) => Self::Pkcs1(key.into()), + PrivateKeyDer::Sec1(_) => Self::Sec1(key.into()), + PrivateKeyDer::Pkcs8(_) => Self::Pkcs8(key.into()), + }) + } +} + +/// A DER-encoded plaintext RSA private key; as specified in PKCS#1/RFC 3447 +/// +/// RSA private keys are identified in PEM context as `RSA PRIVATE KEY` and when stored in a +/// file usually use a `.pem` or `.key` extension. +/// +/// ```rust +/// # #[cfg(all(feature = "alloc", feature = "std"))] { +/// use rustls_pki_types::{PrivatePkcs1KeyDer, pem::PemObject}; +/// +/// // load from a PEM file +/// PrivatePkcs1KeyDer::from_pem_file("tests/data/rsa1024.pkcs1.pem").unwrap(); +/// +/// // or from a PEM byte slice... +/// # let byte_slice = include_bytes!("../tests/data/rsa1024.pkcs1.pem"); +/// PrivatePkcs1KeyDer::from_pem_slice(byte_slice).unwrap(); +/// # } +/// ``` +#[derive(PartialEq, Eq)] +pub struct PrivatePkcs1KeyDer<'a>(Der<'a>); + +impl PrivatePkcs1KeyDer<'_> { + /// Clone the private key to a `'static` value + #[cfg(feature = "alloc")] + pub fn clone_key(&self) -> PrivatePkcs1KeyDer<'static> { + PrivatePkcs1KeyDer::from(self.0.as_ref().to_vec()) + } + + /// Yield the DER-encoded bytes of the private key + pub fn secret_pkcs1_der(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[cfg(feature = "alloc")] +impl PemObjectFilter for PrivatePkcs1KeyDer<'static> { + const KIND: SectionKind = SectionKind::RsaPrivateKey; +} + +impl<'a> From<&'a [u8]> for PrivatePkcs1KeyDer<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(Der(BytesInner::Borrowed(slice))) + } +} + +#[cfg(feature = "alloc")] +impl From> for PrivatePkcs1KeyDer<'_> { + fn from(vec: Vec) -> Self { + Self(Der(BytesInner::Owned(vec))) + } +} + +impl fmt::Debug for PrivatePkcs1KeyDer<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PrivatePkcs1KeyDer") + .field(&"[secret key elided]") + .finish() + } +} + +/// A Sec1-encoded plaintext private key; as specified in RFC 5915 +/// +/// Sec1 private keys are identified in PEM context as `EC PRIVATE KEY` and when stored in a +/// file usually use a `.pem` or `.key` extension. For more on PEM files, refer to the crate +/// documentation. +/// +/// ```rust +/// # #[cfg(all(feature = "alloc", feature = "std"))] { +/// use rustls_pki_types::{PrivateSec1KeyDer, pem::PemObject}; +/// +/// // load from a PEM file +/// PrivateSec1KeyDer::from_pem_file("tests/data/nistp256key.pem").unwrap(); +/// +/// // or from a PEM byte slice... +/// # let byte_slice = include_bytes!("../tests/data/nistp256key.pem"); +/// PrivateSec1KeyDer::from_pem_slice(byte_slice).unwrap(); +/// # } +/// ``` +#[derive(PartialEq, Eq)] +pub struct PrivateSec1KeyDer<'a>(Der<'a>); + +impl PrivateSec1KeyDer<'_> { + /// Clone the private key to a `'static` value + #[cfg(feature = "alloc")] + pub fn clone_key(&self) -> PrivateSec1KeyDer<'static> { + PrivateSec1KeyDer::from(self.0.as_ref().to_vec()) + } + + /// Yield the DER-encoded bytes of the private key + pub fn secret_sec1_der(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[cfg(feature = "alloc")] +impl PemObjectFilter for PrivateSec1KeyDer<'static> { + const KIND: SectionKind = SectionKind::EcPrivateKey; +} + +impl<'a> From<&'a [u8]> for PrivateSec1KeyDer<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(Der(BytesInner::Borrowed(slice))) + } +} + +#[cfg(feature = "alloc")] +impl From> for PrivateSec1KeyDer<'_> { + fn from(vec: Vec) -> Self { + Self(Der(BytesInner::Owned(vec))) + } +} + +impl fmt::Debug for PrivateSec1KeyDer<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PrivateSec1KeyDer") + .field(&"[secret key elided]") + .finish() + } +} + +/// A DER-encoded plaintext private key; as specified in PKCS#8/RFC 5958 +/// +/// PKCS#8 private keys are identified in PEM context as `PRIVATE KEY` and when stored in a +/// file usually use a `.pem` or `.key` extension. For more on PEM files, refer to the crate +/// documentation. +/// +/// ```rust +/// # #[cfg(all(feature = "alloc", feature = "std"))] { +/// use rustls_pki_types::{PrivatePkcs8KeyDer, pem::PemObject}; +/// +/// // load from a PEM file +/// PrivatePkcs8KeyDer::from_pem_file("tests/data/nistp256key.pkcs8.pem").unwrap(); +/// PrivatePkcs8KeyDer::from_pem_file("tests/data/rsa1024.pkcs8.pem").unwrap(); +/// +/// // or from a PEM byte slice... +/// # let byte_slice = include_bytes!("../tests/data/nistp256key.pkcs8.pem"); +/// PrivatePkcs8KeyDer::from_pem_slice(byte_slice).unwrap(); +/// # } +/// ``` +#[derive(PartialEq, Eq)] +pub struct PrivatePkcs8KeyDer<'a>(Der<'a>); + +impl PrivatePkcs8KeyDer<'_> { + /// Clone the private key to a `'static` value + #[cfg(feature = "alloc")] + pub fn clone_key(&self) -> PrivatePkcs8KeyDer<'static> { + PrivatePkcs8KeyDer::from(self.0.as_ref().to_vec()) + } + + /// Yield the DER-encoded bytes of the private key + pub fn secret_pkcs8_der(&self) -> &[u8] { + self.0.as_ref() + } +} + +#[cfg(feature = "alloc")] +impl PemObjectFilter for PrivatePkcs8KeyDer<'static> { + const KIND: SectionKind = SectionKind::PrivateKey; +} + +impl<'a> From<&'a [u8]> for PrivatePkcs8KeyDer<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(Der(BytesInner::Borrowed(slice))) + } +} + +#[cfg(feature = "alloc")] +impl From> for PrivatePkcs8KeyDer<'_> { + fn from(vec: Vec) -> Self { + Self(Der(BytesInner::Owned(vec))) + } +} + +impl fmt::Debug for PrivatePkcs8KeyDer<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("PrivatePkcs8KeyDer") + .field(&"[secret key elided]") + .finish() + } +} + +/// A trust anchor (a.k.a. root CA) +/// +/// Traditionally, certificate verification libraries have represented trust anchors as full X.509 +/// root certificates. However, those certificates contain a lot more data than is needed for +/// verifying certificates. The [`TrustAnchor`] representation allows an application to store +/// just the essential elements of trust anchors. +/// +/// The most common way to get one of these is to call [`rustls_webpki::anchor_from_trusted_cert()`]. +/// +/// [`rustls_webpki::anchor_from_trusted_cert()`]: https://docs.rs/rustls-webpki/latest/webpki/fn.anchor_from_trusted_cert.html +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TrustAnchor<'a> { + /// Value of the `subject` field of the trust anchor + pub subject: Der<'a>, + /// Value of the `subjectPublicKeyInfo` field of the trust anchor + pub subject_public_key_info: Der<'a>, + /// Value of DER-encoded `NameConstraints`, containing name constraints to the trust anchor, if any + pub name_constraints: Option>, +} + +impl TrustAnchor<'_> { + /// Yield a `'static` lifetime of the `TrustAnchor` by allocating owned `Der` variants + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> TrustAnchor<'static> { + #[cfg(not(feature = "std"))] + use alloc::borrow::ToOwned; + TrustAnchor { + subject: self.subject.as_ref().to_owned().into(), + subject_public_key_info: self.subject_public_key_info.as_ref().to_owned().into(), + name_constraints: self + .name_constraints + .as_ref() + .map(|nc| nc.as_ref().to_owned().into()), + } + } +} + +/// A Certificate Revocation List; as specified in RFC 5280 +/// +/// Certificate revocation lists are identified in PEM context as `X509 CRL` and when stored in a +/// file usually use a `.crl` extension. For more on PEM files, refer to the crate documentation. +/// +/// ```rust +/// # #[cfg(all(feature = "alloc", feature = "std"))] { +/// use rustls_pki_types::{CertificateRevocationListDer, pem::PemObject}; +/// +/// // load several from a PEM file +/// let crls: Vec<_> = CertificateRevocationListDer::pem_file_iter("tests/data/crl.pem") +/// .unwrap() +/// .collect(); +/// assert!(crls.len() >= 1); +/// +/// // or one from a PEM byte slice... +/// # let byte_slice = include_bytes!("../tests/data/crl.pem"); +/// CertificateRevocationListDer::from_pem_slice(byte_slice).unwrap(); +/// +/// // or several from a PEM byte slice +/// let crls: Vec<_> = CertificateRevocationListDer::pem_slice_iter(byte_slice) +/// .collect(); +/// assert!(crls.len() >= 1); +/// # } +/// ``` + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CertificateRevocationListDer<'a>(Der<'a>); + +#[cfg(feature = "alloc")] +impl PemObjectFilter for CertificateRevocationListDer<'static> { + const KIND: SectionKind = SectionKind::Crl; +} + +impl AsRef<[u8]> for CertificateRevocationListDer<'_> { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Deref for CertificateRevocationListDer<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl<'a> From<&'a [u8]> for CertificateRevocationListDer<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(Der::from(slice)) + } +} + +#[cfg(feature = "alloc")] +impl From> for CertificateRevocationListDer<'_> { + fn from(vec: Vec) -> Self { + Self(Der::from(vec)) + } +} + +/// A Certificate Signing Request; as specified in RFC 2986 +/// +/// Certificate signing requests are identified in PEM context as `CERTIFICATE REQUEST` and when stored in a +/// file usually use a `.csr` extension. For more on PEM files, refer to the crate documentation. +/// +/// ```rust +/// # #[cfg(all(feature = "alloc", feature = "std"))] { +/// use rustls_pki_types::{CertificateSigningRequestDer, pem::PemObject}; +/// +/// // load from a PEM file +/// CertificateSigningRequestDer::from_pem_file("tests/data/csr.pem").unwrap(); +/// +/// // or from a PEM byte slice... +/// # let byte_slice = include_bytes!("../tests/data/csr.pem"); +/// CertificateSigningRequestDer::from_pem_slice(byte_slice).unwrap(); +/// # } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CertificateSigningRequestDer<'a>(Der<'a>); + +#[cfg(feature = "alloc")] +impl PemObjectFilter for CertificateSigningRequestDer<'static> { + const KIND: SectionKind = SectionKind::Csr; +} + +impl AsRef<[u8]> for CertificateSigningRequestDer<'_> { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Deref for CertificateSigningRequestDer<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl<'a> From<&'a [u8]> for CertificateSigningRequestDer<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(Der::from(slice)) + } +} + +#[cfg(feature = "alloc")] +impl From> for CertificateSigningRequestDer<'_> { + fn from(vec: Vec) -> Self { + Self(Der::from(vec)) + } +} + +/// A DER-encoded X.509 certificate; as specified in RFC 5280 +/// +/// Certificates are identified in PEM context as `CERTIFICATE` and when stored in a +/// file usually use a `.pem`, `.cer` or `.crt` extension. For more on PEM files, refer to the +/// crate documentation. +/// +/// ```rust +/// # #[cfg(all(feature = "alloc", feature = "std"))] { +/// use rustls_pki_types::{CertificateDer, pem::PemObject}; +/// +/// // load several from a PEM file +/// let certs: Vec<_> = CertificateDer::pem_file_iter("tests/data/certificate.chain.pem") +/// .unwrap() +/// .collect(); +/// assert_eq!(certs.len(), 3); +/// +/// // or one from a PEM byte slice... +/// # let byte_slice = include_bytes!("../tests/data/certificate.chain.pem"); +/// CertificateDer::from_pem_slice(byte_slice).unwrap(); +/// +/// // or several from a PEM byte slice +/// let certs: Vec<_> = CertificateDer::pem_slice_iter(byte_slice) +/// .collect(); +/// assert_eq!(certs.len(), 3); +/// # } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CertificateDer<'a>(Der<'a>); + +impl<'a> CertificateDer<'a> { + /// A const constructor to create a `CertificateDer` from a slice of DER. + pub const fn from_slice(bytes: &'a [u8]) -> Self { + Self(Der::from_slice(bytes)) + } +} + +#[cfg(feature = "alloc")] +impl PemObjectFilter for CertificateDer<'static> { + const KIND: SectionKind = SectionKind::Certificate; +} + +impl AsRef<[u8]> for CertificateDer<'_> { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Deref for CertificateDer<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl<'a> From<&'a [u8]> for CertificateDer<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(Der::from(slice)) + } +} + +#[cfg(feature = "alloc")] +impl From> for CertificateDer<'_> { + fn from(vec: Vec) -> Self { + Self(Der::from(vec)) + } +} + +impl CertificateDer<'_> { + /// Converts this certificate into its owned variant, unfreezing borrowed content (if any) + #[cfg(feature = "alloc")] + pub fn into_owned(self) -> CertificateDer<'static> { + CertificateDer(Der(self.0 .0.into_owned())) + } +} + +/// A DER-encoded SubjectPublicKeyInfo (SPKI), as specified in RFC 5280. +#[deprecated(since = "1.7.0", note = "Prefer `SubjectPublicKeyInfoDer` instead")] +pub type SubjectPublicKeyInfo<'a> = SubjectPublicKeyInfoDer<'a>; + +/// A DER-encoded SubjectPublicKeyInfo (SPKI), as specified in RFC 5280. +/// +/// Public keys are identified in PEM context as a `PUBLIC KEY`. +/// +/// ```rust +/// # #[cfg(all(feature = "alloc", feature = "std"))] { +/// use rustls_pki_types::{SubjectPublicKeyInfoDer, pem::PemObject}; +/// +/// // load from a PEM file +/// SubjectPublicKeyInfoDer::from_pem_file("tests/data/spki.pem").unwrap(); +/// +/// // or from a PEM byte slice... +/// # let byte_slice = include_bytes!("../tests/data/spki.pem"); +/// SubjectPublicKeyInfoDer::from_pem_slice(byte_slice).unwrap(); +/// # } +/// ``` +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SubjectPublicKeyInfoDer<'a>(Der<'a>); + +#[cfg(feature = "alloc")] +impl PemObjectFilter for SubjectPublicKeyInfoDer<'static> { + const KIND: SectionKind = SectionKind::PublicKey; +} + +impl AsRef<[u8]> for SubjectPublicKeyInfoDer<'_> { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Deref for SubjectPublicKeyInfoDer<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl<'a> From<&'a [u8]> for SubjectPublicKeyInfoDer<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(Der::from(slice)) + } +} + +#[cfg(feature = "alloc")] +impl From> for SubjectPublicKeyInfoDer<'_> { + fn from(vec: Vec) -> Self { + Self(Der::from(vec)) + } +} + +impl SubjectPublicKeyInfoDer<'_> { + /// Converts this SubjectPublicKeyInfo into its owned variant, unfreezing borrowed content (if any) + #[cfg(feature = "alloc")] + pub fn into_owned(self) -> SubjectPublicKeyInfoDer<'static> { + SubjectPublicKeyInfoDer(Der(self.0 .0.into_owned())) + } +} + +/// A TLS-encoded Encrypted Client Hello (ECH) configuration list (`ECHConfigList`); as specified in +/// [draft-ietf-tls-esni-18 §4](https://datatracker.ietf.org/doc/html/draft-ietf-tls-esni-18#section-4) +#[derive(Clone, Eq, PartialEq)] +pub struct EchConfigListBytes<'a>(BytesInner<'a>); + +impl EchConfigListBytes<'_> { + /// Converts this config into its owned variant, unfreezing borrowed content (if any) + #[cfg(feature = "alloc")] + pub fn into_owned(self) -> EchConfigListBytes<'static> { + EchConfigListBytes(self.0.into_owned()) + } +} + +#[cfg(feature = "alloc")] +impl EchConfigListBytes<'static> { + /// Convert an iterator over PEM items into an `EchConfigListBytes` and private key. + /// + /// This handles the "ECHConfig file" format specified in + /// + /// + /// Use it like: + /// + /// ```rust + /// # #[cfg(all(feature = "alloc", feature = "std"))] { + /// # use rustls_pki_types::{EchConfigListBytes, pem::PemObject}; + /// let (config, key) = EchConfigListBytes::config_and_key_from_iter( + /// PemObject::pem_file_iter("tests/data/ech.pem").unwrap() + /// ).unwrap(); + /// # } + /// ``` + pub fn config_and_key_from_iter( + iter: impl Iterator), pem::Error>>, + ) -> Result<(Self, PrivatePkcs8KeyDer<'static>), pem::Error> { + let mut key = None; + let mut config = None; + + for item in iter { + let (kind, data) = item?; + match kind { + SectionKind::PrivateKey => { + key = PrivatePkcs8KeyDer::from_pem(kind, data); + } + SectionKind::EchConfigList => { + config = Self::from_pem(kind, data); + } + _ => continue, + }; + + if let (Some(_key), Some(_config)) = (&key, &config) { + return Ok((config.take().unwrap(), key.take().unwrap())); + } + } + + Err(pem::Error::NoItemsFound) + } +} + +#[cfg(feature = "alloc")] +impl PemObjectFilter for EchConfigListBytes<'static> { + const KIND: SectionKind = SectionKind::EchConfigList; +} + +impl fmt::Debug for EchConfigListBytes<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + hex(f, self.as_ref()) + } +} + +impl AsRef<[u8]> for EchConfigListBytes<'_> { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Deref for EchConfigListBytes<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl<'a> From<&'a [u8]> for EchConfigListBytes<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(BytesInner::Borrowed(slice)) + } +} + +#[cfg(feature = "alloc")] +impl From> for EchConfigListBytes<'_> { + fn from(vec: Vec) -> Self { + Self(BytesInner::Owned(vec)) + } +} + +/// An abstract signature verification algorithm. +/// +/// One of these is needed per supported pair of public key type (identified +/// with `public_key_alg_id()`) and `signatureAlgorithm` (identified with +/// `signature_alg_id()`). Note that both of these `AlgorithmIdentifier`s include +/// the parameters encoding, so separate `SignatureVerificationAlgorithm`s are needed +/// for each possible public key or signature parameters. +/// +/// Debug implementations should list the public key algorithm identifier and +/// signature algorithm identifier in human friendly form (i.e. not encoded bytes), +/// along with the name of the implementing library (to distinguish different +/// implementations of the same algorithms). +pub trait SignatureVerificationAlgorithm: Send + Sync + fmt::Debug { + /// Verify a signature. + /// + /// `public_key` is the `subjectPublicKey` value from a `SubjectPublicKeyInfo` encoding + /// and is untrusted. The key's `subjectPublicKeyInfo` matches the [`AlgorithmIdentifier`] + /// returned by `public_key_alg_id()`. + /// + /// `message` is the data over which the signature was allegedly computed. + /// It is not hashed; implementations of this trait function must do hashing + /// if that is required by the algorithm they implement. + /// + /// `signature` is the signature allegedly over `message`. + /// + /// Return `Ok(())` only if `signature` is a valid signature on `message`. + /// + /// Return `Err(InvalidSignature)` if the signature is invalid, including if the `public_key` + /// encoding is invalid. There is no need or opportunity to produce errors + /// that are more specific than this. + fn verify_signature( + &self, + public_key: &[u8], + message: &[u8], + signature: &[u8], + ) -> Result<(), InvalidSignature>; + + /// Return the `AlgorithmIdentifier` that must equal a public key's + /// `subjectPublicKeyInfo` value for this `SignatureVerificationAlgorithm` + /// to be used for signature verification. + fn public_key_alg_id(&self) -> AlgorithmIdentifier; + + /// Return the `AlgorithmIdentifier` that must equal the `signatureAlgorithm` value + /// on the data to be verified for this `SignatureVerificationAlgorithm` to be used + /// for signature verification. + fn signature_alg_id(&self) -> AlgorithmIdentifier; + + /// Return `true` if this is backed by a FIPS-approved implementation. + fn fips(&self) -> bool { + false + } +} + +/// A detail-less error when a signature is not valid. +#[derive(Debug, Copy, Clone)] +pub struct InvalidSignature; + +/// A DER encoding of the PKIX AlgorithmIdentifier type: +/// +/// ```ASN.1 +/// AlgorithmIdentifier ::= SEQUENCE { +/// algorithm OBJECT IDENTIFIER, +/// parameters ANY DEFINED BY algorithm OPTIONAL } +/// -- contains a value of the type +/// -- registered for use with the +/// -- algorithm object identifier value +/// ``` +/// (from ) +/// +/// The outer sequence encoding is *not included*, so this is the DER encoding +/// of an OID for `algorithm` plus the `parameters` value. +/// +/// For example, this is the `rsaEncryption` algorithm: +/// +/// ``` +/// let rsa_encryption = rustls_pki_types::AlgorithmIdentifier::from_slice( +/// &[ +/// // algorithm: 1.2.840.113549.1.1.1 +/// 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, +/// // parameters: NULL +/// 0x05, 0x00 +/// ] +/// ); +/// ``` +#[derive(Clone, Copy, PartialEq, Eq)] +pub struct AlgorithmIdentifier(&'static [u8]); + +impl AlgorithmIdentifier { + /// Makes a new `AlgorithmIdentifier` from a static octet slice. + /// + /// This does not validate the contents of the slice. + pub const fn from_slice(bytes: &'static [u8]) -> Self { + Self(bytes) + } +} + +impl AsRef<[u8]> for AlgorithmIdentifier { + fn as_ref(&self) -> &[u8] { + self.0 + } +} + +impl fmt::Debug for AlgorithmIdentifier { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + hex(f, self.0) + } +} + +impl Deref for AlgorithmIdentifier { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +/// A timestamp, tracking the number of non-leap seconds since the Unix epoch. +/// +/// The Unix epoch is defined January 1, 1970 00:00:00 UTC. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub struct UnixTime(u64); + +impl UnixTime { + /// The current time, as a `UnixTime` + #[cfg(any( + all( + feature = "std", + not(all(target_family = "wasm", target_os = "unknown")) + ), + all(target_family = "wasm", target_os = "unknown", feature = "web") + ))] + pub fn now() -> Self { + Self::since_unix_epoch( + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap(), // Safe: this code did not exist before 1970. + ) + } + + /// Convert a `Duration` since the start of 1970 to a `UnixTime` + /// + /// The `duration` must be relative to the Unix epoch. + pub fn since_unix_epoch(duration: Duration) -> Self { + Self(duration.as_secs()) + } + + /// Number of seconds since the Unix epoch + pub fn as_secs(&self) -> u64 { + self.0 + } +} + +/// DER-encoded data, either owned or borrowed +/// +/// This wrapper type is used to represent DER-encoded data in a way that is agnostic to whether +/// the data is owned (by a `Vec`) or borrowed (by a `&[u8]`). Support for the owned +/// variant is only available when the `alloc` feature is enabled. +#[derive(Clone, Eq, PartialEq)] +pub struct Der<'a>(BytesInner<'a>); + +impl<'a> Der<'a> { + /// A const constructor to create a `Der` from a borrowed slice + pub const fn from_slice(der: &'a [u8]) -> Self { + Self(BytesInner::Borrowed(der)) + } +} + +impl AsRef<[u8]> for Der<'_> { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl Deref for Der<'_> { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl<'a> From<&'a [u8]> for Der<'a> { + fn from(slice: &'a [u8]) -> Self { + Self(BytesInner::Borrowed(slice)) + } +} + +#[cfg(feature = "alloc")] +impl From> for Der<'static> { + fn from(vec: Vec) -> Self { + Self(BytesInner::Owned(vec)) + } +} + +impl fmt::Debug for Der<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + hex(f, self.as_ref()) + } +} + +#[derive(Debug, Clone)] +enum BytesInner<'a> { + #[cfg(feature = "alloc")] + Owned(Vec), + Borrowed(&'a [u8]), +} + +#[cfg(feature = "alloc")] +impl BytesInner<'_> { + fn into_owned(self) -> BytesInner<'static> { + BytesInner::Owned(match self { + Self::Owned(vec) => vec, + Self::Borrowed(slice) => slice.to_vec(), + }) + } +} + +impl AsRef<[u8]> for BytesInner<'_> { + fn as_ref(&self) -> &[u8] { + match &self { + #[cfg(feature = "alloc")] + BytesInner::Owned(vec) => vec.as_ref(), + BytesInner::Borrowed(slice) => slice, + } + } +} + +impl PartialEq for BytesInner<'_> { + fn eq(&self, other: &Self) -> bool { + self.as_ref() == other.as_ref() + } +} + +impl Eq for BytesInner<'_> {} + +// Format an iterator of u8 into a hex string +fn hex<'a>(f: &mut fmt::Formatter<'_>, payload: impl IntoIterator) -> fmt::Result { + for (i, b) in payload.into_iter().enumerate() { + if i == 0 { + write!(f, "0x")?; + } + write!(f, "{:02x}", b)?; + } + Ok(()) +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use super::*; + + #[test] + fn der_debug() { + let der = Der::from_slice(&[0x01, 0x02, 0x03]); + assert_eq!(format!("{:?}", der), "0x010203"); + } + + #[test] + fn alg_id_debug() { + let alg_id = AlgorithmIdentifier::from_slice(&[0x01, 0x02, 0x03]); + assert_eq!(format!("{:?}", alg_id), "0x010203"); + } + + #[test] + fn bytes_inner_equality() { + let owned_a = BytesInner::Owned(vec![1, 2, 3]); + let owned_b = BytesInner::Owned(vec![4, 5]); + let borrowed_a = BytesInner::Borrowed(&[1, 2, 3]); + let borrowed_b = BytesInner::Borrowed(&[99]); + + // Self-equality. + assert_eq!(owned_a, owned_a); + assert_eq!(owned_b, owned_b); + assert_eq!(borrowed_a, borrowed_a); + assert_eq!(borrowed_b, borrowed_b); + + // Borrowed vs Owned equality + assert_eq!(owned_a, borrowed_a); + assert_eq!(borrowed_a, owned_a); + + // Owned inequality + assert_ne!(owned_a, owned_b); + assert_ne!(owned_b, owned_a); + + // Borrowed inequality + assert_ne!(borrowed_a, borrowed_b); + assert_ne!(borrowed_b, borrowed_a); + + // Borrowed vs Owned inequality + assert_ne!(owned_a, borrowed_b); + assert_ne!(borrowed_b, owned_a); + } +} diff --git a/src/pem.rs b/src/pem.rs new file mode 100644 index 0000000..243979a --- /dev/null +++ b/src/pem.rs @@ -0,0 +1,482 @@ +use alloc::borrow::ToOwned; +use alloc::format; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; +use core::fmt; +use core::marker::PhantomData; +use core::ops::ControlFlow; +#[cfg(feature = "std")] +use std::fs::File; +#[cfg(feature = "std")] +use std::io::{self, ErrorKind}; + +use crate::base64; + +/// Items that can be decoded from PEM data. +pub trait PemObject: Sized { + /// Decode the first section of this type from PEM contained in + /// a byte slice. + /// + /// [`Error::NoItemsFound`] is returned if no such items are found. + fn from_pem_slice(pem: &[u8]) -> Result { + Self::pem_slice_iter(pem) + .next() + .unwrap_or(Err(Error::NoItemsFound)) + } + + /// Iterate over all sections of this type from PEM contained in + /// a byte slice. + fn pem_slice_iter(pem: &[u8]) -> SliceIter<'_, Self> { + SliceIter { + current: pem, + _ty: PhantomData, + } + } + + /// Decode the first section of this type from the PEM contents of the named file. + /// + /// [`Error::NoItemsFound`] is returned if no such items are found. + #[cfg(feature = "std")] + fn from_pem_file(file_name: impl AsRef) -> Result { + Self::pem_file_iter(file_name)? + .next() + .unwrap_or(Err(Error::NoItemsFound)) + } + + /// Iterate over all sections of this type from the PEM contents of the named file. + /// + /// This reports errors in two phases: + /// + /// - errors opening the file are reported from this function directly, + /// - errors reading from the file are reported from the returned iterator, + #[cfg(feature = "std")] + fn pem_file_iter( + file_name: impl AsRef, + ) -> Result, Self>, Error> { + Ok(ReadIter::<_, Self> { + rd: io::BufReader::new(File::open(file_name).map_err(Error::Io)?), + _ty: PhantomData, + }) + } + + /// Decode the first section of this type from PEM read from an [`io::Read`]. + #[cfg(feature = "std")] + fn from_pem_reader(rd: impl std::io::Read) -> Result { + Self::pem_reader_iter(rd) + .next() + .unwrap_or(Err(Error::NoItemsFound)) + } + + /// Iterate over all sections of this type from PEM present in an [`io::Read`]. + #[cfg(feature = "std")] + fn pem_reader_iter(rd: R) -> ReadIter, Self> { + ReadIter::<_, Self> { + rd: io::BufReader::new(rd), + _ty: PhantomData, + } + } + + /// Conversion from a PEM [`SectionKind`] and body data. + /// + /// This inspects `kind`, and if it matches this type's PEM section kind, + /// converts `der` into this type. + fn from_pem(kind: SectionKind, der: Vec) -> Option; +} + +pub(crate) trait PemObjectFilter: PemObject + From> { + const KIND: SectionKind; +} + +impl>> PemObject for T { + fn from_pem(kind: SectionKind, der: Vec) -> Option { + match Self::KIND == kind { + true => Some(Self::from(der)), + false => None, + } + } +} + +/// Extract and return all PEM sections by reading `rd`. +#[cfg(feature = "std")] +pub struct ReadIter { + rd: R, + _ty: PhantomData, +} + +#[cfg(feature = "std")] +impl ReadIter { + /// Create a new iterator. + pub fn new(rd: R) -> Self { + Self { + rd, + _ty: PhantomData, + } + } +} + +#[cfg(feature = "std")] +impl Iterator for ReadIter { + type Item = Result; + + fn next(&mut self) -> Option { + loop { + return match from_buf(&mut self.rd) { + Ok(Some((sec, item))) => match T::from_pem(sec, item) { + Some(res) => Some(Ok(res)), + None => continue, + }, + Ok(None) => return None, + Err(err) => Some(Err(err)), + }; + } + } +} + +/// Iterator over all PEM sections in a `&[u8]` slice. +pub struct SliceIter<'a, T> { + current: &'a [u8], + _ty: PhantomData, +} + +impl<'a, T: PemObject> SliceIter<'a, T> { + /// Create a new iterator. + pub fn new(current: &'a [u8]) -> Self { + Self { + current, + _ty: PhantomData, + } + } + + /// Returns the rest of the unparsed data. + /// + /// This is the slice immediately following the most + /// recently returned item from `next()`. + #[doc(hidden)] + pub fn remainder(&self) -> &'a [u8] { + self.current + } +} + +impl Iterator for SliceIter<'_, T> { + type Item = Result; + + fn next(&mut self) -> Option { + loop { + return match from_slice(self.current) { + Ok(Some(((sec, item), rest))) => { + self.current = rest; + match T::from_pem(sec, item) { + Some(res) => Some(Ok(res)), + None => continue, + } + } + Ok(None) => return None, + Err(err) => Some(Err(err)), + }; + } + } +} + +impl PemObject for (SectionKind, Vec) { + fn from_pem(kind: SectionKind, der: Vec) -> Option { + Some((kind, der)) + } +} + +/// Extract and decode the next supported PEM section from `input` +/// +/// - `Ok(None)` is returned if there is no PEM section to read from `input` +/// - Syntax errors and decoding errors produce a `Err(...)` +/// - Otherwise each decoded section is returned with a `Ok(Some((..., remainder)))` where +/// `remainder` is the part of the `input` that follows the returned section +#[allow(clippy::type_complexity)] +fn from_slice(mut input: &[u8]) -> Result), &[u8])>, Error> { + let mut b64buf = Vec::with_capacity(1024); + let mut section = None::<(Vec<_>, Vec<_>)>; + + loop { + let next_line = if let Some(index) = input + .iter() + .position(|byte| *byte == b'\n' || *byte == b'\r') + { + let (line, newline_plus_remainder) = input.split_at(index); + input = &newline_plus_remainder[1..]; + Some(line) + } else if !input.is_empty() { + let next_line = input; + input = &[]; + Some(next_line) + } else { + None + }; + + match read(next_line, &mut section, &mut b64buf)? { + ControlFlow::Continue(()) => continue, + ControlFlow::Break(item) => return Ok(item.map(|item| (item, input))), + } + } +} + +/// Extract and decode the next supported PEM section from `rd`. +/// +/// - Ok(None) is returned if there is no PEM section read from `rd`. +/// - Underlying IO errors produce a `Err(...)` +/// - Otherwise each decoded section is returned with a `Ok(Some(...))` +#[cfg(feature = "std")] +pub fn from_buf(rd: &mut dyn io::BufRead) -> Result)>, Error> { + let mut b64buf = Vec::with_capacity(1024); + let mut section = None::<(Vec<_>, Vec<_>)>; + let mut line = Vec::with_capacity(80); + + loop { + line.clear(); + let len = read_until_newline(rd, &mut line).map_err(Error::Io)?; + + let next_line = if len == 0 { + None + } else { + Some(line.as_slice()) + }; + + match read(next_line, &mut section, &mut b64buf) { + Ok(ControlFlow::Break(opt)) => return Ok(opt), + Ok(ControlFlow::Continue(())) => continue, + Err(e) => return Err(e), + } + } +} + +#[allow(clippy::type_complexity)] +fn read( + next_line: Option<&[u8]>, + section: &mut Option<(Vec, Vec)>, + b64buf: &mut Vec, +) -> Result)>, ()>, Error> { + let line = if let Some(line) = next_line { + line + } else { + // EOF + return match section.take() { + Some((_, end_marker)) => Err(Error::MissingSectionEnd { end_marker }), + None => Ok(ControlFlow::Break(None)), + }; + }; + + if line.starts_with(b"-----BEGIN ") { + let (mut trailer, mut pos) = (0, line.len()); + for (i, &b) in line.iter().enumerate().rev() { + match b { + b'-' => { + trailer += 1; + pos = i; + } + b'\n' | b'\r' | b' ' => continue, + _ => break, + } + } + + if trailer != 5 { + return Err(Error::IllegalSectionStart { + line: line.to_vec(), + }); + } + + let ty = &line[11..pos]; + let mut end = Vec::with_capacity(10 + 4 + ty.len()); + end.extend_from_slice(b"-----END "); + end.extend_from_slice(ty); + end.extend_from_slice(b"-----"); + *section = Some((ty.to_owned(), end)); + return Ok(ControlFlow::Continue(())); + } + + if let Some((section_label, end_marker)) = section.as_ref() { + if line.starts_with(end_marker) { + let kind = match SectionKind::try_from(§ion_label[..]) { + Ok(kind) => kind, + // unhandled section: have caller try again + Err(()) => { + *section = None; + b64buf.clear(); + return Ok(ControlFlow::Continue(())); + } + }; + + let mut der = vec![0u8; base64::decoded_length(b64buf.len())]; + let der_len = match kind.secret() { + true => base64::decode_secret(b64buf, &mut der), + false => base64::decode_public(b64buf, &mut der), + } + .map_err(|err| Error::Base64Decode(format!("{err:?}")))? + .len(); + + der.truncate(der_len); + + return Ok(ControlFlow::Break(Some((kind, der)))); + } + } + + if section.is_some() { + b64buf.extend(line); + } + + Ok(ControlFlow::Continue(())) +} + +/// A single recognised section in a PEM file. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum SectionKind { + /// A DER-encoded x509 certificate. + /// + /// Appears as "CERTIFICATE" in PEM files. + Certificate, + + /// A DER-encoded Subject Public Key Info; as specified in RFC 7468. + /// + /// Appears as "PUBLIC KEY" in PEM files. + PublicKey, + + /// A DER-encoded plaintext RSA private key; as specified in PKCS #1/RFC 3447 + /// + /// Appears as "RSA PRIVATE KEY" in PEM files. + RsaPrivateKey, + + /// A DER-encoded plaintext private key; as specified in PKCS #8/RFC 5958 + /// + /// Appears as "PRIVATE KEY" in PEM files. + PrivateKey, + + /// A Sec1-encoded plaintext private key; as specified in RFC 5915 + /// + /// Appears as "EC PRIVATE KEY" in PEM files. + EcPrivateKey, + + /// A Certificate Revocation List; as specified in RFC 5280 + /// + /// Appears as "X509 CRL" in PEM files. + Crl, + + /// A Certificate Signing Request; as specified in RFC 2986 + /// + /// Appears as "CERTIFICATE REQUEST" in PEM files. + Csr, + + /// An EchConfigList structure, as specified in + /// . + /// + /// Appears as "ECHCONFIG" in PEM files. + EchConfigList, +} + +impl SectionKind { + fn secret(&self) -> bool { + match self { + Self::RsaPrivateKey | Self::PrivateKey | Self::EcPrivateKey => true, + Self::Certificate | Self::PublicKey | Self::Crl | Self::Csr | Self::EchConfigList => { + false + } + } + } +} + +impl TryFrom<&[u8]> for SectionKind { + type Error = (); + + fn try_from(value: &[u8]) -> Result { + Ok(match value { + b"CERTIFICATE" => Self::Certificate, + b"PUBLIC KEY" => Self::PublicKey, + b"RSA PRIVATE KEY" => Self::RsaPrivateKey, + b"PRIVATE KEY" => Self::PrivateKey, + b"EC PRIVATE KEY" => Self::EcPrivateKey, + b"X509 CRL" => Self::Crl, + b"CERTIFICATE REQUEST" => Self::Csr, + b"ECHCONFIG" => Self::EchConfigList, + _ => return Err(()), + }) + } +} + +/// Errors that may arise when parsing the contents of a PEM file +#[non_exhaustive] +#[derive(Debug)] +pub enum Error { + /// a section is missing its "END marker" line + MissingSectionEnd { + /// the expected "END marker" line that was not found + end_marker: Vec, + }, + + /// syntax error found in the line that starts a new section + IllegalSectionStart { + /// line that contains the syntax error + line: Vec, + }, + + /// base64 decode error + Base64Decode(String), + + /// I/O errors, from APIs that accept `std::io` types. + #[cfg(feature = "std")] + Io(io::Error), + + /// No items found of desired type + NoItemsFound, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingSectionEnd { end_marker } => { + write!(f, "missing section end marker: {end_marker:?}") + } + Self::IllegalSectionStart { line } => { + write!(f, "illegal section start: {line:?}") + } + Self::Base64Decode(e) => write!(f, "base64 decode error: {e}"), + #[cfg(feature = "std")] + Self::Io(e) => write!(f, "I/O error: {e}"), + Self::NoItemsFound => write!(f, "no items found"), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Error {} + +// Ported from https://github.com/rust-lang/rust/blob/91cfcb021935853caa06698b759c293c09d1e96a/library/std/src/io/mod.rs#L1990 and +// modified to look for our accepted newlines. +#[cfg(feature = "std")] +fn read_until_newline(r: &mut R, buf: &mut Vec) -> io::Result { + let mut read = 0; + loop { + let (done, used) = { + let available = match r.fill_buf() { + Ok(n) => n, + Err(ref e) if e.kind() == ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + }; + match available + .iter() + .copied() + .position(|b| b == b'\n' || b == b'\r') + { + Some(i) => { + buf.extend_from_slice(&available[..=i]); + (true, i + 1) + } + None => { + buf.extend_from_slice(available); + (false, available.len()) + } + } + }; + r.consume(used); + read += used; + if done || used == 0 { + return Ok(read); + } + } +} diff --git a/src/server_name.rs b/src/server_name.rs new file mode 100644 index 0000000..64dec3c --- /dev/null +++ b/src/server_name.rs @@ -0,0 +1,1194 @@ +//! DNS name validation according to RFC1035, but with underscores allowed. + +#[cfg(all(feature = "alloc", feature = "std"))] +use alloc::borrow::Cow; +#[cfg(feature = "alloc")] +use alloc::string::{String, ToString}; +use core::hash::{Hash, Hasher}; +use core::{fmt, mem, str}; +#[cfg(feature = "std")] +use std::error::Error as StdError; + +/// Encodes ways a client can know the expected name of the server. +/// +/// This currently covers knowing the DNS name of the server, but +/// will be extended in the future to supporting privacy-preserving names +/// for the server ("ECH"). For this reason this enum is `non_exhaustive`. +/// +/// # Making one +/// +/// If you have a DNS name as a `&str`, this type implements `TryFrom<&str>`, +/// so you can do: +/// +/// ``` +/// # use rustls_pki_types::ServerName; +/// ServerName::try_from("example.com").expect("invalid DNS name"); +/// ``` +/// +/// If you have an owned `String`, you can use `TryFrom` directly: +/// +/// ``` +/// # use rustls_pki_types::ServerName; +/// let name = "example.com".to_string(); +/// #[cfg(feature = "alloc")] +/// ServerName::try_from(name).expect("invalid DNS name"); +/// ``` +/// +/// which will yield a `ServerName<'static>` if successful. +/// +/// or, alternatively... +/// +/// ``` +/// # use rustls_pki_types::ServerName; +/// let x: ServerName = "example.com".try_into().expect("invalid DNS name"); +/// ``` +#[non_exhaustive] +#[derive(Clone, Eq, Hash, PartialEq)] +pub enum ServerName<'a> { + /// The server is identified by a DNS name. The name + /// is sent in the TLS Server Name Indication (SNI) + /// extension. + DnsName(DnsName<'a>), + + /// The server is identified by an IP address. SNI is not + /// done. + IpAddress(IpAddr), +} + +impl ServerName<'_> { + /// Produce an owned `ServerName` from this (potentially borrowed) `ServerName`. + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> ServerName<'static> { + match self { + Self::DnsName(d) => ServerName::DnsName(d.to_owned()), + Self::IpAddress(i) => ServerName::IpAddress(*i), + } + } + + /// Return the string representation of this `ServerName`. + /// + /// In the case of a `ServerName::DnsName` instance, this function returns a borrowed `str`. + /// For a `ServerName::IpAddress` instance it returns an allocated `String`. + #[cfg(feature = "std")] + pub fn to_str(&self) -> Cow<'_, str> { + match self { + Self::DnsName(d) => d.as_ref().into(), + Self::IpAddress(i) => std::net::IpAddr::from(*i).to_string().into(), + } + } +} + +impl fmt::Debug for ServerName<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::DnsName(d) => f.debug_tuple("DnsName").field(&d.as_ref()).finish(), + Self::IpAddress(i) => f.debug_tuple("IpAddress").field(i).finish(), + } + } +} + +#[cfg(feature = "alloc")] +impl TryFrom for ServerName<'static> { + type Error = InvalidDnsNameError; + + fn try_from(value: String) -> Result { + match DnsName::try_from_string(value) { + Ok(dns) => Ok(Self::DnsName(dns)), + Err(value) => match IpAddr::try_from(value.as_str()) { + Ok(ip) => Ok(Self::IpAddress(ip)), + Err(_) => Err(InvalidDnsNameError), + }, + } + } +} + +impl<'a> TryFrom<&'a [u8]> for ServerName<'a> { + type Error = InvalidDnsNameError; + + fn try_from(value: &'a [u8]) -> Result { + match str::from_utf8(value) { + Ok(s) => Self::try_from(s), + Err(_) => Err(InvalidDnsNameError), + } + } +} + +/// Attempt to make a ServerName from a string by parsing as a DNS name or IP address. +impl<'a> TryFrom<&'a str> for ServerName<'a> { + type Error = InvalidDnsNameError; + fn try_from(s: &'a str) -> Result { + match DnsName::try_from(s) { + Ok(dns) => Ok(Self::DnsName(dns)), + Err(InvalidDnsNameError) => match IpAddr::try_from(s) { + Ok(ip) => Ok(Self::IpAddress(ip)), + Err(_) => Err(InvalidDnsNameError), + }, + } + } +} + +impl From for ServerName<'_> { + fn from(addr: IpAddr) -> Self { + Self::IpAddress(addr) + } +} + +#[cfg(feature = "std")] +impl From for ServerName<'_> { + fn from(addr: std::net::IpAddr) -> Self { + Self::IpAddress(addr.into()) + } +} + +impl From for ServerName<'_> { + fn from(v4: Ipv4Addr) -> Self { + Self::IpAddress(IpAddr::V4(v4)) + } +} + +impl From for ServerName<'_> { + fn from(v6: Ipv6Addr) -> Self { + Self::IpAddress(IpAddr::V6(v6)) + } +} + +#[cfg(feature = "std")] +impl From for ServerName<'_> { + fn from(v4: std::net::Ipv4Addr) -> Self { + Self::IpAddress(IpAddr::V4(v4.into())) + } +} + +#[cfg(feature = "std")] +impl From for ServerName<'_> { + fn from(v6: std::net::Ipv6Addr) -> Self { + Self::IpAddress(IpAddr::V6(v6.into())) + } +} + +/// A type which encapsulates a string (borrowed or owned) that is a syntactically valid DNS name. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct DnsName<'a>(DnsNameInner<'a>); + +impl<'a> DnsName<'a> { + /// Produce a borrowed `DnsName` from this owned `DnsName`. + pub fn borrow(&'a self) -> Self { + Self(match self { + Self(DnsNameInner::Borrowed(s)) => DnsNameInner::Borrowed(s), + #[cfg(feature = "alloc")] + Self(DnsNameInner::Owned(s)) => DnsNameInner::Borrowed(s.as_str()), + }) + } + + /// Copy this object to produce an owned `DnsName`, smashing the case to lowercase + /// in one operation. + #[cfg(feature = "alloc")] + pub fn to_lowercase_owned(&self) -> DnsName<'static> { + DnsName(DnsNameInner::Owned(self.as_ref().to_ascii_lowercase())) + } + + /// Produce an owned `DnsName` from this (potentially borrowed) `DnsName`. + #[cfg(feature = "alloc")] + pub fn to_owned(&self) -> DnsName<'static> { + DnsName(DnsNameInner::Owned(match self { + Self(DnsNameInner::Borrowed(s)) => s.to_string(), + #[cfg(feature = "alloc")] + Self(DnsNameInner::Owned(s)) => s.clone(), + })) + } + + #[cfg(feature = "alloc")] + fn try_from_string(s: String) -> Result { + match validate(s.as_bytes()) { + Ok(_) => Ok(Self(DnsNameInner::Owned(s))), + Err(_) => Err(s), + } + } + + /// Produces a borrowed [`DnsName`] from a borrowed [`str`]. + pub const fn try_from_str(s: &str) -> Result, InvalidDnsNameError> { + match validate(s.as_bytes()) { + Ok(_) => Ok(DnsName(DnsNameInner::Borrowed(s))), + Err(err) => Err(err), + } + } +} + +#[cfg(feature = "alloc")] +impl TryFrom for DnsName<'static> { + type Error = InvalidDnsNameError; + + fn try_from(value: String) -> Result { + Self::try_from_string(value).map_err(|_| InvalidDnsNameError) + } +} + +impl<'a> TryFrom<&'a str> for DnsName<'a> { + type Error = InvalidDnsNameError; + + fn try_from(value: &'a str) -> Result { + DnsName::try_from_str(value) + } +} + +impl<'a> TryFrom<&'a [u8]> for DnsName<'a> { + type Error = InvalidDnsNameError; + + fn try_from(value: &'a [u8]) -> Result { + validate(value)?; + Ok(Self(DnsNameInner::Borrowed(str::from_utf8(value).unwrap()))) + } +} + +impl AsRef for DnsName<'_> { + fn as_ref(&self) -> &str { + match self { + Self(DnsNameInner::Borrowed(s)) => s, + #[cfg(feature = "alloc")] + Self(DnsNameInner::Owned(s)) => s.as_str(), + } + } +} + +#[derive(Clone, Eq)] +enum DnsNameInner<'a> { + Borrowed(&'a str), + #[cfg(feature = "alloc")] + Owned(String), +} + +impl PartialEq for DnsNameInner<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Borrowed(s), Self::Borrowed(o)) => s.eq_ignore_ascii_case(o), + #[cfg(feature = "alloc")] + (Self::Borrowed(s), Self::Owned(o)) => s.eq_ignore_ascii_case(o.as_str()), + #[cfg(feature = "alloc")] + (Self::Owned(s), Self::Borrowed(o)) => s.eq_ignore_ascii_case(o), + #[cfg(feature = "alloc")] + (Self::Owned(s), Self::Owned(o)) => s.eq_ignore_ascii_case(o.as_str()), + } + } +} + +impl Hash for DnsNameInner<'_> { + fn hash(&self, state: &mut H) { + let s = match self { + Self::Borrowed(s) => s, + #[cfg(feature = "alloc")] + Self::Owned(s) => s.as_str(), + }; + + s.chars().for_each(|c| c.to_ascii_lowercase().hash(state)); + } +} + +impl fmt::Debug for DnsNameInner<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Borrowed(s) => f.write_fmt(format_args!("{:?}", s)), + #[cfg(feature = "alloc")] + Self::Owned(s) => f.write_fmt(format_args!("{:?}", s)), + } + } +} + +/// The provided input could not be parsed because +/// it is not a syntactically-valid DNS Name. +#[derive(Debug)] +pub struct InvalidDnsNameError; + +impl fmt::Display for InvalidDnsNameError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("invalid dns name") + } +} + +#[cfg(feature = "std")] +impl StdError for InvalidDnsNameError {} + +const fn validate(input: &[u8]) -> Result<(), InvalidDnsNameError> { + enum State { + Start, + Next, + NumericOnly { len: usize }, + NextAfterNumericOnly, + Subsequent { len: usize }, + Hyphen { len: usize }, + } + + use State::*; + let mut state = Start; + + /// "Labels must be 63 characters or less." + const MAX_LABEL_LENGTH: usize = 63; + + /// https://devblogs.microsoft.com/oldnewthing/20120412-00/?p=7873 + const MAX_NAME_LENGTH: usize = 253; + + if input.len() > MAX_NAME_LENGTH { + return Err(InvalidDnsNameError); + } + + let mut idx = 0; + while idx < input.len() { + let ch = input[idx]; + state = match (state, ch) { + (Start | Next | NextAfterNumericOnly | Hyphen { .. }, b'.') => { + return Err(InvalidDnsNameError) + } + (Subsequent { .. }, b'.') => Next, + (NumericOnly { .. }, b'.') => NextAfterNumericOnly, + (Subsequent { len } | NumericOnly { len } | Hyphen { len }, _) + if len >= MAX_LABEL_LENGTH => + { + return Err(InvalidDnsNameError) + } + (Start | Next | NextAfterNumericOnly, b'0'..=b'9') => NumericOnly { len: 1 }, + (NumericOnly { len }, b'0'..=b'9') => NumericOnly { len: len + 1 }, + (Start | Next | NextAfterNumericOnly, b'a'..=b'z' | b'A'..=b'Z' | b'_') => { + Subsequent { len: 1 } + } + (Subsequent { len } | NumericOnly { len } | Hyphen { len }, b'-') => { + Hyphen { len: len + 1 } + } + ( + Subsequent { len } | NumericOnly { len } | Hyphen { len }, + b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'0'..=b'9', + ) => Subsequent { len: len + 1 }, + _ => return Err(InvalidDnsNameError), + }; + idx += 1; + } + + if matches!( + state, + Start | Hyphen { .. } | NumericOnly { .. } | NextAfterNumericOnly + ) { + return Err(InvalidDnsNameError); + } + + Ok(()) +} + +/// `no_std` implementation of `std::net::IpAddr`. +/// +/// Note: because we intend to replace this type with `core::net::IpAddr` as soon as it is +/// stabilized, the identity of this type should not be considered semver-stable. However, the +/// attached interfaces are stable; they form a subset of those provided by `core::net::IpAddr`. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum IpAddr { + /// An Ipv4 address. + V4(Ipv4Addr), + /// An Ipv6 address. + V6(Ipv6Addr), +} + +impl TryFrom<&str> for IpAddr { + type Error = AddrParseError; + + fn try_from(value: &str) -> Result { + match Ipv4Addr::try_from(value) { + Ok(v4) => Ok(Self::V4(v4)), + Err(_) => match Ipv6Addr::try_from(value) { + Ok(v6) => Ok(Self::V6(v6)), + Err(e) => Err(e), + }, + } + } +} + +#[cfg(feature = "std")] +impl From for IpAddr { + fn from(addr: std::net::IpAddr) -> Self { + match addr { + std::net::IpAddr::V4(v4) => Self::V4(v4.into()), + std::net::IpAddr::V6(v6) => Self::V6(v6.into()), + } + } +} + +#[cfg(feature = "std")] +impl From for std::net::IpAddr { + fn from(value: IpAddr) -> Self { + match value { + IpAddr::V4(v4) => Self::from(std::net::Ipv4Addr::from(v4)), + IpAddr::V6(v6) => Self::from(std::net::Ipv6Addr::from(v6)), + } + } +} + +#[cfg(feature = "std")] +impl From for IpAddr { + fn from(v4: std::net::Ipv4Addr) -> Self { + Self::V4(v4.into()) + } +} + +#[cfg(feature = "std")] +impl From for IpAddr { + fn from(v6: std::net::Ipv6Addr) -> Self { + Self::V6(v6.into()) + } +} + +/// `no_std` implementation of `std::net::Ipv4Addr`. +/// +/// Note: because we intend to replace this type with `core::net::Ipv4Addr` as soon as it is +/// stabilized, the identity of this type should not be considered semver-stable. However, the +/// attached interfaces are stable; they form a subset of those provided by `core::net::Ipv4Addr`. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Ipv4Addr([u8; 4]); + +impl TryFrom<&str> for Ipv4Addr { + type Error = AddrParseError; + + fn try_from(value: &str) -> Result { + // don't try to parse if too long + if value.len() > 15 { + Err(AddrParseError(AddrKind::Ipv4)) + } else { + Parser::new(value.as_bytes()).parse_with(|p| p.read_ipv4_addr(), AddrKind::Ipv4) + } + } +} + +#[cfg(feature = "std")] +impl From for Ipv4Addr { + fn from(addr: std::net::Ipv4Addr) -> Self { + Self(addr.octets()) + } +} + +#[cfg(feature = "std")] +impl From for std::net::Ipv4Addr { + fn from(value: Ipv4Addr) -> Self { + Self::from(value.0) + } +} + +impl AsRef<[u8; 4]> for Ipv4Addr { + fn as_ref(&self) -> &[u8; 4] { + &self.0 + } +} + +/// `no_std` implementation of `std::net::Ipv6Addr`. +/// +/// Note: because we intend to replace this type with `core::net::Ipv6Addr` as soon as it is +/// stabilized, the identity of this type should not be considered semver-stable. However, the +/// attached interfaces are stable; they form a subset of those provided by `core::net::Ipv6Addr`. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Ipv6Addr([u8; 16]); + +impl TryFrom<&str> for Ipv6Addr { + type Error = AddrParseError; + + fn try_from(value: &str) -> Result { + Parser::new(value.as_bytes()).parse_with(|p| p.read_ipv6_addr(), AddrKind::Ipv6) + } +} + +impl From<[u16; 8]> for Ipv6Addr { + fn from(value: [u16; 8]) -> Self { + // Adapted from `std::net::Ipv6Addr::new()` + let addr16 = [ + value[0].to_be(), + value[1].to_be(), + value[2].to_be(), + value[3].to_be(), + value[4].to_be(), + value[5].to_be(), + value[6].to_be(), + value[7].to_be(), + ]; + Self( + // All elements in `addr16` are big endian. + // SAFETY: `[u16; 8]` is always safe to transmute to `[u8; 16]`. + unsafe { mem::transmute::<[u16; 8], [u8; 16]>(addr16) }, + ) + } +} + +#[cfg(feature = "std")] +impl From for Ipv6Addr { + fn from(addr: std::net::Ipv6Addr) -> Self { + Self(addr.octets()) + } +} + +#[cfg(feature = "std")] +impl From for std::net::Ipv6Addr { + fn from(value: Ipv6Addr) -> Self { + Self::from(value.0) + } +} + +impl AsRef<[u8; 16]> for Ipv6Addr { + fn as_ref(&self) -> &[u8; 16] { + &self.0 + } +} + +// Adapted from core, 2023-11-23 +// +// https://github.com/rust-lang/rust/blob/fc13ca6d70f7381513c22443fc5aaee1d151ea45/library/core/src/net/parser.rs#L34 +mod parser { + use super::{AddrParseError, Ipv4Addr, Ipv6Addr}; + + pub(super) struct Parser<'a> { + // Parsing as ASCII, so can use byte array. + state: &'a [u8], + } + + impl<'a> Parser<'a> { + pub(super) fn new(input: &'a [u8]) -> Self { + Parser { state: input } + } + + /// Run a parser, and restore the pre-parse state if it fails. + fn read_atomically(&mut self, inner: F) -> Option + where + F: FnOnce(&mut Parser<'_>) -> Option, + { + let state = self.state; + let result = inner(self); + if result.is_none() { + self.state = state; + } + result + } + + /// Run a parser, but fail if the entire input wasn't consumed. + /// Doesn't run atomically. + pub(super) fn parse_with( + &mut self, + inner: F, + kind: AddrKind, + ) -> Result + where + F: FnOnce(&mut Parser<'_>) -> Option, + { + let result = inner(self); + if self.state.is_empty() { result } else { None }.ok_or(AddrParseError(kind)) + } + + /// Peek the next character from the input + fn peek_char(&self) -> Option { + self.state.first().map(|&b| char::from(b)) + } + + /// Read the next character from the input + fn read_char(&mut self) -> Option { + self.state.split_first().map(|(&b, tail)| { + self.state = tail; + char::from(b) + }) + } + + #[must_use] + /// Read the next character from the input if it matches the target. + fn read_given_char(&mut self, target: char) -> Option<()> { + self.read_atomically(|p| { + p.read_char() + .and_then(|c| if c == target { Some(()) } else { None }) + }) + } + + /// Helper for reading separators in an indexed loop. Reads the separator + /// character iff index > 0, then runs the parser. When used in a loop, + /// the separator character will only be read on index > 0 (see + /// read_ipv4_addr for an example) + fn read_separator(&mut self, sep: char, index: usize, inner: F) -> Option + where + F: FnOnce(&mut Parser<'_>) -> Option, + { + self.read_atomically(move |p| { + if index > 0 { + p.read_given_char(sep)?; + } + inner(p) + }) + } + + // Read a number off the front of the input in the given radix, stopping + // at the first non-digit character or eof. Fails if the number has more + // digits than max_digits or if there is no number. + fn read_number( + &mut self, + radix: u32, + max_digits: Option, + allow_zero_prefix: bool, + ) -> Option { + self.read_atomically(move |p| { + let mut result = T::ZERO; + let mut digit_count = 0; + let has_leading_zero = p.peek_char() == Some('0'); + + while let Some(digit) = p.read_atomically(|p| p.read_char()?.to_digit(radix)) { + result = result.checked_mul(radix)?; + result = result.checked_add(digit)?; + digit_count += 1; + if let Some(max_digits) = max_digits { + if digit_count > max_digits { + return None; + } + } + } + + if digit_count == 0 || (!allow_zero_prefix && has_leading_zero && digit_count > 1) { + None + } else { + Some(result) + } + }) + } + + /// Read an IPv4 address. + pub(super) fn read_ipv4_addr(&mut self) -> Option { + self.read_atomically(|p| { + let mut groups = [0; 4]; + + for (i, slot) in groups.iter_mut().enumerate() { + *slot = p.read_separator('.', i, |p| { + // Disallow octal number in IP string. + // https://tools.ietf.org/html/rfc6943#section-3.1.1 + p.read_number(10, Some(3), false) + })?; + } + + Some(Ipv4Addr(groups)) + }) + } + + /// Read an IPv6 Address. + pub(super) fn read_ipv6_addr(&mut self) -> Option { + /// Read a chunk of an IPv6 address into `groups`. Returns the number + /// of groups read, along with a bool indicating if an embedded + /// trailing IPv4 address was read. Specifically, read a series of + /// colon-separated IPv6 groups (0x0000 - 0xFFFF), with an optional + /// trailing embedded IPv4 address. + fn read_groups(p: &mut Parser<'_>, groups: &mut [u16]) -> (usize, bool) { + let limit = groups.len(); + + for (i, slot) in groups.iter_mut().enumerate() { + // Try to read a trailing embedded IPv4 address. There must be + // at least two groups left. + if i < limit - 1 { + let ipv4 = p.read_separator(':', i, |p| p.read_ipv4_addr()); + + if let Some(v4_addr) = ipv4 { + let [one, two, three, four] = v4_addr.0; + groups[i] = u16::from_be_bytes([one, two]); + groups[i + 1] = u16::from_be_bytes([three, four]); + return (i + 2, true); + } + } + + let group = p.read_separator(':', i, |p| p.read_number(16, Some(4), true)); + + match group { + Some(g) => *slot = g, + None => return (i, false), + } + } + (groups.len(), false) + } + + self.read_atomically(|p| { + // Read the front part of the address; either the whole thing, or up + // to the first :: + let mut head = [0; 8]; + let (head_size, head_ipv4) = read_groups(p, &mut head); + + if head_size == 8 { + return Some(head.into()); + } + + // IPv4 part is not allowed before `::` + if head_ipv4 { + return None; + } + + // Read `::` if previous code parsed less than 8 groups. + // `::` indicates one or more groups of 16 bits of zeros. + p.read_given_char(':')?; + p.read_given_char(':')?; + + // Read the back part of the address. The :: must contain at least one + // set of zeroes, so our max length is 7. + let mut tail = [0; 7]; + let limit = 8 - (head_size + 1); + let (tail_size, _) = read_groups(p, &mut tail[..limit]); + + // Concat the head and tail of the IP address + head[(8 - tail_size)..8].copy_from_slice(&tail[..tail_size]); + + Some(head.into()) + }) + } + } + + trait ReadNumberHelper: Sized { + const ZERO: Self; + fn checked_mul(&self, other: u32) -> Option; + fn checked_add(&self, other: u32) -> Option; + } + + macro_rules! impl_helper { + ($($t:ty)*) => ($(impl ReadNumberHelper for $t { + const ZERO: Self = 0; + #[inline] + fn checked_mul(&self, other: u32) -> Option { + Self::checked_mul(*self, other.try_into().ok()?) + } + #[inline] + fn checked_add(&self, other: u32) -> Option { + Self::checked_add(*self, other.try_into().ok()?) + } + })*) + } + + impl_helper! { u8 u16 u32 } + + #[derive(Debug, Clone, Copy, Eq, PartialEq)] + pub(super) enum AddrKind { + Ipv4, + Ipv6, + } +} + +use parser::{AddrKind, Parser}; + +/// Failure to parse an IP address +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct AddrParseError(AddrKind); + +impl core::fmt::Display for AddrParseError { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.write_str(match self.0 { + AddrKind::Ipv4 => "invalid IPv4 address syntax", + AddrKind::Ipv6 => "invalid IPv6 address syntax", + }) + } +} + +#[cfg(feature = "std")] +impl ::std::error::Error for AddrParseError {} + +#[cfg(test)] +mod tests { + use super::*; + #[cfg(feature = "alloc")] + use alloc::format; + + #[cfg(feature = "alloc")] + static TESTS: &[(&str, bool)] = &[ + ("", false), + ("localhost", true), + ("LOCALHOST", true), + (".localhost", false), + ("..localhost", false), + ("1.2.3.4", false), + ("127.0.0.1", false), + ("absolute.", true), + ("absolute..", false), + ("multiple.labels.absolute.", true), + ("foo.bar.com", true), + ("infix-hyphen-allowed.com", true), + ("-prefixhypheninvalid.com", false), + ("suffixhypheninvalid--", false), + ("suffixhypheninvalid-.com", false), + ("foo.lastlabelendswithhyphen-", false), + ("infix_underscore_allowed.com", true), + ("_prefixunderscorevalid.com", true), + ("labelendswithnumber1.bar.com", true), + ("xn--bcher-kva.example", true), + ( + "sixtythreesixtythreesixtythreesixtythreesixtythreesixtythreesix.com", + true, + ), + ( + "sixtyfoursixtyfoursixtyfoursixtyfoursixtyfoursixtyfoursixtyfours.com", + false, + ), + ( + "012345678901234567890123456789012345678901234567890123456789012.com", + true, + ), + ( + "0123456789012345678901234567890123456789012345678901234567890123.com", + false, + ), + ( + "01234567890123456789012345678901234567890123456789012345678901-.com", + false, + ), + ( + "012345678901234567890123456789012345678901234567890123456789012-.com", + false, + ), + ("numeric-only-final-label.1", false), + ("numeric-only-final-label.absolute.1.", false), + ("1starts-with-number.com", true), + ("1Starts-with-number.com", true), + ("1.2.3.4.com", true), + ("123.numeric-only-first-label", true), + ("a123b.com", true), + ("numeric-only-middle-label.4.com", true), + ("1000-sans.badssl.com", true), + ("twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfiftythreecharacters.twohundredandfi", true), + ("twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourcharacters.twohundredandfiftyfourc", false), + ]; + + #[cfg(feature = "alloc")] + #[test] + fn test_validation() { + for (input, expected) in TESTS { + #[cfg(feature = "std")] + println!("test: {:?} expected valid? {:?}", input, expected); + let name_ref = DnsName::try_from(*input); + assert_eq!(*expected, name_ref.is_ok()); + let name = DnsName::try_from(input.to_string()); + assert_eq!(*expected, name.is_ok()); + } + } + + #[cfg(feature = "alloc")] + #[test] + fn error_is_debug() { + assert_eq!(format!("{:?}", InvalidDnsNameError), "InvalidDnsNameError"); + } + + #[cfg(feature = "alloc")] + #[test] + fn error_is_display() { + assert_eq!(format!("{}", InvalidDnsNameError), "invalid dns name"); + } + + #[cfg(feature = "alloc")] + #[test] + fn dns_name_is_debug() { + let example = DnsName::try_from("example.com".to_string()).unwrap(); + assert_eq!(format!("{:?}", example), "DnsName(\"example.com\")"); + } + + #[cfg(feature = "alloc")] + #[test] + fn dns_name_traits() { + let example = DnsName::try_from("example.com".to_string()).unwrap(); + assert_eq!(example, example); // PartialEq + + #[cfg(feature = "std")] + { + use std::collections::HashSet; + let mut h = HashSet::::new(); + h.insert(example); + } + } + + #[cfg(feature = "alloc")] + #[test] + fn try_from_ascii_rejects_bad_utf8() { + assert_eq!( + format!("{:?}", DnsName::try_from(&b"\x80"[..])), + "Err(InvalidDnsNameError)" + ); + } + + const fn ipv4_address( + ip_address: &str, + octets: [u8; 4], + ) -> (&str, Result) { + (ip_address, Ok(Ipv4Addr(octets))) + } + + const IPV4_ADDRESSES: &[(&str, Result)] = &[ + // Valid IPv4 addresses + ipv4_address("0.0.0.0", [0, 0, 0, 0]), + ipv4_address("1.1.1.1", [1, 1, 1, 1]), + ipv4_address("205.0.0.0", [205, 0, 0, 0]), + ipv4_address("0.205.0.0", [0, 205, 0, 0]), + ipv4_address("0.0.205.0", [0, 0, 205, 0]), + ipv4_address("0.0.0.205", [0, 0, 0, 205]), + ipv4_address("0.0.0.20", [0, 0, 0, 20]), + // Invalid IPv4 addresses + ("", Err(AddrParseError(AddrKind::Ipv4))), + ("...", Err(AddrParseError(AddrKind::Ipv4))), + (".0.0.0.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.0.0.", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.0.", Err(AddrParseError(AddrKind::Ipv4))), + ("256.0.0.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.256.0.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.256.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.0.256", Err(AddrParseError(AddrKind::Ipv4))), + ("1..1.1.1", Err(AddrParseError(AddrKind::Ipv4))), + ("1.1..1.1", Err(AddrParseError(AddrKind::Ipv4))), + ("1.1.1..1", Err(AddrParseError(AddrKind::Ipv4))), + ("025.0.0.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.025.0.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.025.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.0.025", Err(AddrParseError(AddrKind::Ipv4))), + ("1234.0.0.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.1234.0.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.1234.0", Err(AddrParseError(AddrKind::Ipv4))), + ("0.0.0.1234", Err(AddrParseError(AddrKind::Ipv4))), + ]; + + #[test] + fn parse_ipv4_address_test() { + for &(ip_address, expected_result) in IPV4_ADDRESSES { + assert_eq!(Ipv4Addr::try_from(ip_address), expected_result); + } + } + + const fn ipv6_address( + ip_address: &str, + octets: [u8; 16], + ) -> (&str, Result) { + (ip_address, Ok(Ipv6Addr(octets))) + } + + const IPV6_ADDRESSES: &[(&str, Result)] = &[ + // Valid IPv6 addresses + ipv6_address( + "2a05:d018:076c:b685:e8ab:afd3:af51:3aed", + [ + 0x2a, 0x05, 0xd0, 0x18, 0x07, 0x6c, 0xb6, 0x85, 0xe8, 0xab, 0xaf, 0xd3, 0xaf, 0x51, + 0x3a, 0xed, + ], + ), + ipv6_address( + "2A05:D018:076C:B685:E8AB:AFD3:AF51:3AED", + [ + 0x2a, 0x05, 0xd0, 0x18, 0x07, 0x6c, 0xb6, 0x85, 0xe8, 0xab, 0xaf, 0xd3, 0xaf, 0x51, + 0x3a, 0xed, + ], + ), + ipv6_address( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], + ), + ipv6_address( + "FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF", + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], + ), + ipv6_address( + "FFFF:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + [ + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, + ], + ), + // Wrong hexadecimal characters on different positions + ( + "ffgf:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:gfff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:fffg:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffgf:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff:gfff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff:ffff:fgff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff:ffff:ffff:ffgf:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff:ffff:ffff:ffgf:fffg", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Wrong colons on uncompressed addresses + ( + ":ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff::ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff::ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff::ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff::ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff:ffff::ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff:ffff:ffff::ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff::ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // More colons than allowed + ( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ( + "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // v Invalid hexadecimal + ( + "ga05:d018:076c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Cannot start with colon + ( + ":a05:d018:076c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Cannot end with colon + ( + "2a05:d018:076c:b685:e8ab:afd3:af51:3ae:", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Cannot have more than seven colons + ( + "2a05:d018:076c:b685:e8ab:afd3:af51:3a::", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Cannot contain two colons in a row + ( + "2a05::018:076c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // v Textual block size is longer + ( + "2a056:d018:076c:b685:e8ab:afd3:af51:3ae", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // v Textual block size is shorter + ( + "2a0:d018:076c:b685:e8ab:afd3:af51:3aed ", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Shorter IPv6 address + ( + "d018:076c:b685:e8ab:afd3:af51:3aed", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Longer IPv6 address + ( + "2a05:d018:076c:b685:e8ab:afd3:af51:3aed3aed", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ]; + + #[test] + fn parse_ipv6_address_test() { + for &(ip_address, expected_result) in IPV6_ADDRESSES { + assert_eq!(Ipv6Addr::try_from(ip_address), expected_result); + } + } + + #[test] + fn try_from_ascii_ip_address_test() { + const IP_ADDRESSES: &[(&str, Result)] = &[ + // Valid IPv4 addresses + ("127.0.0.1", Ok(IpAddr::V4(Ipv4Addr([127, 0, 0, 1])))), + // Invalid IPv4 addresses + ( + // Ends with a dot; misses one octet + "127.0.0.", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Valid IPv6 addresses + ( + "0000:0000:0000:0000:0000:0000:0000:0001", + Ok(IpAddr::V6(Ipv6Addr([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ]))), + ), + // Something else + ( + // A hostname + "example.com", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ]; + for &(ip_address, expected_result) in IP_ADDRESSES { + assert_eq!(IpAddr::try_from(ip_address), expected_result) + } + } + + #[test] + fn try_from_ascii_str_ip_address_test() { + const IP_ADDRESSES: &[(&str, Result)] = &[ + // Valid IPv4 addresses + ("127.0.0.1", Ok(IpAddr::V4(Ipv4Addr([127, 0, 0, 1])))), + // Invalid IPv4 addresses + ( + // Ends with a dot; misses one octet + "127.0.0.", + Err(AddrParseError(AddrKind::Ipv6)), + ), + // Valid IPv6 addresses + ( + "0000:0000:0000:0000:0000:0000:0000:0001", + Ok(IpAddr::V6(Ipv6Addr([ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + ]))), + ), + // Something else + ( + // A hostname + "example.com", + Err(AddrParseError(AddrKind::Ipv6)), + ), + ]; + for &(ip_address, expected_result) in IP_ADDRESSES { + assert_eq!(IpAddr::try_from(ip_address), expected_result) + } + } + + #[test] + #[cfg(feature = "std")] + fn to_str() { + let domain_str = "example.com"; + let domain_servername = ServerName::try_from(domain_str).unwrap(); + assert_eq!(domain_str, domain_servername.to_str()); + + let ipv4_str = "127.0.0.1"; + let ipv4_servername = ServerName::try_from("127.0.0.1").unwrap(); + assert_eq!(ipv4_str, ipv4_servername.to_str()); + + let ipv6_str = "::1"; + let ipv6_servername = ServerName::try_from(ipv6_str).unwrap(); + assert_eq!("::1", ipv6_servername.to_str()); + } +} diff --git a/tests/data/certificate.chain.pem b/tests/data/certificate.chain.pem new file mode 100644 index 0000000..b7db426 --- /dev/null +++ b/tests/data/certificate.chain.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIBuDCCAWqgAwIBAgICAcgwBQYDK2VwMC4xLDAqBgNVBAMMI3Bvbnl0b3duIEVk +RFNBIGxldmVsIDIgaW50ZXJtZWRpYXRlMB4XDTE5MDgxNjEzMjg1MVoXDTI1MDIw +NTEzMjg1MVowGTEXMBUGA1UEAwwOdGVzdHNlcnZlci5jb20wKjAFBgMrZXADIQAQ +9M4hrE+Ucw4QUmaKOeKfphklBJi1qsqtX4u+knbseqOBwDCBvTAMBgNVHRMBAf8E +AjAAMAsGA1UdDwQEAwIGwDAdBgNVHQ4EFgQUa/gnV4+a22BUKTouAYX6nfLnPKYw +RAYDVR0jBD0wO4AUFxIwU406tG3CsPWkHWqfuUT48auhIKQeMBwxGjAYBgNVBAMM +EXBvbnl0b3duIEVkRFNBIENBggF7MDsGA1UdEQQ0MDKCDnRlc3RzZXJ2ZXIuY29t +ghVzZWNvbmQudGVzdHNlcnZlci5jb22CCWxvY2FsaG9zdDAFBgMrZXADQQApDiBQ +ns3fuvsWuFpIS+osj2B/gQ0b6eBAZ1UBxRyDlAo5++JZ0PtaEROyGo2t2gqi2Lyz +47mLyGCvqgVbC6cH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBVzCCAQmgAwIBAgIBezAFBgMrZXAwHDEaMBgGA1UEAwwRcG9ueXRvd24gRWRE +U0EgQ0EwHhcNMTkwODE2MTMyODUxWhcNMjkwODEzMTMyODUxWjAuMSwwKgYDVQQD +DCNwb255dG93biBFZERTQSBsZXZlbCAyIGludGVybWVkaWF0ZTAqMAUGAytlcAMh +AD4h3t0UCoMDGgIq4UW4P5zDngsY4vy1pE3wzLPFI4Vdo14wXDAdBgNVHQ4EFgQU +FxIwU406tG3CsPWkHWqfuUT48aswIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgH+MAUGAytlcANBAAZFvMek +Z71I8CXsBmx/0E6Weoaan9mJHgKqgQdK4w4h4dRg6DjNG957IbrLFO3vZduBMnna +qHP3xTFF+11Eyg8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBTDCB/6ADAgECAhRXcvbYynz4+usVvPtJp++sBUih3TAFBgMrZXAwHDEaMBgG +A1UEAwwRcG9ueXRvd24gRWREU0EgQ0EwHhcNMTkwODE2MTMyODUwWhcNMjkwODEz +MTMyODUwWjAcMRowGAYDVQQDDBFwb255dG93biBFZERTQSBDQTAqMAUGAytlcAMh +AIE4tLweIfcBGfhPqyXFp5pjVxjaiKk+9fTbRy46jAFKo1MwUTAdBgNVHQ4EFgQU +z5b9HjkOxffbtCZhWGg+bnxuD6wwHwYDVR0jBBgwFoAUz5b9HjkOxffbtCZhWGg+ +bnxuD6wwDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXADQQBNlt7z4bZ7KhzecxZEe3i5 +lH9MRqbpP9Rg4HyzAJfTzFGT183HoJiISdPLbxwMn0KaqSGlVe+9GgNKswoaRAwH +-----END CERTIFICATE----- diff --git a/tests/data/certificate.pem b/tests/data/certificate.pem new file mode 100644 index 0000000..74d4a01 --- /dev/null +++ b/tests/data/certificate.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEnzCCAoegAwIBAgIBezANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9wb255 +dG93biBSU0EgQ0EwHhcNMTkwNjA5MTcxNTEyWhcNMjkwNjA2MTcxNTEyWjAsMSow +KAYDVQQDDCFwb255dG93biBSU0EgbGV2ZWwgMiBpbnRlcm1lZGlhdGUwggGiMA0G +CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCj/tOFeSW3WB+TtuLCR1L/84lZytFw +zbpzOTGB1kPEKNbrMsv3lHXm5bHa8Bl3k113k7Hi7OAt/nkMm05s8LcUoovhaG5C +G7tjzL+ld1nO74gNS3IQHCzxRdRwIgaDZHyICfBQBfB9/m+9z3yRtOKWJl6i/MT9 +HRN6yADW/8gHFlMzRkCKBjIKXehKsu8cbtB+5MukwtXI4rKf9aYXZQOEUn1kEwQJ +ZIKBXR0eyloQiZervUE7meRCTBvzXT9VoSEX49/mempp4hnfdHlRNzre4/tphBf1 +fRUdpVXZ3DvmzoHdXRVzxx3X5LvDpf7Eb3ViGkXDFwkSfHEhkRnAl4lIzTH/1F25 +stmT8a0PA/lCNMrzJBzkLcuem1G1uMHoQZo1f3OpslJ8gHbE9ZlIbIKmpmJS9oop +Vh1BH+aOy5doCrF8uOLTQ3d5CqA/EZMGahDHy7IkeNYmG/RXUKNltv+r95gwuRP+ +9UIJ9FTa4REQbIpGWP5XibI6x4LqLTJj+VsCAwEAAaNeMFwwHQYDVR0OBBYEFEKP +y8hHZVazpvIsxFcGo4YrkEkwMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEF +BQcDAjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIB/jANBgkqhkiG9w0BAQsFAAOC +AgEAMzTRDLBExVFlw98AuX+pM+/R2Gjw5KFHvSYLKLbMRfuuZK1yNYYaYtNrtF+V +a53OFgaZj56o7tXc2PB8kw4MELD0ViR8Do2bvZieFcEe4DwhdjGCjuLehVLT29qI +7T3N/JkJ5daemKZcRB6Ne0F4+6QlVVNck28HUKbQThl88RdwLUImmSAfgKSt6uJ5 +wlH7wiYQR2vPXwSuEYzwot+L/91eBwuQr4Lovx9+TCKTbwQOKYjX4KfcOOQ1rx0M +IMrvwWqnabc6m1F0O6//ibL0kuFkJYEgOH2uJA12FBHO+/q2tcytejkOWKWMJj6Y +2etwIHcpzXaEP7fZ75cFGqcE3s7XGsweBIPLjMP1bKxEcFKzygURm/auUuXBCFBl +E16PB6JEAeCKe/8VFeyucvjPuQDWB49aq+r2SbpbI4IeZdz/QgEIOb0MpwStrvhH +9f/DtGMbjvuAEkRoOorK4m5k4GY3LsWTR2bey27AXk8N7pKarpu2N7ChBPm+EV0Y +H+tAI/OfdZuNUCES00F5UAFdU8zBUZo19ao2ZqfEADimE7Epk2s0bUe4GSqEXJp6 +68oVSMhZmMf/RCSNlr97f34sNiUA1YJ0JbCRZmw8KWNm9H1PARLbrgeRBZ/k31Li +WLDr3fiEVk7SGxj3zo94cS6AT55DyXLiSD/bFmL1QXgZweA= +-----END CERTIFICATE----- diff --git a/tests/data/crl.pem b/tests/data/crl.pem new file mode 100644 index 0000000..adda089 --- /dev/null +++ b/tests/data/crl.pem @@ -0,0 +1,16 @@ +-----BEGIN X509 CRL----- +MIICiTBzAgEBMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMMD3Bvbnl0b3duIFJT +QSBDQRcNMjMwNjI3MDgyODEyWhcNMjMwNzI3MDgyODEyWjAVMBMCAgHIFw0yMzA2 +MjcwODI3NTlaoA4wDDAKBgNVHRQEAwIBAjANBgkqhkiG9w0BAQsFAAOCAgEAP6EX +9+hxjx/AqdBpynZXjGkEqigBcLcJ2PADOXngdQI1jC0WuYnZymUimemeULtt8X+1 +ai2KxAuF1m4NEKZsrGKvO+/9s/X1xbGroyHSAMKtZafFopFpoB2aNbYlx7yIyLtD +BBIZIF50g20U+3izqpHutTD10itdk9TLsSceJHpwTkNJtaWMkOfBV28nKzEzVutV +f6WzRpURGzui6nQy7aIqImeanpoBoz323psMfC32U0uMBCZltyHNqsX58/2Uhucx +0IPnitNuhv4scCPf/jeRfGIWDrTf1/25LDzRxyg1S4z9aa+3GM4O3dqy4igZEhgT +q3pjlJ2hUL5E0oqbZDIQD1SN8UUUv5N2AjwZcxVBNnYeGyuO7YpTBYiu62o73iL2 +CjgElfaMq/9hEr9GR9kJozh7VTxtQPbnr4DiucQvhv8o/A1z+zkC0gj8iCLFtDbO +8bvDowcdle9LKkrLaBe6sO+fSH/I9Wj8vrEJKsuwaEraIdEaq2VrIMUPEWN0/MH9 +vTwHyadGSMK4CWtrn9fCAgSLw6NX74D7Cx1IaS8vstMjpeUqOS0dk5ThiW47HceB +DTko7rV5N+RGH2nW1ynLoZKCJQqqZcLilFMyKPui3jifJnQlMFi54jGVgg/D6UQn +7dA7wb2ux/1hSiaarp+mi7ncVOyByz6/WQP8mfc= +-----END X509 CRL----- diff --git a/tests/data/csr.pem b/tests/data/csr.pem new file mode 100644 index 0000000..b1ea6d4 --- /dev/null +++ b/tests/data/csr.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIIC+zCCAeMCAQAwfDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRQwEgYD +VQQHDAtTYW4gQW50b25pbzEdMBsGA1UECgwURXhhbXBsZSBPcmdhbml6YXRpb24x +EjAQBgNVBAsMCU1hcmtldGluZzEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwJw0BcbuqZyiABlmSYTi1tcr8DB0D +NcTtzsYe7tlyIKd3mEs+u6Pi3rEQGvOw5eo6CmWII2qmVOqJ2f6gjl2lZJ5DUE6B +I+NNE73zfFMrttUtI8X4ChnE4rrGqqUsSvYz1YVU0KiJ/00YMjEY5XlJYYa9FgfZ +sUrhj4aCFdXS6CU9jueRr+udEBElDcgTS9+pB+LFhVfUMTdxnJ3BcT4ZDDqODH3/ +5RAgq03dhRpkkaVIg2uVKTBDoM3hs8T1zIxLM7hItaZzMv4uHdfI8y+BdHrePT33 +BoTlocvTEZEqqXEdw2kUd4PDgyUTjFE3b9OeLk0Ju5GRvuCW3UcS5gFvAgMBAAGg +OjA4BgkqhkiG9w0BCQ4xKzApMCcGA1UdEQQgMB6CC2V4YW1wbGUuY29tgg9mb28u +ZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBACWsgxPw13QUpoCJOqvp8B1A +EfsxRJITSROmukV3ZQycPT76Y3GVrM9sGjO8p13J/CVw2KcWc9xmgHF0MdvPNhnW +OB6Y07hVpNnJVHb1KglOkNkTy6sVDtnZHg2klqGSyzIbwZ9R3JG8HtRdkceIrm3D +gdiZyLcf1VDCCUGaskEi2CsggCQQJNyGi+8BSQ8MPKm/m0KrSchGQ157eWCCjopz +f5GQe2UGOg5T7g8+S4GdECMwkMlTGUwlAM6LuOG/NZqP528PCAYQv0eOYdSwALQT +GwTyU4AZ9y1uBFuaFxABew9GbDEtNY/XHTF8308edUwGBk6jfD+UuTeEwRZGs9E= +-----END CERTIFICATE REQUEST----- diff --git a/tests/data/ech.pem b/tests/data/ech.pem new file mode 100644 index 0000000..94104b1 --- /dev/null +++ b/tests/data/ech.pem @@ -0,0 +1,7 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEICjd4yGRdsoP9gU7YT7My8DHx1Tjme8GYDXrOMCi8v1V +-----END PRIVATE KEY----- +-----BEGIN ECHCONFIG----- +AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEA +AQALZXhhbXBsZS5jb20AAA== +-----END ECHCONFIG----- diff --git a/tests/data/gunk.pem b/tests/data/gunk.pem new file mode 100644 index 0000000..fa11b6c Binary files /dev/null and b/tests/data/gunk.pem differ diff --git a/tests/data/mixed-line-endings.crt b/tests/data/mixed-line-endings.crt new file mode 100644 index 0000000..6a82253 --- /dev/null +++ b/tests/data/mixed-line-endings.crt @@ -0,0 +1,41 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM 9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L 93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy rqXRfboQnoZsG4q5WTP468SQvvG5 -----END CERTIFICATE----- -----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM 9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L 93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU 5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy rqXRfboQnoZsG4q5WTP468SQvvG5 -----END CERTIFICATE----- \ No newline at end of file diff --git a/tests/data/nistp256curve.pem b/tests/data/nistp256curve.pem new file mode 100644 index 0000000..a76e47d --- /dev/null +++ b/tests/data/nistp256curve.pem @@ -0,0 +1,3 @@ +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- diff --git a/tests/data/nistp256key.pem b/tests/data/nistp256key.pem new file mode 100644 index 0000000..943a769 --- /dev/null +++ b/tests/data/nistp256key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMaA7bFrjDDBSik057bIKo7UQXJZNwLK9AjYZQ7yIWFloAoGCCqGSM49 +AwEHoUQDQgAExu0Z/w8nQJZAXeOXOnZun9HiZscY9H/KwYcXpeZHu+f9P9mOUEkH +5Z0av+JKtzhFspjngNLVgWcjlA1L5AJLdA== +-----END EC PRIVATE KEY----- diff --git a/tests/data/nistp256key.pkcs8.pem b/tests/data/nistp256key.pkcs8.pem new file mode 100644 index 0000000..aae4967 --- /dev/null +++ b/tests/data/nistp256key.pkcs8.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxoDtsWuMMMFKKTTn +tsgqjtRBclk3Asr0CNhlDvIhYWWhRANCAATG7Rn/DydAlkBd45c6dm6f0eJmxxj0 +f8rBhxel5ke75/0/2Y5QSQflnRq/4kq3OEWymOeA0tWBZyOUDUvkAkt0 +-----END PRIVATE KEY----- diff --git a/tests/data/rsa-key-no-trailing-newline.pem b/tests/data/rsa-key-no-trailing-newline.pem new file mode 100644 index 0000000..54eafa3 --- /dev/null +++ b/tests/data/rsa-key-no-trailing-newline.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCrOnJjNc/rt+EA +5V5w6Jq+d6WzukJqrFrEVogUI3ByMagTGx5YYABtbqnJXZ5Ad5gUxZzihqCvNzEP +IPS4OGIfhvzKhRno1qIK4u/luoVdc/kItZ5gNE7OIW6hmgD6X1JRQjnc6GNoOINX +2wmL7zePhD6mCqNIwlqQjjDh+GHkPdlhn/XXnv/LgRYpb52lfVmihqcAej5mIr6g +5xdyV1ql53QwdOEx0IJayOVuCvVSt2l7y8vqolHQ4CwNc4XN+rTYXdia8akysFiH +dMeUQi46TqWarvK+0s3hcq5NNzesg6p5miNywpUVffAYLmd6Rr9vDOkFTwQzIX8U +3Pi05TNtAgMBAAECggEALEZWoZSeiMLpKUQl4Wgj6zAg5pI47kBW0AHX+e0X/E8e +uKIAfLWGJsXAnVLZwq2p42Udpe1Ny0CoLNNGtAPQS3qqDuvPwUcOa2Y2xd4u8fU0 +5a2goBumbOJ0KtEZg6P2MxC+yYJFggNq4uK+WzKl2TRGxsRlaEDpDgvWU/fXyB5Q +pkMVkHU8VWKtSG+rZ0nMZnGuRFKFdTmtLcTYJip4qY4EqqkmhNYdkQrBDOCGEU4o +cV/Nx48iNs78SP06ZLl3rp0xxI436ZTFvPw6E1B3j0Bsh2ufYU37RtuiUTbXABkw +X67Lt5evvomxWHovpD3zXg8dP8DEOBZMkc1HFJAIuQKBgQDxH3uWrAhCSEMvLB+2 +1KGlpEe2V/Sq5c6I2+PqgKNU9/RdJIX0QiNZ5VQQGcBzWRv6HDWnrDWO7AxuQSA8 +golZhH3bMXe8k9XWk6yn+XZsnvR6XQ702LqUH6e9cX9VRjR1gCWhSc9OcnfruwWT +penS3L94qxZi8lrQSKyS0R+7OwKBgQC1yvvMKF/FDoLmbch01ep7dbWUBfXp6oM/ +HSzm3Q7U8cyQdwNcwc1zR8x7TFnuvyPxqV1K1hVbUot77s5yQ9x89EQNN682zEjT +7Ox8ELYj7RxLc2cl23/Rv8Exvotv2HvbA+vr9lI/UlyUsRmZFnn+2cAcKge+o1M6 +BTwTzT7RdwKBgHS45c3pV1ImwwcZ1/xccCfMH77gUxtLhbBwqaMCRI7EPTG9lW7J +eW1x+0CUBrqP32AyubKCRab7E4Vn12ATXMPNxFMQXkMWWYS3FfR2aWJpjDWTyMK0 +C5XawQuO3rH7+zcKIq0yGr2B4hVmAmwX+9nMbI/QWlTptxZup8OmojKzAoGAL5sK +OTpRaf8U1FbnaYEjOFVoyWyOK0VYPUzcl1BINAdl9GbWIJI3xPqGV7t0yYqQVRZu +8cwCJ7oEAN5WfaG7uZUVxQhR/92bLLQccZjGub277R45YraKUFkQtIAbb5yXQpFS +VKZaf26IBAAknew+4jgPkNCI2qlWgBki1GSpEJ0CgYBpksYOxy58fVySeKtxz7Vy +ckfVCsCsJKIRK3GU4M67GPiDUXGPJCb8l5QUcjwD1hte6aT6/z+DhW552kfcRd/w +d9UuI3uBcZosFBI5UQM4oQQ5gOLOcSmHfesRE0X5hQdcgjN025lUxUTGJqsrD6wj +gh5gNFkL4M5Hoktydw93eQ== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/tests/data/rsa1024.pkcs1.pem b/tests/data/rsa1024.pkcs1.pem new file mode 100644 index 0000000..0074a0e --- /dev/null +++ b/tests/data/rsa1024.pkcs1.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC1Dt8tFmGS76ciuNXvk/QRrV8wCcArWxvl7Ku0aSQXgcFBAav6 +P5RD8b+dC9DihSu/r+6OOfjsAZ6oKCq3OTUfmoUhLpoBomxPczJgLyyLD+nQkp5q +B1Q3WB6ACL/HJRRjJEIn7lc5u1FVBGbiCAHKMiaP4BDSym8oqimKC6uiaQIDAQAB +AoGAGKmY7sxQqDIqwwkIYyT1Jv9FqwZ4/a7gYvZVATMdLnKHP3KZ2XGVoZepcRvt +7R0Us3ykcw0kgglKcj9eaizJtnSuoDPPwt53mDypPN2sU3hZgyk2tPgr49DB3MIp +fjoqw4RL/p60ksgGXbDEqBuXqOtH5i61khWlMj+BWL9VDq0CQQDaELWPQGjgs+7X +/QyWMJwOF4FXE4jecH/CcPVDB9K1ukllyC1HqTNe44Sp2bIDuSXXWb8yEixrEWBE +ci2CSSjXAkEA1I4W9IzwEmAeLtL6VBip9ks52O0JKu373/Xv1F2GYdhnQaFw7IC6 +1lSzcYMKGTmDuM8Cj26caldyv19Q0SPmvwJAdRHjZzS9GWWAJJTF3Rvbq/USix0B +renXrRvXkFTy2n1YSjxdkstTuO2Mm2M0HquXlTWpX8hB8HkzpYtmwztjoQJAECKl +LXVReCOhxu4vIJkqtc6qGoSL8J1WRH8X8KgU3nKeDAZkWx++jyyo3pIS/y01iZ71 +U8wSxaPTyyFCMk4mYwJBALjg7g8yDy1Lg9GFfOZvAVzPjqD28jZh/VJsDz9IhYoG +z89iHWHkllOisbOm+SeynVC8CoFXmJPc26U65GcjI18= +-----END RSA PRIVATE KEY----- diff --git a/tests/data/rsa1024.pkcs8.pem b/tests/data/rsa1024.pkcs8.pem new file mode 100644 index 0000000..16a17c7 --- /dev/null +++ b/tests/data/rsa1024.pkcs8.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALUO3y0WYZLvpyK4 +1e+T9BGtXzAJwCtbG+Xsq7RpJBeBwUEBq/o/lEPxv50L0OKFK7+v7o45+OwBnqgo +Krc5NR+ahSEumgGibE9zMmAvLIsP6dCSnmoHVDdYHoAIv8clFGMkQifuVzm7UVUE +ZuIIAcoyJo/gENLKbyiqKYoLq6JpAgMBAAECgYAYqZjuzFCoMirDCQhjJPUm/0Wr +Bnj9ruBi9lUBMx0ucoc/cpnZcZWhl6lxG+3tHRSzfKRzDSSCCUpyP15qLMm2dK6g +M8/C3neYPKk83axTeFmDKTa0+Cvj0MHcwil+OirDhEv+nrSSyAZdsMSoG5eo60fm +LrWSFaUyP4FYv1UOrQJBANoQtY9AaOCz7tf9DJYwnA4XgVcTiN5wf8Jw9UMH0rW6 +SWXILUepM17jhKnZsgO5JddZvzISLGsRYERyLYJJKNcCQQDUjhb0jPASYB4u0vpU +GKn2SznY7Qkq7fvf9e/UXYZh2GdBoXDsgLrWVLNxgwoZOYO4zwKPbpxqV3K/X1DR +I+a/AkB1EeNnNL0ZZYAklMXdG9ur9RKLHQGt6detG9eQVPLafVhKPF2Sy1O47Yyb +YzQeq5eVNalfyEHweTOli2bDO2OhAkAQIqUtdVF4I6HG7i8gmSq1zqoahIvwnVZE +fxfwqBTecp4MBmRbH76PLKjekhL/LTWJnvVTzBLFo9PLIUIyTiZjAkEAuODuDzIP +LUuD0YV85m8BXM+OoPbyNmH9UmwPP0iFigbPz2IdYeSWU6Kxs6b5J7KdULwKgVeY +k9zbpTrkZyMjXw== +-----END PRIVATE KEY----- diff --git a/tests/data/spki.pem b/tests/data/spki.pem new file mode 100644 index 0000000..314a0f2 --- /dev/null +++ b/tests/data/spki.pem @@ -0,0 +1,14 @@ +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqIh8FTj9DIgI8DAoCBh+ +6UXOfaWkvNaGZx2GwXl4WDAa/ZSE5/8ofg/6V59bmk9yry57UR4F+blscBvE4g3U +dTvWJOBRD900l21vwpDLKzZguyGOCmKwJu3vCnAQKzBRXW5sDgvO67GeU6kpaic9 +LYPYnYaoxCRTYTZu0wy72rW5G0Fe8Gg/duJmUH7vqGIZupTTVzIBMbFVPBMJqprT +MStDhaUL0JiAz0ZgTeNLRIBZWV9mY4PG3rZtbV0BZGR1ipAq9xfgqJcURCcKl/ZT +UMtzvgk8s5hYkIJX0ZL3qsfdM4BMgIFhHq/GisQKbbu9kWldBrxQylOwa6r0m3Jv +KJX2ViDSORndaCz2sppmVx5HDHnj+Bw381yawphnpumP3BJK4iof//uYKvfdc4RC +y2EXL8PYPsT5DMB0jaBt92ytR5sLhn8Sl9Hk0buN4IjrYPISrdhS45xQXUqxcp9O +9hcU+rSaQyZ45cj+VlWhKq8MDvGvaAONBFSEh01mnUwoJObsAZNVFVtuOkwAli0F +kGouMycQY1BGscpdC516Nya361Hk/ICyby2Y0BJrrVGaSM6poXH9yEjglzAdtSDb +Cvhn/zlAI5ltm4Nv2qTgYBDns5JRGVhBym6RbbZ1C/KfCgn0hOxiw3N7AN4d0K5n +LI6p7U9RnNVbWgbqsuoxBtkCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/tests/data/whitespace-prefix.crt b/tests/data/whitespace-prefix.crt new file mode 100644 index 0000000..227fa8a --- /dev/null +++ b/tests/data/whitespace-prefix.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- + MIIDaTCCAlGgAwIBAgIJAOq/zL+84IswMA0GCSqGSIb3DQEBCwUAMFoxCzAJBgNV + BAYTAlVTMQswCQYDVQQIDAJOQzEMMAoGA1UEBwwDUlRQMQ8wDQYDVQQKDAZOZXRB + cHAxDTALBgNVBAsMBEVTSVMxEDAOBgNVBAMMB1NTRk1DQ0EwHhcNMTcxMTAxMjEw + OTQyWhcNMjcxMDMwMjEwOTQyWjBaMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTkMx + DDAKBgNVBAcMA1JUUDEPMA0GA1UECgwGTmV0QXBwMQ0wCwYDVQQLDARFU0lTMRAw + DgYDVQQDDAdTU0ZNQ0NBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA + iaD9Ee0Yrdka0I+9GTJBIW/Fp5JU6kyjaxfOldW/R9lEubegXQFhDD2Xi1HZ+fTM + f224glB9xLJXAHhipRK01C2MgC4kSH75WL1iAiYeOBloExqmK6OCX+sdyO7RXm/H + Ra9tN2INWdvyO2pnmxsSnq56mCMsUZLtrRKp89FWgcxLg5r8QxH7xwfh5k54rxjE + 144TD9yrIiQOgRSIRHUrVJ9l/F/gnwzP8wcNABeXwN71Mzl7mliPA703kONQIAyU + 0E0tLpmy/U8dZdMmTBZGB7jI9f95Hl1RunfwhR371a6z38kgkvwrLzl4qflfsPjw + K9n4omNk9rCH9H9tWkxxjwIDAQABozIwMDAdBgNVHQ4EFgQU/bFyCCnqdDFKlQBJ + ExtV6wcMYkEwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAOQMs + Pz2iBD1+3RcSOsahB36WAwPCjgPiXXXpU+Zri11+m6I0Lq+OWtf+YgaQ8ylLmCQd + 0p1wHlYA4qo896SycrhTQfy9GlS/aQqN192k3oBGoJcMIUnGUBGuEvyZ2aDUfkzy + JUqBe+0KaT7pkvvbRL7VUz34I7ouq9fQIRZ26vUDLTY3KM1n/DXBj3e30GHGMV3K + NN2twuLXPNjnryfgpliHU1rwV7r1WvrCVn4StjimP2bO5HGqD/SbiYUL2M9LOuLK + 6mqY4OHumYXq3k7CHrvt0FepsN0L14LYEt1LvpPDFWP3SdN4z4KqT9AGqBaJnhhl + Qiq8GWnAChspdBLxCg== +-----END CERTIFICATE----- diff --git a/tests/data/zen.pem b/tests/data/zen.pem new file mode 100644 index 0000000..f9ba7a6 --- /dev/null +++ b/tests/data/zen.pem @@ -0,0 +1,169 @@ +one with everything +-----BEGIN CERTIFICATE----- +MIIBuDCCAWqgAwIBAgICAcgwBQYDK2VwMC4xLDAqBgNVBAMMI3Bvbnl0b3duIEVk +RFNBIGxldmVsIDIgaW50ZXJtZWRpYXRlMB4XDTE5MDgxNjEzMjg1MVoXDTI1MDIw +NTEzMjg1MVowGTEXMBUGA1UEAwwOdGVzdHNlcnZlci5jb20wKjAFBgMrZXADIQAQ +9M4hrE+Ucw4QUmaKOeKfphklBJi1qsqtX4u+knbseqOBwDCBvTAMBgNVHRMBAf8E +AjAAMAsGA1UdDwQEAwIGwDAdBgNVHQ4EFgQUa/gnV4+a22BUKTouAYX6nfLnPKYw +RAYDVR0jBD0wO4AUFxIwU406tG3CsPWkHWqfuUT48auhIKQeMBwxGjAYBgNVBAMM +EXBvbnl0b3duIEVkRFNBIENBggF7MDsGA1UdEQQ0MDKCDnRlc3RzZXJ2ZXIuY29t +ghVzZWNvbmQudGVzdHNlcnZlci5jb22CCWxvY2FsaG9zdDAFBgMrZXADQQApDiBQ +ns3fuvsWuFpIS+osj2B/gQ0b6eBAZ1UBxRyDlAo5++JZ0PtaEROyGo2t2gqi2Lyz +47mLyGCvqgVbC6cH +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBVzCCAQmgAwIBAgIBezAFBgMrZXAwHDEaMBgGA1UEAwwRcG9ueXRvd24gRWRE +U0EgQ0EwHhcNMTkwODE2MTMyODUxWhcNMjkwODEzMTMyODUxWjAuMSwwKgYDVQQD +DCNwb255dG93biBFZERTQSBsZXZlbCAyIGludGVybWVkaWF0ZTAqMAUGAytlcAMh +AD4h3t0UCoMDGgIq4UW4P5zDngsY4vy1pE3wzLPFI4Vdo14wXDAdBgNVHQ4EFgQU +FxIwU406tG3CsPWkHWqfuUT48aswIAYDVR0lAQH/BBYwFAYIKwYBBQUHAwEGCCsG +AQUFBwMCMAwGA1UdEwQFMAMBAf8wCwYDVR0PBAQDAgH+MAUGAytlcANBAAZFvMek +Z71I8CXsBmx/0E6Weoaan9mJHgKqgQdK4w4h4dRg6DjNG957IbrLFO3vZduBMnna +qHP3xTFF+11Eyg8= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIBTDCB/6ADAgECAhRXcvbYynz4+usVvPtJp++sBUih3TAFBgMrZXAwHDEaMBgG +A1UEAwwRcG9ueXRvd24gRWREU0EgQ0EwHhcNMTkwODE2MTMyODUwWhcNMjkwODEz +MTMyODUwWjAcMRowGAYDVQQDDBFwb255dG93biBFZERTQSBDQTAqMAUGAytlcAMh +AIE4tLweIfcBGfhPqyXFp5pjVxjaiKk+9fTbRy46jAFKo1MwUTAdBgNVHQ4EFgQU +z5b9HjkOxffbtCZhWGg+bnxuD6wwHwYDVR0jBBgwFoAUz5b9HjkOxffbtCZhWGg+ +bnxuD6wwDwYDVR0TAQH/BAUwAwEB/zAFBgMrZXADQQBNlt7z4bZ7KhzecxZEe3i5 +lH9MRqbpP9Rg4HyzAJfTzFGT183HoJiISdPLbxwMn0KaqSGlVe+9GgNKswoaRAwH +-----END CERTIFICATE----- +-----BEGIN ZEN MASTER JOKE----- +QSBaZW4gbWFzdGVyIHZpc2l0aW5nIE5ldyBZb3JrIENpdHkgZ29lcyB1cCB0byBhIGhvdCBkb2cg +dmVuZG9yIGFuZCBzYXlzLCAiTWFrZSBtZSBvbmUgd2l0aCBldmVyeXRoaW5nLiIKVGhlIGhvdCBk +b2cgdmVuZG9yIGZpeGVzIGEgaG90IGRvZyBhbmQgaGFuZHMgaXQgdG8gdGhlIFplbiBtYXN0ZXIs +IHdobyBwYXlzIHdpdGggYSAkMjAgYmlsbC4KVGhlIHZlbmRvciBwdXRzIHRoZSBiaWxsIGluIHRo +ZSBjYXNoIGJveCBhbmQgY2xvc2VzIGl0LiAiRXhjdXNlIG1lLCBidXQgd2hlcmUncyBteSBjaGFu +Z2U/IiBhc2tzIHRoZSBaZW4gbWFzdGVyLgpUaGUgdmVuZG9yIHJlc3BvbmRzLCAiQ2hhbmdlIG11 +c3QgY29tZSBmcm9tIHdpdGhpbi4iCg== +-----END ZEN MASTER JOKE----- +-----BEGIN CERTIFICATE----- +MIIEnzCCAoegAwIBAgIBezANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9wb255 +dG93biBSU0EgQ0EwHhcNMTkwNjA5MTcxNTEyWhcNMjkwNjA2MTcxNTEyWjAsMSow +KAYDVQQDDCFwb255dG93biBSU0EgbGV2ZWwgMiBpbnRlcm1lZGlhdGUwggGiMA0G +CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCj/tOFeSW3WB+TtuLCR1L/84lZytFw +zbpzOTGB1kPEKNbrMsv3lHXm5bHa8Bl3k113k7Hi7OAt/nkMm05s8LcUoovhaG5C +G7tjzL+ld1nO74gNS3IQHCzxRdRwIgaDZHyICfBQBfB9/m+9z3yRtOKWJl6i/MT9 +HRN6yADW/8gHFlMzRkCKBjIKXehKsu8cbtB+5MukwtXI4rKf9aYXZQOEUn1kEwQJ +ZIKBXR0eyloQiZervUE7meRCTBvzXT9VoSEX49/mempp4hnfdHlRNzre4/tphBf1 +fRUdpVXZ3DvmzoHdXRVzxx3X5LvDpf7Eb3ViGkXDFwkSfHEhkRnAl4lIzTH/1F25 +stmT8a0PA/lCNMrzJBzkLcuem1G1uMHoQZo1f3OpslJ8gHbE9ZlIbIKmpmJS9oop +Vh1BH+aOy5doCrF8uOLTQ3d5CqA/EZMGahDHy7IkeNYmG/RXUKNltv+r95gwuRP+ +9UIJ9FTa4REQbIpGWP5XibI6x4LqLTJj+VsCAwEAAaNeMFwwHQYDVR0OBBYEFEKP +y8hHZVazpvIsxFcGo4YrkEkwMCAGA1UdJQEB/wQWMBQGCCsGAQUFBwMBBggrBgEF +BQcDAjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIB/jANBgkqhkiG9w0BAQsFAAOC +AgEAMzTRDLBExVFlw98AuX+pM+/R2Gjw5KFHvSYLKLbMRfuuZK1yNYYaYtNrtF+V +a53OFgaZj56o7tXc2PB8kw4MELD0ViR8Do2bvZieFcEe4DwhdjGCjuLehVLT29qI +7T3N/JkJ5daemKZcRB6Ne0F4+6QlVVNck28HUKbQThl88RdwLUImmSAfgKSt6uJ5 +wlH7wiYQR2vPXwSuEYzwot+L/91eBwuQr4Lovx9+TCKTbwQOKYjX4KfcOOQ1rx0M +IMrvwWqnabc6m1F0O6//ibL0kuFkJYEgOH2uJA12FBHO+/q2tcytejkOWKWMJj6Y +2etwIHcpzXaEP7fZ75cFGqcE3s7XGsweBIPLjMP1bKxEcFKzygURm/auUuXBCFBl +E16PB6JEAeCKe/8VFeyucvjPuQDWB49aq+r2SbpbI4IeZdz/QgEIOb0MpwStrvhH +9f/DtGMbjvuAEkRoOorK4m5k4GY3LsWTR2bey27AXk8N7pKarpu2N7ChBPm+EV0Y +H+tAI/OfdZuNUCES00F5UAFdU8zBUZo19ao2ZqfEADimE7Epk2s0bUe4GSqEXJp6 +68oVSMhZmMf/RCSNlr97f34sNiUA1YJ0JbCRZmw8KWNm9H1PARLbrgeRBZ/k31Li +WLDr3fiEVk7SGxj3zo94cS6AT55DyXLiSD/bFmL1QXgZweA= +-----END CERTIFICATE----- +-----BEGIN NOT SUPPORTED----- +This is not required to be valid base64, it should be exactly +ignored. +-----END NOT SUPPORTED----- +-----BEGIN EC PARAMETERS----- +BggqhkjOPQMBBw== +-----END EC PARAMETERS----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMaA7bFrjDDBSik057bIKo7UQXJZNwLK9AjYZQ7yIWFloAoGCCqGSM49 +AwEHoUQDQgAExu0Z/w8nQJZAXeOXOnZun9HiZscY9H/KwYcXpeZHu+f9P9mOUEkH +5Z0av+JKtzhFspjngNLVgWcjlA1L5AJLdA== +-----END EC PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxoDtsWuMMMFKKTTn +tsgqjtRBclk3Asr0CNhlDvIhYWWhRANCAATG7Rn/DydAlkBd45c6dm6f0eJmxxj0 +f8rBhxel5ke75/0/2Y5QSQflnRq/4kq3OEWymOeA0tWBZyOUDUvkAkt0 +-----END PRIVATE KEY----- +-----BEGIN PUBLIC KEY----- +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqIh8FTj9DIgI8DAoCBh+ +6UXOfaWkvNaGZx2GwXl4WDAa/ZSE5/8ofg/6V59bmk9yry57UR4F+blscBvE4g3U +dTvWJOBRD900l21vwpDLKzZguyGOCmKwJu3vCnAQKzBRXW5sDgvO67GeU6kpaic9 +LYPYnYaoxCRTYTZu0wy72rW5G0Fe8Gg/duJmUH7vqGIZupTTVzIBMbFVPBMJqprT +MStDhaUL0JiAz0ZgTeNLRIBZWV9mY4PG3rZtbV0BZGR1ipAq9xfgqJcURCcKl/ZT +UMtzvgk8s5hYkIJX0ZL3qsfdM4BMgIFhHq/GisQKbbu9kWldBrxQylOwa6r0m3Jv +KJX2ViDSORndaCz2sppmVx5HDHnj+Bw381yawphnpumP3BJK4iof//uYKvfdc4RC +y2EXL8PYPsT5DMB0jaBt92ytR5sLhn8Sl9Hk0buN4IjrYPISrdhS45xQXUqxcp9O +9hcU+rSaQyZ45cj+VlWhKq8MDvGvaAONBFSEh01mnUwoJObsAZNVFVtuOkwAli0F +kGouMycQY1BGscpdC516Nya361Hk/ICyby2Y0BJrrVGaSM6poXH9yEjglzAdtSDb +Cvhn/zlAI5ltm4Nv2qTgYBDns5JRGVhBym6RbbZ1C/KfCgn0hOxiw3N7AN4d0K5n +LI6p7U9RnNVbWgbqsuoxBtkCAwEAAQ== +-----END PUBLIC KEY----- +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQC1Dt8tFmGS76ciuNXvk/QRrV8wCcArWxvl7Ku0aSQXgcFBAav6 +P5RD8b+dC9DihSu/r+6OOfjsAZ6oKCq3OTUfmoUhLpoBomxPczJgLyyLD+nQkp5q +B1Q3WB6ACL/HJRRjJEIn7lc5u1FVBGbiCAHKMiaP4BDSym8oqimKC6uiaQIDAQAB +AoGAGKmY7sxQqDIqwwkIYyT1Jv9FqwZ4/a7gYvZVATMdLnKHP3KZ2XGVoZepcRvt +7R0Us3ykcw0kgglKcj9eaizJtnSuoDPPwt53mDypPN2sU3hZgyk2tPgr49DB3MIp +fjoqw4RL/p60ksgGXbDEqBuXqOtH5i61khWlMj+BWL9VDq0CQQDaELWPQGjgs+7X +/QyWMJwOF4FXE4jecH/CcPVDB9K1ukllyC1HqTNe44Sp2bIDuSXXWb8yEixrEWBE +ci2CSSjXAkEA1I4W9IzwEmAeLtL6VBip9ks52O0JKu373/Xv1F2GYdhnQaFw7IC6 +1lSzcYMKGTmDuM8Cj26caldyv19Q0SPmvwJAdRHjZzS9GWWAJJTF3Rvbq/USix0B +renXrRvXkFTy2n1YSjxdkstTuO2Mm2M0HquXlTWpX8hB8HkzpYtmwztjoQJAECKl +LXVReCOhxu4vIJkqtc6qGoSL8J1WRH8X8KgU3nKeDAZkWx++jyyo3pIS/y01iZ71 +U8wSxaPTyyFCMk4mYwJBALjg7g8yDy1Lg9GFfOZvAVzPjqD28jZh/VJsDz9IhYoG +z89iHWHkllOisbOm+SeynVC8CoFXmJPc26U65GcjI18= +-----END RSA PRIVATE KEY----- +-----BEGIN PRIVATE KEY----- +MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALUO3y0WYZLvpyK4 +1e+T9BGtXzAJwCtbG+Xsq7RpJBeBwUEBq/o/lEPxv50L0OKFK7+v7o45+OwBnqgo +Krc5NR+ahSEumgGibE9zMmAvLIsP6dCSnmoHVDdYHoAIv8clFGMkQifuVzm7UVUE +ZuIIAcoyJo/gENLKbyiqKYoLq6JpAgMBAAECgYAYqZjuzFCoMirDCQhjJPUm/0Wr +Bnj9ruBi9lUBMx0ucoc/cpnZcZWhl6lxG+3tHRSzfKRzDSSCCUpyP15qLMm2dK6g +M8/C3neYPKk83axTeFmDKTa0+Cvj0MHcwil+OirDhEv+nrSSyAZdsMSoG5eo60fm +LrWSFaUyP4FYv1UOrQJBANoQtY9AaOCz7tf9DJYwnA4XgVcTiN5wf8Jw9UMH0rW6 +SWXILUepM17jhKnZsgO5JddZvzISLGsRYERyLYJJKNcCQQDUjhb0jPASYB4u0vpU +GKn2SznY7Qkq7fvf9e/UXYZh2GdBoXDsgLrWVLNxgwoZOYO4zwKPbpxqV3K/X1DR +I+a/AkB1EeNnNL0ZZYAklMXdG9ur9RKLHQGt6detG9eQVPLafVhKPF2Sy1O47Yyb +YzQeq5eVNalfyEHweTOli2bDO2OhAkAQIqUtdVF4I6HG7i8gmSq1zqoahIvwnVZE +fxfwqBTecp4MBmRbH76PLKjekhL/LTWJnvVTzBLFo9PLIUIyTiZjAkEAuODuDzIP +LUuD0YV85m8BXM+OoPbyNmH9UmwPP0iFigbPz2IdYeSWU6Kxs6b5J7KdULwKgVeY +k9zbpTrkZyMjXw== +-----END PRIVATE KEY----- +-----BEGIN X509 CRL----- +MIICiTBzAgEBMA0GCSqGSIb3DQEBCwUAMBoxGDAWBgNVBAMMD3Bvbnl0b3duIFJT +QSBDQRcNMjMwNjI3MDgyODEyWhcNMjMwNzI3MDgyODEyWjAVMBMCAgHIFw0yMzA2 +MjcwODI3NTlaoA4wDDAKBgNVHRQEAwIBAjANBgkqhkiG9w0BAQsFAAOCAgEAP6EX +9+hxjx/AqdBpynZXjGkEqigBcLcJ2PADOXngdQI1jC0WuYnZymUimemeULtt8X+1 +ai2KxAuF1m4NEKZsrGKvO+/9s/X1xbGroyHSAMKtZafFopFpoB2aNbYlx7yIyLtD +BBIZIF50g20U+3izqpHutTD10itdk9TLsSceJHpwTkNJtaWMkOfBV28nKzEzVutV +f6WzRpURGzui6nQy7aIqImeanpoBoz323psMfC32U0uMBCZltyHNqsX58/2Uhucx +0IPnitNuhv4scCPf/jeRfGIWDrTf1/25LDzRxyg1S4z9aa+3GM4O3dqy4igZEhgT +q3pjlJ2hUL5E0oqbZDIQD1SN8UUUv5N2AjwZcxVBNnYeGyuO7YpTBYiu62o73iL2 +CjgElfaMq/9hEr9GR9kJozh7VTxtQPbnr4DiucQvhv8o/A1z+zkC0gj8iCLFtDbO +8bvDowcdle9LKkrLaBe6sO+fSH/I9Wj8vrEJKsuwaEraIdEaq2VrIMUPEWN0/MH9 +vTwHyadGSMK4CWtrn9fCAgSLw6NX74D7Cx1IaS8vstMjpeUqOS0dk5ThiW47HceB +DTko7rV5N+RGH2nW1ynLoZKCJQqqZcLilFMyKPui3jifJnQlMFi54jGVgg/D6UQn +7dA7wb2ux/1hSiaarp+mi7ncVOyByz6/WQP8mfc= +-----END X509 CRL----- +-----BEGIN CERTIFICATE REQUEST----- +MIIC+zCCAeMCAQAwfDELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVRleGFzMRQwEgYD +VQQHDAtTYW4gQW50b25pbzEdMBsGA1UECgwURXhhbXBsZSBPcmdhbml6YXRpb24x +EjAQBgNVBAsMCU1hcmtldGluZzEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwJw0BcbuqZyiABlmSYTi1tcr8DB0D +NcTtzsYe7tlyIKd3mEs+u6Pi3rEQGvOw5eo6CmWII2qmVOqJ2f6gjl2lZJ5DUE6B +I+NNE73zfFMrttUtI8X4ChnE4rrGqqUsSvYz1YVU0KiJ/00YMjEY5XlJYYa9FgfZ +sUrhj4aCFdXS6CU9jueRr+udEBElDcgTS9+pB+LFhVfUMTdxnJ3BcT4ZDDqODH3/ +5RAgq03dhRpkkaVIg2uVKTBDoM3hs8T1zIxLM7hItaZzMv4uHdfI8y+BdHrePT33 +BoTlocvTEZEqqXEdw2kUd4PDgyUTjFE3b9OeLk0Ju5GRvuCW3UcS5gFvAgMBAAGg +OjA4BgkqhkiG9w0BCQ4xKzApMCcGA1UdEQQgMB6CC2V4YW1wbGUuY29tgg9mb28u +ZXhhbXBsZS5jb20wDQYJKoZIhvcNAQELBQADggEBACWsgxPw13QUpoCJOqvp8B1A +EfsxRJITSROmukV3ZQycPT76Y3GVrM9sGjO8p13J/CVw2KcWc9xmgHF0MdvPNhnW +OB6Y07hVpNnJVHb1KglOkNkTy6sVDtnZHg2klqGSyzIbwZ9R3JG8HtRdkceIrm3D +gdiZyLcf1VDCCUGaskEi2CsggCQQJNyGi+8BSQ8MPKm/m0KrSchGQ157eWCCjopz +f5GQe2UGOg5T7g8+S4GdECMwkMlTGUwlAM6LuOG/NZqP528PCAYQv0eOYdSwALQT +GwTyU4AZ9y1uBFuaFxABew9GbDEtNY/XHTF8308edUwGBk6jfD+UuTeEwRZGs9E= +-----END CERTIFICATE REQUEST----- +-----BEGIN ECHCONFIG----- +AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEA +AQALZXhhbXBsZS5jb20AAA== +-----END ECHCONFIG----- +... that's all folks! diff --git a/tests/dns_name.rs b/tests/dns_name.rs new file mode 100644 index 0000000..bfe2760 --- /dev/null +++ b/tests/dns_name.rs @@ -0,0 +1,408 @@ +use rustls_pki_types::DnsName; + +// (name, is_valid) +static DNS_NAME_VALIDITY: &[(&[u8], bool)] = &[ + (b"a", true), + (b"a.b", true), + (b"a.b.c", true), + (b"a.b.c.d", true), + + // Hyphens, one component. + (b"-", false), + (b"-a", false), + (b"a-", false), + (b"a-b", true), + + // Hyphens, last component. + (b"a.-", false), + (b"a.-a", false), + (b"a.a-", false), + (b"a.a-b", true), + + // Hyphens, not last component. + (b"-.a", false), + (b"-a.a", false), + (b"a-.a", false), + (b"a-b.a", true), + + // Underscores, one component. + (b"_", true), // TODO: Perhaps this should be rejected for '_' being sole character?. + (b"_a", true), // TODO: Perhaps this should be rejected for '_' being 1st? + (b"a_", true), + (b"a_b", true), + + // Underscores, last component. + (b"a._", true), // TODO: Perhaps this should be rejected for '_' being sole character?. + (b"a._a", true), // TODO: Perhaps this should be rejected for '_' being 1st? + (b"a.a_", true), + (b"a.a_b", true), + + // Underscores, not last component. + (b"_.a", true), // TODO: Perhaps this should be rejected for '_' being sole character?. + (b"_a.a", true), + (b"a_.a", true), + (b"a_b.a", true), + + // empty labels + (b"", false), + (b".", false), + (b"a", true), + (b".a", false), + (b".a.b", false), + (b"..a", false), + (b"a..b", false), + (b"a...b", false), + (b"a..b.c", false), + (b"a.b..c", false), + (b".a.b.c.", false), + + // absolute names + (b"a.", true), + (b"a.b.", true), + (b"a.b.c.", true), + + // absolute names with empty label at end + (b"a..", false), + (b"a.b..", false), + (b"a.b.c..", false), + (b"a...", false), + + // Punycode + (b"xn--", false), + (b"xn--.", false), + (b"xn--.a", false), + (b"a.xn--", false), + (b"a.xn--.", false), + (b"a.xn--.b", false), + (b"a.xn--.b", false), + (b"a.xn--\0.b", false), + (b"a.xn--a.b", true), + (b"xn--a", true), + (b"a.xn--a", true), + (b"a.xn--a.a", true), + (b"\xc4\x95.com", false), // UTF-8 ĕ + (b"xn--jea.com", true), // punycode ĕ + (b"xn--\xc4\x95.com", false), // UTF-8 ĕ, malformed punycode + UTF-8 mashup + + // Surprising punycode + (b"xn--google.com", true), // 䕮䕵䕶䕱.com + (b"xn--citibank.com", true), // 岍岊岊岅岉岎.com + (b"xn--cnn.com", true), // 䁾.com + (b"a.xn--cnn", true), // a.䁾 + (b"a.xn--cnn.com", true), // a.䁾.com + + (b"1.2.3.4", false), // IPv4 address + (b"1::2", false), // IPV6 address + + // whitespace not allowed anywhere. + (b" ", false), + (b" a", false), + (b"a ", false), + (b"a b", false), + (b"a.b 1", false), + (b"a\t", false), + + // Nulls not allowed + (b"\0", false), + (b"a\0", false), + (b"example.org\0.example.com", false), // Hi Moxie! + (b"\0a", false), + (b"xn--\0", false), + + // Allowed character set + (b"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z", true), + (b"A.B.C.D.E.F.G.H.I.J.K.L.M.N.O.P.Q.R.S.T.U.V.W.X.Y.Z", true), + (b"0.1.2.3.4.5.6.7.8.9.a", true), // "a" needed to avoid numeric last label + (b"a-b", true), // hyphen (a label cannot start or end with a hyphen) + + // An invalid character in various positions + (b"!", false), + (b"!a", false), + (b"a!", false), + (b"a!b", false), + (b"a.!", false), + (b"a.a!", false), + (b"a.!a", false), + (b"a.a!a", false), + (b"a.!a.a", false), + (b"a.a!.a", false), + (b"a.a!a.a", false), + + // Various other invalid characters + (b"a!", false), + (b"a@", false), + (b"a#", false), + (b"a$", false), + (b"a%", false), + (b"a^", false), + (b"a&", false), + (b"a*", false), + (b"a(", false), + (b"a)", false), + + // last label can't be fully numeric + (b"1", false), + (b"a.1", false), + + // other labels can be fully numeric + (b"1.a", true), + (b"1.2.a", true), + (b"1.2.3.a", true), + + // last label can be *partly* numeric + (b"1a", true), + (b"1.1a", true), + (b"1-1", true), + (b"a.1-1", true), + (b"a.1-a", true), + + // labels cannot start with a hyphen + (b"-", false), + (b"-1", false), + + // labels cannot end with a hyphen + (b"1-", false), + (b"1-.a", false), + (b"a-", false), + (b"a-.a", false), + (b"a.1-.a", false), + (b"a.a-.a", false), + + // labels can contain a hyphen in the middle + (b"a-b", true), + (b"1-2", true), + (b"a.a-1", true), + + // multiple consecutive hyphens allowed + (b"a--1", true), + (b"1---a", true), + (b"a-----------------b", true), + + // Wildcard specifications are not valid reference names. + (b"*.a", false), + (b"a*", false), + (b"a*.", false), + (b"a*.a", false), + (b"a*.a.", false), + (b"*.a.b", false), + (b"*.a.b.", false), + (b"a*.b.c", false), + (b"*.a.b.c", false), + (b"a*.b.c.d", false), + + // Multiple wildcards. + (b"a**.b.c", false), + (b"a*b*.c.d", false), + (b"a*.b*.c", false), + + // Wildcards not in the first label. + (b"a.*", false), + (b"a.*.b", false), + (b"a.b.*", false), + (b"a.b*.c", false), + (b"*.b*.c", false), + (b".*.a.b", false), + (b".a*.b.c", false), + + // Wildcards not at the end of the first label. + (b"*a.b.c", false), + (b"a*b.c.d", false), + + // Wildcards and IDNA prefix. + (b"x*.a.b", false), + (b"xn*.a.b", false), + (b"xn-*.a.b", false), + (b"xn--*.a.b", false), + (b"xn--w*.a.b", false), + + // Redacted labels from RFC6962bis draft 4 + // https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-04#section-3.2.2 + (b"(PRIVATE).foo", false), + + // maximum label length is 63 characters + (b"123456789012345678901234567890123456789012345678901234567890abc", true), + (b"123456789012345678901234567890123456789012345678901234567890abcd", false), + + // maximum total length is 253 characters + (b"12345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890.123456789012345678901234567890123456789012345678a", + true), + (b"12345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890.12345678901234567890123456789012345678901234567890.1234567890123456789012345678901234567890123456789a", + false), +]; + +// (IP address, is valid DNS name). The comments here refer to the validity of +// the string as an IP address, not as a DNS name validity. +static IP_ADDRESS_DNS_VALIDITY: &[(&[u8], bool)] = &[ + (b"", false), + (b"1", false), + (b"1.2", false), + (b"1.2.3", false), + (b"1.2.3.4", false), + (b"1.2.3.4.5", false), + (b"1.2.3.4a", true), // a DNS name! + (b"a.2.3.4", false), // not even a DNS name! + (b"1::2", false), // IPv6 address + // Whitespace not allowed + (b" 1.2.3.4", false), + (b"1.2.3.4 ", false), + (b"1 .2.3.4", false), + (b"\n1.2.3.4", false), + (b"1.2.3.4\n", false), + // Nulls not allowed + (b"\x00", false), + (b"\x001.2.3.4", false), + (b"1.2.3.4\x00", false), + (b"1.2.3.4\x00.5", false), + // Range + (b"0.0.0.0", false), + (b"255.255.255.255", false), + (b"256.0.0.0", false), + (b"0.256.0.0", false), + (b"0.0.256.0", false), + (b"0.0.0.256", false), + (b"999.0.0.0", false), + (b"9999999999999999999.0.0.0", false), + // All digits allowed + (b"0.1.2.3", false), + (b"4.5.6.7", false), + (b"8.9.0.1", false), + // Leading zeros not allowed + (b"01.2.3.4", false), + (b"001.2.3.4", false), + (b"00000000001.2.3.4", false), + (b"010.2.3.4", false), + (b"1.02.3.4", false), + (b"1.2.03.4", false), + (b"1.2.3.04", false), + // Empty components + (b".2.3.4", false), + (b"1..3.4", false), + (b"1.2..4", false), + (b"1.2.3.", false), + // Too many components + (b"1.2.3.4.5", false), + (b"1.2.3.4.5.6", false), + (b"0.1.2.3.4", false), + (b"1.2.3.4.0", false), + // Leading/trailing dot + (b".1.2.3.4", false), + (b"1.2.3.4.", false), + // Other common forms of IPv4 address + // http://en.wikipedia.org/wiki/IPv4#Address_representations + (b"192.0.2.235", false), // dotted decimal (control value) + (b"0xC0.0x00.0x02.0xEB", true), // dotted hex - actually a DNS name! + (b"0301.0000.0002.0353", false), // dotted octal + (b"0xC00002EB", true), // non-dotted hex, actually a DNS name! + (b"3221226219", false), // non-dotted decimal + (b"030000001353", false), // non-dotted octal + (b"192.0.0002.0xEB", true), // mixed, actually a DNS name! + (b"1234", false), + (b"1234:5678", false), + (b"1234:5678:9abc", false), + (b"1234:5678:9abc:def0", false), + (b"1234:5678:9abc:def0:1234:", false), + (b"1234:5678:9abc:def0:1234:5678:", false), + (b"1234:5678:9abc:def0:1234:5678:9abc:", false), + (b"1234:5678:9abc:def0:1234:5678:9abc:def0", false), + (b"1234:5678:9abc:def0:1234:5678:9abc:def0:", false), + (b":1234:5678:9abc:def0:1234:5678:9abc:def0", false), + (b"1234:5678:9abc:def0:1234:5678:9abc:def0:0000", false), + // Valid contractions + (b"::1", false), + (b"::1234", false), + (b"1234::", false), + (b"1234::5678", false), + (b"1234:5678::abcd", false), + (b"1234:5678:9abc:def0:1234:5678:9abc::", false), + // Contraction in full IPv6 addresses not allowed + (b"::1234:5678:9abc:def0:1234:5678:9abc:def0", false), // start + (b"1234:5678:9abc:def0:1234:5678:9abc:def0::", false), // end + (b"1234:5678::9abc:def0:1234:5678:9abc:def0", false), // interior + // Multiple contractions not allowed + (b"::1::", false), + (b"::1::2", false), + (b"1::2::", false), + // Colon madness! + (b":", false), + (b"::", false), + (b":::", false), + (b"::::", false), + (b":::1", false), + (b"::::1", false), + (b"1:::2", false), + (b"1::::2", false), + (b"1:2:::", false), + (b"1:2::::", false), + (b"::1234:", false), + (b":1234::", false), + (b"01234::", false), // too many digits, even if zero + (b"12345678::", false), // too many digits or missing colon + // uppercase + (b"ABCD:EFAB::", false), + // miXeD CAse + (b"aBcd:eFAb::", false), + // IPv4-style + (b"::2.3.4.5", false), + (b"1234::2.3.4.5", false), + (b"::abcd:2.3.4.5", false), + (b"1234:5678:9abc:def0:1234:5678:252.253.254.255", false), + (b"1234:5678:9abc:def0:1234::252.253.254.255", false), + (b"1234::252.253.254", false), + (b"::252.253.254", false), + (b"::252.253.254.300", false), + (b"1234::252.253.254.255:", false), + (b"1234::252.253.254.255:5678", false), + // Contractions that don't contract + (b"::1234:5678:9abc:def0:1234:5678:9abc:def0", false), + (b"1234:5678:9abc:def0:1234:5678:9abc:def0::", false), + (b"1234:5678:9abc:def0::1234:5678:9abc:def0", false), + (b"1234:5678:9abc:def0:1234:5678::252.253.254.255", false), + // With and without leading zeros + (b"::123", false), + (b"::0123", false), + (b"::012", false), + (b"::0012", false), + (b"::01", false), + (b"::001", false), + (b"::0001", false), + (b"::0", false), + (b"::00", false), + (b"::000", false), + (b"::0000", false), + (b"::01234", false), + (b"::00123", false), + (b"::000123", false), + // Trailing zero + (b"::12340", false), + // Whitespace + (b" 1234:5678:9abc:def0:1234:5678:9abc:def0", false), + (b"\t1234:5678:9abc:def0:1234:5678:9abc:def0", false), + (b"\t1234:5678:9abc:def0:1234:5678:9abc:def0\n", false), + (b"1234 :5678:9abc:def0:1234:5678:9abc:def0", false), + (b"1234: 5678:9abc:def0:1234:5678:9abc:def0", false), + (b":: 2.3.4.5", false), + (b"1234::252.253.254.255 ", false), + (b"1234::252.253.254.255\n", false), + (b"1234::252.253. 254.255", false), + // Nulls + (b"\x00", false), + (b"::1\x00:2", false), + (b"::1\x00", false), + (b"::1.2.3.4\x00", false), + (b"::1.2\x002.3.4", false), +]; + +#[test] +fn dns_name_ref_try_from_ascii_test() { + for &(s, is_valid) in DNS_NAME_VALIDITY + .iter() + .chain(IP_ADDRESS_DNS_VALIDITY.iter()) + { + assert_eq!( + DnsName::try_from(s).is_ok(), + is_valid, + "DnsNameRef::try_from_ascii_str failed for \"{:?}\"", + s + ); + } +} diff --git a/tests/key_type.rs b/tests/key_type.rs new file mode 100644 index 0000000..b15e9ca --- /dev/null +++ b/tests/key_type.rs @@ -0,0 +1,42 @@ +use rustls_pki_types::PrivateKeyDer; + +#[test] +fn test_private_key_from_der() { + const NIST_P256_KEY_SEC1: &[u8] = include_bytes!("../tests/keys/nistp256key.der"); + const NIST_P384_KEY_SEC1: &[u8] = include_bytes!("../tests/keys/nistp384key.der"); + const NIST_P521_KEY_SEC1: &[u8] = include_bytes!("../tests/keys/nistp521key.der"); + for bytes in [NIST_P256_KEY_SEC1, NIST_P384_KEY_SEC1, NIST_P521_KEY_SEC1] { + assert!(matches!( + PrivateKeyDer::try_from(bytes).unwrap(), + PrivateKeyDer::Sec1(_) + )); + } + + const RSA_2048_KEY_PKCS1: &[u8] = include_bytes!("../tests/keys/rsa2048key.pkcs1.der"); + assert!(matches!( + PrivateKeyDer::try_from(RSA_2048_KEY_PKCS1).unwrap(), + PrivateKeyDer::Pkcs1(_) + )); + + const NIST_P256_KEY_PKCS8: &[u8] = include_bytes!("../tests/keys/nistp256key.pkcs8.der"); + const NIST_P384_KEY_PKCS8: &[u8] = include_bytes!("../tests/keys/nistp384key.pkcs8.der"); + const NIST_P521_KEY_PKCS8: &[u8] = include_bytes!("../tests/keys/nistp521key.pkcs8.der"); + const RSA_2048_KEY_PKCS8: &[u8] = include_bytes!("../tests/keys/rsa2048key.pkcs8.der"); + const RSA_4096_KEY: &[u8] = include_bytes!("../tests/keys/rsa4096key.pkcs8.der"); + const ED25519_KEY: &[u8] = include_bytes!("../tests/keys/edd25519_v2.der"); + const PKCS8_KEYS: &[&[u8]] = &[ + NIST_P256_KEY_PKCS8, + NIST_P384_KEY_PKCS8, + NIST_P521_KEY_PKCS8, + RSA_2048_KEY_PKCS8, + RSA_4096_KEY, + ED25519_KEY, + ]; + + for &bytes in PKCS8_KEYS { + assert!(matches!( + PrivateKeyDer::try_from(bytes).unwrap(), + PrivateKeyDer::Pkcs8(_) + )); + } +} diff --git a/tests/keys/edd25519_v2.der b/tests/keys/edd25519_v2.der new file mode 100644 index 0000000..9257be2 Binary files /dev/null and b/tests/keys/edd25519_v2.der differ diff --git a/tests/keys/eddsakey.der b/tests/keys/eddsakey.der new file mode 100644 index 0000000..8eff00d Binary files /dev/null and b/tests/keys/eddsakey.der differ diff --git a/tests/keys/nistp256key.der b/tests/keys/nistp256key.der new file mode 100644 index 0000000..5755182 Binary files /dev/null and b/tests/keys/nistp256key.der differ diff --git a/tests/keys/nistp256key.pkcs8.der b/tests/keys/nistp256key.pkcs8.der new file mode 100644 index 0000000..8e64b2c Binary files /dev/null and b/tests/keys/nistp256key.pkcs8.der differ diff --git a/tests/keys/nistp384key.der b/tests/keys/nistp384key.der new file mode 100644 index 0000000..80f0769 Binary files /dev/null and b/tests/keys/nistp384key.der differ diff --git a/tests/keys/nistp384key.pkcs8.der b/tests/keys/nistp384key.pkcs8.der new file mode 100644 index 0000000..f85de43 Binary files /dev/null and b/tests/keys/nistp384key.pkcs8.der differ diff --git a/tests/keys/nistp521key.der b/tests/keys/nistp521key.der new file mode 100644 index 0000000..98700ee Binary files /dev/null and b/tests/keys/nistp521key.der differ diff --git a/tests/keys/nistp521key.pkcs8.der b/tests/keys/nistp521key.pkcs8.der new file mode 100644 index 0000000..1623c37 Binary files /dev/null and b/tests/keys/nistp521key.pkcs8.der differ diff --git a/tests/keys/rsa2048key.pkcs1.der b/tests/keys/rsa2048key.pkcs1.der new file mode 100644 index 0000000..d93402d Binary files /dev/null and b/tests/keys/rsa2048key.pkcs1.der differ diff --git a/tests/keys/rsa2048key.pkcs8.der b/tests/keys/rsa2048key.pkcs8.der new file mode 100644 index 0000000..8e5c2e8 Binary files /dev/null and b/tests/keys/rsa2048key.pkcs8.der differ diff --git a/tests/keys/rsa4096key.pkcs8.der b/tests/keys/rsa4096key.pkcs8.der new file mode 100644 index 0000000..242ea48 Binary files /dev/null and b/tests/keys/rsa4096key.pkcs8.der differ diff --git a/tests/pem.rs b/tests/pem.rs new file mode 100644 index 0000000..f8eaab7 --- /dev/null +++ b/tests/pem.rs @@ -0,0 +1,334 @@ +#![cfg(feature = "std")] + +use std::io::Cursor; + +use rustls_pki_types::pem::PemObject; +use rustls_pki_types::{ + pem, CertificateDer, CertificateRevocationListDer, CertificateSigningRequestDer, + EchConfigListBytes, PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, PrivateSec1KeyDer, + SubjectPublicKeyInfoDer, +}; + +#[test] +fn pkcs1_private_key() { + let data = include_bytes!("data/zen.pem"); + + PrivatePkcs1KeyDer::from_pem_slice(data).unwrap(); + PrivatePkcs1KeyDer::from_pem_reader(&mut Cursor::new(&data[..])).unwrap(); + PrivatePkcs1KeyDer::from_pem_file("tests/data/zen.pem").unwrap(); + + assert!(matches!( + PrivatePkcs1KeyDer::from_pem_file("tests/data/certificate.chain.pem").unwrap_err(), + pem::Error::NoItemsFound + )); +} + +#[test] +fn pkcs8_private_key() { + let data = include_bytes!("data/zen.pem"); + + PrivatePkcs8KeyDer::from_pem_slice(data).unwrap(); + PrivatePkcs8KeyDer::from_pem_reader(&mut Cursor::new(&data[..])).unwrap(); + PrivatePkcs8KeyDer::from_pem_file("tests/data/zen.pem").unwrap(); + + assert!(matches!( + PrivatePkcs8KeyDer::from_pem_file("tests/data/certificate.chain.pem").unwrap_err(), + pem::Error::NoItemsFound + )); +} + +#[test] +fn sec1_private_key() { + let data = include_bytes!("data/zen.pem"); + + PrivateSec1KeyDer::from_pem_slice(data).unwrap(); + PrivateSec1KeyDer::from_pem_reader(&mut Cursor::new(&data[..])).unwrap(); + PrivateSec1KeyDer::from_pem_file("tests/data/zen.pem").unwrap(); + + assert!(matches!( + PrivateSec1KeyDer::from_pem_file("tests/data/certificate.chain.pem").unwrap_err(), + pem::Error::NoItemsFound + )); +} + +#[test] +fn any_private_key() { + let data = include_bytes!("data/zen.pem"); + + PrivateKeyDer::from_pem_slice(data).unwrap(); + PrivateKeyDer::from_pem_reader(&mut Cursor::new(&data[..])).unwrap(); + PrivateKeyDer::from_pem_file("tests/data/zen.pem").unwrap(); + + for other_file in [ + "tests/data/nistp256key.pem", + "tests/data/nistp256key.pkcs8.pem", + "tests/data/rsa1024.pkcs1.pem", + "tests/data/rsa1024.pkcs8.pem", + ] { + PrivateKeyDer::from_pem_file(other_file).unwrap(); + } + + assert!(matches!( + PrivateKeyDer::from_pem_file("tests/data/certificate.chain.pem").unwrap_err(), + pem::Error::NoItemsFound + )); +} + +#[test] +fn no_trailing_newline() { + let data = include_bytes!("data/rsa-key-no-trailing-newline.pem"); + assert_eq!( + PrivatePkcs8KeyDer::pem_slice_iter(data) + .collect::, _>>() + .unwrap() + .len(), + 1 + ); + + assert_eq!( + PrivatePkcs8KeyDer::pem_file_iter("tests/data/rsa-key-no-trailing-newline.pem") + .unwrap() + .collect::, _>>() + .unwrap() + .len(), + 1 + ); +} + +#[test] +fn certificates() { + let data = include_bytes!("data/zen.pem"); + + assert_eq!( + CertificateDer::pem_slice_iter(data) + .collect::, _>>() + .unwrap() + .len(), + 4 + ); + assert_eq!( + CertificateDer::pem_reader_iter(&mut Cursor::new(&data[..])) + .collect::, _>>() + .unwrap() + .len(), + 4 + ); + assert_eq!( + CertificateDer::pem_file_iter("tests/data/zen.pem") + .unwrap() + .count(), + 4 + ); + + assert!(matches!( + CertificateDer::from_pem_file("tests/data/crl.pem").unwrap_err(), + pem::Error::NoItemsFound + )); + + assert_eq!( + CertificateDer::pem_file_iter("tests/data/certificate.chain.pem") + .unwrap() + .count(), + 3 + ); +} + +#[test] +fn public_keys() { + let data = include_bytes!("data/spki.pem"); + + SubjectPublicKeyInfoDer::from_pem_slice(data).unwrap(); + SubjectPublicKeyInfoDer::from_pem_reader(&mut Cursor::new(&data[..])).unwrap(); + SubjectPublicKeyInfoDer::from_pem_file("tests/data/spki.pem").unwrap(); + + assert!(matches!( + SubjectPublicKeyInfoDer::from_pem_file("tests/data/certificate.chain.pem").unwrap_err(), + pem::Error::NoItemsFound + )); +} + +#[test] +fn csr() { + let data = include_bytes!("data/zen.pem"); + + CertificateSigningRequestDer::from_pem_slice(data).unwrap(); + CertificateSigningRequestDer::from_pem_reader(&mut Cursor::new(&data[..])).unwrap(); + CertificateSigningRequestDer::from_pem_file("tests/data/zen.pem").unwrap(); + + assert!(matches!( + CertificateSigningRequestDer::from_pem_file("tests/data/certificate.chain.pem") + .unwrap_err(), + pem::Error::NoItemsFound + )); +} + +#[test] +fn crls() { + let data = include_bytes!("data/zen.pem"); + + assert_eq!( + CertificateRevocationListDer::pem_slice_iter(data) + .collect::, _>>() + .unwrap() + .len(), + 1 + ); + assert_eq!( + CertificateRevocationListDer::pem_reader_iter(&mut Cursor::new(&data[..])) + .collect::, _>>() + .unwrap() + .len(), + 1 + ); + assert_eq!( + CertificateRevocationListDer::pem_file_iter("tests/data/zen.pem") + .unwrap() + .count(), + 1 + ); + + assert!(matches!( + CertificateRevocationListDer::pem_file_iter("tests/data/certificate.chain.pem") + .unwrap() + .count(), + 0 + )); + + assert_eq!( + CertificateRevocationListDer::pem_file_iter("tests/data/crl.pem") + .unwrap() + .count(), + 1 + ); +} + +#[test] +fn ech_config() { + let data = include_bytes!("data/zen.pem"); + + EchConfigListBytes::from_pem_slice(data).unwrap(); + EchConfigListBytes::from_pem_reader(&mut Cursor::new(&data[..])).unwrap(); + EchConfigListBytes::from_pem_file("tests/data/zen.pem").unwrap(); + + assert!(matches!( + EchConfigListBytes::from_pem_file("tests/data/certificate.chain.pem").unwrap_err(), + pem::Error::NoItemsFound + )); + + let (config, key) = EchConfigListBytes::config_and_key_from_iter( + PemObject::pem_file_iter("tests/data/ech.pem").unwrap(), + ) + .unwrap(); + println!("{config:?} {key:?}"); + + assert!(matches!( + EchConfigListBytes::config_and_key_from_iter( + PemObject::pem_file_iter("tests/data/certificate.chain.pem").unwrap(), + ) + .unwrap_err(), + pem::Error::NoItemsFound, + )); +} + +#[test] +fn certificates_with_binary() { + let data = include_bytes!("data/gunk.pem"); + + assert_eq!( + CertificateDer::pem_slice_iter(data) + .collect::, _>>() + .unwrap() + .len(), + 2 + ); + assert_eq!( + CertificateDer::pem_reader_iter(&mut Cursor::new(&data[..])) + .collect::, _>>() + .unwrap() + .len(), + 2 + ); + assert_eq!( + CertificateDer::pem_file_iter("tests/data/gunk.pem") + .unwrap() + .count(), + 2 + ); +} + +#[test] +fn parse_in_order() { + let data = include_bytes!("data/zen.pem"); + let items = <(pem::SectionKind, Vec) as PemObject>::pem_slice_iter(data) + .collect::, _>>() + .unwrap(); + assert_eq!(items.len(), 12); + assert!(matches!(items[0], (pem::SectionKind::Certificate, _))); + assert!(matches!(items[1], (pem::SectionKind::Certificate, _))); + assert!(matches!(items[2], (pem::SectionKind::Certificate, _))); + assert!(matches!(items[3], (pem::SectionKind::Certificate, _))); + assert!(matches!(items[4], (pem::SectionKind::EcPrivateKey, _))); + assert!(matches!(items[5], (pem::SectionKind::PrivateKey, _))); + assert!(matches!(items[6], (pem::SectionKind::PublicKey, _))); + assert!(matches!(items[7], (pem::SectionKind::RsaPrivateKey, _))); + assert!(matches!(items[8], (pem::SectionKind::PrivateKey, _))); + assert!(matches!(items[9], (pem::SectionKind::Crl, _))); + assert!(matches!(items[10], (pem::SectionKind::Csr, _))); + assert!(matches!(items[11], (pem::SectionKind::EchConfigList, _))); +} + +#[test] +fn whitespace_prefix() { + CertificateDer::from_pem_file("tests/data/whitespace-prefix.crt").unwrap(); +} + +#[test] +fn different_line_endings() { + let data = include_bytes!("data/mixed-line-endings.crt"); + + // Ensure non-LF line endings are not lost by mistake, causing the test + // to silently regress. + let mut contained_unix_ending = false; + let mut contained_other_ending = false; + for byte in data.iter().copied() { + if contained_other_ending && contained_unix_ending { + break; + } + + if byte == b'\n' { + contained_unix_ending = true; + } else if byte == b'\r' { + contained_other_ending = true; + } + } + assert!(contained_unix_ending); + assert!(contained_other_ending); + + assert_eq!( + CertificateDer::pem_slice_iter(data) + .collect::, _>>() + .unwrap() + .len(), + 4 + ); + assert_eq!( + CertificateDer::pem_file_iter("tests/data/mixed-line-endings.crt") + .unwrap() + .count(), + 4 + ); +} + +#[test] +fn slice_iterator() { + let slice = b"hello\n-----BEGIN CERTIFICATE-----\naGk=\n-----END CERTIFICATE-----\ngoodbye\n"; + + let mut iter = CertificateDer::pem_slice_iter(slice); + assert_eq!(iter.remainder(), slice); + assert_eq!( + iter.next().unwrap().unwrap(), + CertificateDer::from(&b"hi"[..]) + ); + assert_eq!(iter.remainder(), b"goodbye\n"); + assert!(iter.next().is_none()); +} diff --git a/tests/server_name.rs b/tests/server_name.rs new file mode 100644 index 0000000..2876eba --- /dev/null +++ b/tests/server_name.rs @@ -0,0 +1,59 @@ +use rustls_pki_types::ServerName; + +fn compile_time_assert_hash() {} +fn compile_time_assert_send() {} +fn compile_time_assert_sync() {} + +#[test] +fn test_server_name_traits() { + compile_time_assert_hash::(); + compile_time_assert_send::(); + compile_time_assert_sync::(); + + let a = ServerName::try_from(&b"example.com"[..]).unwrap(); + + // `Clone` + #[allow(clippy::clone_on_copy)] + let _ = a.clone(); + // TODO: verify the clone is the same as `a`. + + // TODO: Don't require `alloc` for these. + #[cfg(feature = "alloc")] + { + // `Debug`. + assert_eq!(format!("{:?}", &a), "DnsName(\"example.com\")"); + } +} + +#[cfg(feature = "alloc")] +#[test] +fn test_alloc_server_name_traits() { + let a_ref = ServerName::try_from(&b"example.com"[..]).unwrap(); + let a = a_ref.to_owned(); + + // `Clone`, `Debug`, `PartialEq`. + assert_eq!(&a, &a.clone()); + + // `Debug`. + assert_eq!(format!("{:?}", &a), "DnsName(\"example.com\")"); + + // PartialEq is case-insensitive + assert_eq!( + a, + ServerName::try_from(&b"Example.Com"[..]) + .unwrap() + .to_owned() + ); + + // PartialEq isn't completely wrong. + assert_ne!( + a, + ServerName::try_from(&b"fxample.com"[..]) + .unwrap() + .to_owned() + ); + assert_ne!( + a, + ServerName::try_from(&b"example.co"[..]).unwrap().to_owned() + ); +}