├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ └── ci.yml ├── .gitignore ├── .rustfmt.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── codecov.yml ├── rcgen ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── docs │ └── 0.12-to-0.13.md ├── examples │ ├── rsa-irc-openssl.rs │ ├── sign-leaf-with-ca.rs │ └── simple.rs ├── src │ ├── certificate.rs │ ├── crl.rs │ ├── csr.rs │ ├── error.rs │ ├── key_pair.rs │ ├── lib.rs │ ├── oid.rs │ ├── ring_like.rs │ ├── sign_algo.rs │ └── string.rs └── tests │ ├── botan.rs │ ├── generic.rs │ ├── openssl.rs │ ├── util.rs │ └── webpki.rs └── rustls-cert-gen ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── cert.rs ├── lib.rs └── main.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | tab_width = 4 7 | end_of_line=lf 8 | charset=utf-8 9 | trim_trailing_whitespace=true 10 | insert_final_newline=true 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: ['main', 'rel-*', 'ci/*'] 6 | pull_request: 7 | merge_group: 8 | schedule: 9 | - cron: '0 18 * * *' 10 | workflow_dispatch: 11 | 12 | env: 13 | RUSTFLAGS: -D warnings 14 | 15 | jobs: 16 | lint: 17 | name: Format & clippy 18 | runs-on: ubuntu-latest 19 | continue-on-error: true 20 | steps: 21 | - name: Checkout sources 22 | uses: actions/checkout@v4 23 | with: 24 | persist-credentials: false 25 | - name: Install rust toolchain 26 | uses: dtolnay/rust-toolchain@stable 27 | with: 28 | components: clippy, rustfmt 29 | - run: cargo fmt -- --check 30 | - run: cargo clippy --all-features --all-targets 31 | # rustls-cert-gen require either aws_lc_rs or ring feature 32 | - run: cargo clippy -p rcgen --no-default-features --all-targets 33 | - run: cargo clippy --no-default-features --features ring --all-targets 34 | - run: cargo clippy --no-default-features --features aws_lc_rs --all-targets 35 | - run: cargo clippy --no-default-features --features aws_lc_rs,pem --all-targets 36 | 37 | rustdoc: 38 | name: Documentation 39 | runs-on: ubuntu-latest 40 | strategy: 41 | matrix: 42 | toolchain: [stable, nightly] 43 | steps: 44 | - name: Checkout sources 45 | uses: actions/checkout@v4 46 | with: 47 | persist-credentials: false 48 | - name: Install rust toolchain 49 | uses: dtolnay/rust-toolchain@master 50 | with: 51 | toolchain: ${{ matrix.toolchain }} 52 | - name: cargo doc (all features) 53 | run: cargo doc --all-features --document-private-items 54 | env: 55 | RUSTDOCFLAGS: ${{ matrix.toolchain == 'nightly' && '-Dwarnings --cfg=docsrs' || '-Dwarnings' }} 56 | 57 | check-external-types: 58 | name: Validate external types appearing in public API 59 | runs-on: ubuntu-latest 60 | steps: 61 | - name: Checkout sources 62 | uses: actions/checkout@v4 63 | with: 64 | persist-credentials: false 65 | - name: Install rust toolchain 66 | uses: dtolnay/rust-toolchain@master 67 | with: 68 | toolchain: nightly-2025-05-04 69 | # ^ sync with https://github.com/awslabs/cargo-check-external-types/blob/main/rust-toolchain.toml 70 | - run: cargo install --locked cargo-check-external-types 71 | - name: run cargo-check-external-types for rcgen/ 72 | working-directory: rcgen/ 73 | run: cargo check-external-types --all-features 74 | 75 | semver: 76 | name: Check semver compatibility 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Checkout sources 80 | uses: actions/checkout@v4 81 | with: 82 | persist-credentials: false 83 | 84 | - name: Check semver 85 | uses: obi1kenobi/cargo-semver-checks-action@v2 86 | with: 87 | exclude: rustls-cert-gen 88 | 89 | build-windows: 90 | runs-on: windows-latest 91 | env: 92 | # botan doesn't build on windows if the source is 93 | # on a different drive than the artifacts 94 | # https://github.com/randombit/botan-rs/issues/82 95 | BOTAN_CONFIGURE_LINK_METHOD: copy 96 | steps: 97 | - uses: actions/checkout@v4 98 | with: 99 | persist-credentials: false 100 | - uses: actions/cache@v4 101 | with: 102 | path: | 103 | ~/.cargo/bin/ 104 | ~/.cargo/registry/index/ 105 | ~/.cargo/registry/cache/ 106 | ~/.cargo/git/db/ 107 | target/ 108 | $VCPKG_DEFAULT_BINARY_CACHE 109 | key: ${{ runner.os }}-cargo-stable-${{ hashFiles('Cargo.lock') }} 110 | - uses: egor-tensin/vs-shell@v2 111 | with: 112 | arch: amd64 113 | - name: Install Rust 114 | uses: dtolnay/rust-toolchain@stable 115 | - name: Install NASM for aws-lc-rs on Windows 116 | uses: ilammy/setup-nasm@v1 117 | - name: Run cargo check 118 | run: cargo check --all-targets 119 | - name: Run the tests 120 | run: cargo test 121 | - name: Run the tests with x509-parser enabled 122 | run: cargo test --features x509-parser 123 | - name: Run the tests with aws_lc_rs backend enabled 124 | run: cargo test --no-default-features --features aws_lc_rs,pem 125 | # rustls-cert-gen require either aws_lc_rs or ring feature 126 | - name: Run the tests with no features enabled 127 | run: cargo test -p rcgen --no-default-features 128 | 129 | build: 130 | strategy: 131 | matrix: 132 | os: [macOS-latest, ubuntu-latest] 133 | toolchain: [stable, beta, nightly, stable 7 months ago] 134 | exclude: 135 | - os: macOS-latest 136 | toolchain: beta 137 | - os: macOS-latest 138 | toolchain: nightly 139 | - os: macOS-latest 140 | toolchain: stable 7 months ago 141 | runs-on: ${{ matrix.os }} 142 | steps: 143 | - uses: actions/checkout@v4 144 | with: 145 | persist-credentials: false 146 | - uses: actions/cache@v4 147 | with: 148 | path: | 149 | ~/.cargo/bin/ 150 | ~/.cargo/registry/index/ 151 | ~/.cargo/registry/cache/ 152 | ~/.cargo/git/db/ 153 | target/ 154 | key: ${{ runner.os }}-cargo-${{matrix.toolchain}}-${{ hashFiles('Cargo.lock') }} 155 | - name: Install Rust 156 | uses: dtolnay/rust-toolchain@master 157 | with: 158 | toolchain: ${{ matrix.toolchain }} 159 | - name: Run cargo check 160 | run: cargo check --all-targets 161 | - name: Run the tests 162 | run: cargo test 163 | - name: Run the tests with x509-parser enabled 164 | run: cargo test --features x509-parser 165 | - name: Run the tests with aws_lc_rs backend enabled 166 | run: cargo test --no-default-features --features aws_lc_rs,pem 167 | 168 | # Build rustls-cert-gen as a standalone package, see this PR for why it's needed: 169 | # https://github.com/rustls/rcgen/pull/206#pullrequestreview-1816197358 170 | build-rustls-cert-gen-standalone: 171 | name: Build rustls-cert-gen as a standalone package 172 | runs-on: ubuntu-latest 173 | steps: 174 | - name: Checkout sources 175 | uses: actions/checkout@v4 176 | with: 177 | persist-credentials: false 178 | - name: Install rust toolchain 179 | uses: dtolnay/rust-toolchain@stable 180 | - name: Run the tests 181 | run: cargo test --package rustls-cert-gen 182 | 183 | coverage: 184 | name: Measure coverage 185 | runs-on: ubuntu-latest 186 | steps: 187 | - name: Checkout sources 188 | uses: actions/checkout@v4 189 | with: 190 | persist-credentials: false 191 | - uses: actions/cache@v4 192 | with: 193 | path: | 194 | ~/.cargo/bin/ 195 | ~/.cargo/registry/index/ 196 | ~/.cargo/registry/cache/ 197 | ~/.cargo/git/db/ 198 | target/ 199 | key: ${{ runner.os }}-cargo-stable-${{ hashFiles('Cargo.lock') }} 200 | - name: Install cargo-llvm-cov 201 | uses: taiki-e/install-action@cargo-llvm-cov 202 | - name: Install rust toolchain 203 | uses: dtolnay/rust-toolchain@stable 204 | with: 205 | components: llvm-tools 206 | - name: Measure coverage 207 | run: cargo llvm-cov --all-features --lcov --output-path ./lcov.info 208 | - name: Report to codecov.io 209 | uses: codecov/codecov-action@v5 210 | with: 211 | token: ${{ secrets.CODECOV_TOKEN }} 212 | files: ./lcov.info 213 | fail_ci_if_error: false 214 | verbose: true 215 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file only excludes files that are specific to this repository. Please setup your own global 2 | # `.gitignore` file to exclude files that are specific to your operating system and IDE. 3 | # 4 | # From https://git-scm.com/docs/gitignore: 5 | # 6 | # > Patterns which a user wants Git to ignore in all situations (e.g., backup or temporary files 7 | # > generated by the user’s editor of choice) generally go into a file specified by 8 | # > `core.excludesFile` in the user’s `~/.gitconfig`. 9 | # 10 | # Discussed in PR https://github.com/rustls/rcgen/pull/342 11 | 12 | # Rust and Cargo 13 | /target 14 | **/*.rs.bk 15 | 16 | # Project 17 | certs/ 18 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | hard_tabs = true 2 | match_block_trailing_comma = true 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["rcgen", "rustls-cert-gen"] 3 | resolver = "2" 4 | 5 | [workspace.dependencies] 6 | aws-lc-rs = { version = "1.6.0", default-features = false } 7 | pem = "3.0.2" 8 | pki-types = { package = "rustls-pki-types", version = "1.4.1" } 9 | ring = "0.17" 10 | x509-parser = "0.17" 11 | 12 | [workspace.package] 13 | license = "MIT OR Apache-2.0" 14 | edition = "2021" 15 | readme = "README.md" 16 | description = "Rust X.509 certificate generator" 17 | repository = "https://github.com/rustls/rcgen" 18 | keywords = ["mkcert", "ca", "certificate"] 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2025 est31 and contributors 2 | 3 | Licensed under MIT or Apache License 2.0, 4 | at your option. 5 | 6 | The full list of contributors can be obtained by looking 7 | at the VCS log (originally, this crate was git versioned, 8 | there you can do "git shortlog -sn" for this task). 9 | 10 | MIT License 11 | ----------- 12 | 13 | The MIT License (MIT) 14 | 15 | Copyright (c) 2019-2025 est31 and contributors 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | 35 | 36 | 37 | Apache License, version 2.0 38 | --------------------------- 39 | Apache License 40 | Version 2.0, January 2004 41 | http://www.apache.org/licenses/ 42 | 43 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 44 | 45 | 1. Definitions. 46 | 47 | "License" shall mean the terms and conditions for use, reproduction, 48 | and distribution as defined by Sections 1 through 9 of this document. 49 | 50 | "Licensor" shall mean the copyright owner or entity authorized by 51 | the copyright owner that is granting the License. 52 | 53 | "Legal Entity" shall mean the union of the acting entity and all 54 | other entities that control, are controlled by, or are under common 55 | control with that entity. For the purposes of this definition, 56 | "control" means (i) the power, direct or indirect, to cause the 57 | direction or management of such entity, whether by contract or 58 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 59 | outstanding shares, or (iii) beneficial ownership of such entity. 60 | 61 | "You" (or "Your") shall mean an individual or Legal Entity 62 | exercising permissions granted by this License. 63 | 64 | "Source" form shall mean the preferred form for making modifications, 65 | including but not limited to software source code, documentation 66 | source, and configuration files. 67 | 68 | "Object" form shall mean any form resulting from mechanical 69 | transformation or translation of a Source form, including but 70 | not limited to compiled object code, generated documentation, 71 | and conversions to other media types. 72 | 73 | "Work" shall mean the work of authorship, whether in Source or 74 | Object form, made available under the License, as indicated by a 75 | copyright notice that is included in or attached to the work 76 | (an example is provided in the Appendix below). 77 | 78 | "Derivative Works" shall mean any work, whether in Source or Object 79 | form, that is based on (or derived from) the Work and for which the 80 | editorial revisions, annotations, elaborations, or other modifications 81 | represent, as a whole, an original work of authorship. For the purposes 82 | of this License, Derivative Works shall not include works that remain 83 | separable from, or merely link (or bind by name) to the interfaces of, 84 | the Work and Derivative Works thereof. 85 | 86 | "Contribution" shall mean any work of authorship, including 87 | the original version of the Work and any modifications or additions 88 | to that Work or Derivative Works thereof, that is intentionally 89 | submitted to Licensor for inclusion in the Work by the copyright owner 90 | or by an individual or Legal Entity authorized to submit on behalf of 91 | the copyright owner. For the purposes of this definition, "submitted" 92 | means any form of electronic, verbal, or written communication sent 93 | to the Licensor or its representatives, including but not limited to 94 | communication on electronic mailing lists, source code control systems, 95 | and issue tracking systems that are managed by, or on behalf of, the 96 | Licensor for the purpose of discussing and improving the Work, but 97 | excluding communication that is conspicuously marked or otherwise 98 | designated in writing by the copyright owner as "Not a Contribution." 99 | 100 | "Contributor" shall mean Licensor and any individual or Legal Entity 101 | on behalf of whom a Contribution has been received by Licensor and 102 | subsequently incorporated within the Work. 103 | 104 | 2. Grant of Copyright License. Subject to the terms and conditions of 105 | this License, each Contributor hereby grants to You a perpetual, 106 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 107 | copyright license to reproduce, prepare Derivative Works of, 108 | publicly display, publicly perform, sublicense, and distribute the 109 | Work and such Derivative Works in Source or Object form. 110 | 111 | 3. Grant of Patent License. Subject to the terms and conditions of 112 | this License, each Contributor hereby grants to You a perpetual, 113 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 114 | (except as stated in this section) patent license to make, have made, 115 | use, offer to sell, sell, import, and otherwise transfer the Work, 116 | where such license applies only to those patent claims licensable 117 | by such Contributor that are necessarily infringed by their 118 | Contribution(s) alone or by combination of their Contribution(s) 119 | with the Work to which such Contribution(s) was submitted. If You 120 | institute patent litigation against any entity (including a 121 | cross-claim or counterclaim in a lawsuit) alleging that the Work 122 | or a Contribution incorporated within the Work constitutes direct 123 | or contributory patent infringement, then any patent licenses 124 | granted to You under this License for that Work shall terminate 125 | as of the date such litigation is filed. 126 | 127 | 4. Redistribution. You may reproduce and distribute copies of the 128 | Work or Derivative Works thereof in any medium, with or without 129 | modifications, and in Source or Object form, provided that You 130 | meet the following conditions: 131 | 132 | (a) You must give any other recipients of the Work or 133 | Derivative Works a copy of this License; and 134 | 135 | (b) You must cause any modified files to carry prominent notices 136 | stating that You changed the files; and 137 | 138 | (c) You must retain, in the Source form of any Derivative Works 139 | that You distribute, all copyright, patent, trademark, and 140 | attribution notices from the Source form of the Work, 141 | excluding those notices that do not pertain to any part of 142 | the Derivative Works; and 143 | 144 | (d) If the Work includes a "NOTICE" text file as part of its 145 | distribution, then any Derivative Works that You distribute must 146 | include a readable copy of the attribution notices contained 147 | within such NOTICE file, excluding those notices that do not 148 | pertain to any part of the Derivative Works, in at least one 149 | of the following places: within a NOTICE text file distributed 150 | as part of the Derivative Works; within the Source form or 151 | documentation, if provided along with the Derivative Works; or, 152 | within a display generated by the Derivative Works, if and 153 | wherever such third-party notices normally appear. The contents 154 | of the NOTICE file are for informational purposes only and 155 | do not modify the License. You may add Your own attribution 156 | notices within Derivative Works that You distribute, alongside 157 | or as an addendum to the NOTICE text from the Work, provided 158 | that such additional attribution notices cannot be construed 159 | as modifying the License. 160 | 161 | You may add Your own copyright statement to Your modifications and 162 | may provide additional or different license terms and conditions 163 | for use, reproduction, or distribution of Your modifications, or 164 | for any such Derivative Works as a whole, provided Your use, 165 | reproduction, and distribution of the Work otherwise complies with 166 | the conditions stated in this License. 167 | 168 | 5. Submission of Contributions. Unless You explicitly state otherwise, 169 | any Contribution intentionally submitted for inclusion in the Work 170 | by You to the Licensor shall be under the terms and conditions of 171 | this License, without any additional terms or conditions. 172 | Notwithstanding the above, nothing herein shall supersede or modify 173 | the terms of any separate license agreement you may have executed 174 | with Licensor regarding such Contributions. 175 | 176 | 6. Trademarks. This License does not grant permission to use the trade 177 | names, trademarks, service marks, or product names of the Licensor, 178 | except as required for reasonable and customary use in describing the 179 | origin of the Work and reproducing the content of the NOTICE file. 180 | 181 | 7. Disclaimer of Warranty. Unless required by applicable law or 182 | agreed to in writing, Licensor provides the Work (and each 183 | Contributor provides its Contributions) on an "AS IS" BASIS, 184 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 185 | implied, including, without limitation, any warranties or conditions 186 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 187 | PARTICULAR PURPOSE. You are solely responsible for determining the 188 | appropriateness of using or redistributing the Work and assume any 189 | risks associated with Your exercise of permissions under this License. 190 | 191 | 8. Limitation of Liability. In no event and under no legal theory, 192 | whether in tort (including negligence), contract, or otherwise, 193 | unless required by applicable law (such as deliberate and grossly 194 | negligent acts) or agreed to in writing, shall any Contributor be 195 | liable to You for damages, including any direct, indirect, special, 196 | incidental, or consequential damages of any character arising as a 197 | result of this License or out of the use or inability to use the 198 | Work (including but not limited to damages for loss of goodwill, 199 | work stoppage, computer failure or malfunction, or any and all 200 | other commercial damages or losses), even if such Contributor 201 | has been advised of the possibility of such damages. 202 | 203 | 9. Accepting Warranty or Additional Liability. While redistributing 204 | the Work or Derivative Works thereof, You may choose to offer, 205 | and charge a fee for, acceptance of support, warranty, indemnity, 206 | or other liability obligations and/or rights consistent with this 207 | License. However, in accepting such obligations, You may act only 208 | on Your own behalf and on Your sole responsibility, not on behalf 209 | of any other Contributor, and only if You agree to indemnify, 210 | defend, and hold each Contributor harmless for any liability 211 | incurred by, or claims asserted against, such Contributor by reason 212 | of your accepting any such warranty or additional liability. 213 | 214 | END OF TERMS AND CONDITIONS 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rcgen 2 | 3 | [![docs](https://docs.rs/rcgen/badge.svg)](https://docs.rs/rcgen) 4 | [![crates.io](https://img.shields.io/crates/v/rcgen.svg)](https://crates.io/crates/rcgen) 5 | [![dependency status](https://deps.rs/repo/github/est31/rcgen/status.svg)](https://deps.rs/repo/github/est31/rcgen) 6 | 7 | Simple Rust library to generate X.509 certificates. 8 | 9 | ```Rust 10 | use rcgen::{generate_simple_self_signed, CertifiedKey}; 11 | // Generate a certificate that's valid for "localhost" and "hello.world.example" 12 | let subject_alt_names = vec!["hello.world.example".to_string(), 13 | "localhost".to_string()]; 14 | 15 | let CertifiedKey { cert, key_pair } = generate_simple_self_signed(subject_alt_names).unwrap(); 16 | println!("{}", cert.pem()); 17 | println!("{}", key_pair.serialize_pem()); 18 | ``` 19 | 20 | ## Trying it out with openssl 21 | 22 | You can do this: 23 | 24 | ``` 25 | cargo run 26 | openssl x509 -in certs/cert.pem -text -noout 27 | ``` 28 | 29 | For debugging, pasting the PEM formatted text 30 | to [this](https://lapo.it/asn1js/) service is very useful. 31 | 32 | ## Trying it out with quinn 33 | 34 | You can use rcgen together with the [quinn](https://github.com/quinn-rs/quinn) crate. 35 | The whole set of commands is: 36 | ``` 37 | cargo run 38 | cd ../quinn 39 | cargo run --example server -- --cert ../rcgen/certs/cert.pem --key ../rcgen/certs/key.pem ./ 40 | cargo run --example client -- --ca ../rcgen/certs/cert.der https://localhost:4433/README.md 41 | 42 | ``` 43 | 44 | ## MSRV 45 | 46 | The MSRV policy is to strive for supporting 7-month old Rust versions. 47 | 48 | ### License 49 | [license]: #license 50 | 51 | This crate is distributed under the terms of both the MIT license 52 | and the Apache License (Version 2.0), at your option. 53 | 54 | See [LICENSE](LICENSE) for details. 55 | 56 | #### License of your contributions 57 | 58 | Unless you explicitly state otherwise, any contribution intentionally submitted for 59 | inclusion in the work by you, as defined in the Apache-2.0 license, 60 | shall be dual licensed as above, without any additional terms or conditions. 61 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | github_checks: 3 | annotations: false 4 | -------------------------------------------------------------------------------- /rcgen/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changes 3 | 4 | Newer releases can be found [on GitHub](https://github.com/rustls/rcgen/releases). 5 | 6 | ## Release 0.13.1 - April 4th, 2024 7 | 8 | Fixed: 9 | 10 | - Fixed incorrect usage of the subject certificate's parameter's key identifier 11 | method when computing the key identifier of the issuer for the subject's 12 | authority key identifier (AKI) extension. 13 | 14 | ## Release 0.13.0 - March 28th, 2024 15 | 16 | Breaking changes: 17 | 18 | - The API used to create/issue key pairs, certificates, certificate signing 19 | requests (CSRs), and certificate revocation lists (CRLs) has been 20 | restructured to emphasize consistency and avoid common errors with 21 | serialization. 22 | 23 | For each concrete type (cert, CSR, CRL) the process is now the same: 24 | 25 | 0. generate or load a key pair and any information about issuers required. 26 | 1. create parameters, customizing as appropriate. 27 | 2. call a generation `fn` on the parameters, providing subject key pair and 28 | issuer information and as appropriate. 29 | 3. call serialization `fn`s on the finalized type, obtaining DER or PEM. 30 | 31 | For more information, see [rcgen/docs/0.12-to-0.13.md]. 32 | 33 | - Throughout the API DER inputs are now represented using types from the Rustls 34 | `rustls-pki-types` crate, e.g. `PrivateKeyDer`, `CertificateDer`, 35 | `CertificateSigningRequestDer`. Contributed by 36 | [Tudyx](https://github.com/tudyx). 37 | 38 | - String types used in `SanType` and `DnValue` enums for non-UTF8 string types 39 | have been replaced with more specific types that prevent representation of 40 | illegal values. E.g. `Ia5String`, `BmpString`, `PrintableString`, 41 | `TeletexString`, and `UniversalString`. Contributed by 42 | [Tudyx](https://github.com/tudyx). 43 | 44 | - Method names starting with `get_` have been renamed to match Rust convention: 45 | `CertificateRevocationList::get_params()` -> `params()` 46 | `Certificate::get_params()` -> `params()` 47 | `Certificate::get_key_identifier()` -> `Certificate::key_identifier()` 48 | `Certificate::get_times()` -> `Certificate::times()` 49 | 50 | Added: 51 | 52 | - RSA key generation support has been added. This support requires using the 53 | `aws-lc-rs` feature. By default using `KeyPair::generate_for()` with 54 | an RSA `SignatureAlgorithm` will generate an RSA 2048 keypair. See 55 | `KeyPair::generate_rsa_for()` for support for RSA 2048, 3072 and 4096 key sizes. 56 | 57 | - Support for ECDSA P521 signatures and key generation has been added when using 58 | the `aws-lc-rs` feature. Contributed by [Alvenix](https://github.com/alvenix). 59 | 60 | - Support for loading private keys that may be PKCS8, PKCS1, or SEC1 has been 61 | added when using the `aws-lc-rs` feature. Without this feature private keys 62 | must be PKCS8. See `KeyPair::from_pem_and_sign_algo()` and 63 | `KeyPair::from_der_and_sign_algo()` for more information. Contributed by 64 | [Alvenix](https://github.com/alvenix). 65 | 66 | - Support has been added for Subject Alternative Name (SAN) names of type 67 | `OtherName`. Contributed by [Tudyx](https://github.com/tudyx). 68 | 69 | - Support has been added for specifying custom "other" OIDs in extended key 70 | usage. Contributed by [Tudyx](https://github.com/tudyx). 71 | 72 | - Support has been added for building rcgen _without_ cryptography by omitting 73 | the new (default-enabled) `crypto` feature flag. Contributed by 74 | [corrideat](https://github.com/corrideat). 75 | 76 | - Support for using `aws-lc-rs` in `fips` mode can now be activated by using the 77 | `fips` feature in combination with the `aws-lc-rs` feature. Contributed by 78 | [BiagioFesta](https://github.com/biagiofesta). 79 | 80 | - A small command-line tool for certificate generation (`rustls-cert-gen`) was 81 | added. Contributed by [tbro](https://github.com/tbro). 82 | 83 | ## Release 0.12.1 - January 25th, 2024 84 | 85 | - RFC 5280 specifies that a serial number must not be larger than 20 octets in 86 | length. Prior to this release an unintended interaction between rcgen and its 87 | underlying DER encoding library could result in 21 octet serials. This has now 88 | been fixed. 89 | - A regression that caused build errors when the optional `pem` feature was 90 | omitted has been fixed. 91 | 92 | ## Release 0.12.0 - December 16, 2023 93 | 94 | - Rename `RcgenError` to `Error`. Contributed by [thomaseizinger](https://github.com/thomaseizinger). 95 | - The public interface of `Error` has been made not expose external library types: `Error::PemError` now holds a `String` value, and the `Error` type doesn't support `From<_>` based conversion any more. This allows rcgen to update dependencies without impacting downstream users. 96 | - Upgrade to `ring` `v0.17`. Contributed by [thomaseizinger](https://github.com/thomaseizinger). 97 | - Make dependency on `ring` optional and allow usage of `aws-lc-rs` via a cargo feature. Ring remains the default. Contributed by [BiagioFesta](https://github.com/BiagioFesta). 98 | - Add `Ia5String` support for `DistinguishedName`s. 99 | - Add a `KeyIdMethod::PreSpecified` variant to set, and not generate the SKI. `CertificateParams::from_ca_cert_pem` now uses it when building params from an existing CA certificate. Contributed by [Brocar](https://github.com/Brocar). 100 | 101 | ## Release 0.11.3 - October 1, 2023 102 | 103 | - Fix for import errors building without the optional `pem` feature. 104 | 105 | ## Release 0.11.2 - September 21, 2023 106 | 107 | - `rcgen` has joined the umbrella of the [rustls](https://github.com/rustls) organization. 108 | - Support for retrieving signature algorithm from `KeyPair`s. Contributed by [tindzk](https://github.com/tindzk). 109 | - Fix for writing certificate signing requests (CSRs) with custom extensions from parameters without subject alternative names. 110 | - Support for certificate CRL distribution points extension. 111 | - Corrected OID for `ExtendedKeyUsagePurpose::Any`. Contributed by [jgallagher](https://github.com/jgallagher). 112 | - Support for creating certificate revocation lists (CRLs). 113 | 114 | ## Release 0.11.1 - June 17, 2023 115 | 116 | - Make botan a dev-dependency again. Contributed by [mbrubeck](https://github.com/mbrubeck). 117 | 118 | ## Release 0.11.0 - June 15, 2023 119 | 120 | - Parse IP-address subject alternative names. Contributed by [iamjpotts](https://github.com/iamjpotts). 121 | - Emit platform-apropriate line endings. Contributed by [frjonsen](https://github.com/frjonsen). 122 | - Support larger serial numbers. Contributed by [andrenth](https://github.com/andrenth). 123 | - Parse more certificate parameters. Contributed by [andrenth](https://github.com/andrenth). 124 | - Output `SanType::IpAddress` when calling `CertificateParams::new` or `generate_simple_self_signed`. Contributed by [rukai](https://github.com/rukai). 125 | - Update pem to 2.0. Contributed by [koushiro](https://github.com/koushiro). 126 | 127 | ## Release 0.10.0 - September 29, 2022 128 | 129 | - Update x509-parser to 0.14. 130 | - Increase minimum supported Rust version to 1.58.1. 131 | - Update edition to 2021. 132 | - Change `IsCa` enum to have `NoCa` and `ExplicitNoCa` and `Ca(...)`. Contributed by [doraneko94](https://github.com/doraneko94). 133 | 134 | ## Release 0.9.4 - September 28, 2022 135 | 136 | * yanked due to breaking API changes, see 0.10.0 instead. 137 | 138 | ## Release 0.9.3 - July 16, 2022 139 | 140 | - Add a `KeyPair::serialized_der` function. Contributed by [jean-airoldie](https://github.com/jean-airoldie). 141 | 142 | ## Release 0.9.2 - February 21, 2022 143 | 144 | - Update x509-parser to 0.13. Contributed by [matze](https://github.com/matze). 145 | 146 | ## Release 0.9.1 - February 9, 2022 147 | 148 | - Change edition to 2018 in order to support Rust 1.53.0. 149 | 150 | ## Release 0.9.0 - February 2, 2022 151 | 152 | - Add RemoteKeyError for usage by remote keys. 153 | - Support non utf8 strings. Contributed by [omjadas](https://github.com/omjadas). 154 | - Switch from chrono to time. Contributed by [connec](https://github.com/connec). 155 | - Update edition to 2021. 156 | 157 | ## Release 0.8.14 - October 14, 2021 158 | 159 | - Update pem to 1.0. 160 | - Update x509-parser to 0.12. 161 | 162 | ## Release 0.8.13 - August 22, 2021 163 | 164 | - Bugfix release to make Certificate `Send` and `Sync` again. 165 | 166 | ## Release 0.8.12 - August 22, 2021 167 | 168 | - Use public key as default serial number. Contributed by [jpastuszek](https://github.com/jpastuszek). 169 | - Add support for `PKCS_RSA_SHA512` and `PKCS_RSA_SHA384` signature algorithms. 170 | - Add support for the keyUsage extension. Contributed by [jaredwolff](https://github.com/jaredwolff). 171 | - Ability to use remote keys. Contributed by [daxpedda](https://github.com/daxpedda). 172 | 173 | ## Release 0.8.11 - April 28, 2021 174 | 175 | - Add getters for the criticality, content, and `oid_components` of a `CustomExtension` 176 | - Update yasna to 0.4 177 | 178 | ## Release 0.8.10 - April 15, 2021 179 | 180 | - Implement some additional traits for some of the types. Contributed by [zurborg](https://github.com/zurborg). 181 | - Adoption of intra-doc-links 182 | - Addition of the ability to zero key pairs. Contributed by [didier-wenzek](https://github.com/didier-wenzek). 183 | 184 | ## Release 0.8.9 - December 4, 2020 185 | 186 | - Switch CI to Github Actions. 187 | - Strip nanos from `DateTime` as well. Contributed by [@trevor-crypto](https://github.com/trevor-crypto). 188 | 189 | ## Release 0.8.7 - December 1, 2020 190 | 191 | - Turn `botan` back into a dev-dependency. Contributed by [@nthuemmel](https://github.com/nthuemmel). 192 | - Fix signing when CA uses different signing algorithm . Contributed by [@nthuemmel](https://github.com/nthuemmel). 193 | 194 | ## Release 0.8.6 - December 1, 2020 195 | 196 | - Add `KeyPair::from_der` 197 | - Add botan based test to the testsuite 198 | - Update x509-parser to 0.9. Contributed by [@djc](https://github.com/djc). 199 | - Ability to create certificates from CSRs. Contributed by [@djc](https://github.com/djc). 200 | 201 | ## Release 0.8.5 - June 29, 2020 202 | 203 | - Add some more `DnType`s: `OrganizationalUnitName`, `LocalityName`, `StateOrProvinceName` 204 | - Add `remove` function to `DistinguishedName` 205 | - Add ability to specify `NameConstraints` 206 | 207 | ## Release 0.8.4 - June 5, 2020 208 | 209 | - Improve spec compliance in the `notBefore`/`notAfter` fields generated by using `UTCTime` if needed 210 | 211 | ## Release 0.8.3 - May 24, 2020 212 | 213 | - Fix regression of `0.8.1` that generated standards non compliant CSRs 214 | and broke Go toolchain parsers. Contributed by [@thomastaylor312](https://github.com/thomastaylor312). 215 | 216 | ## Release 0.8.2 - May 18, 2020 217 | 218 | - Disable `chrono` default features to get rid of time crate 219 | - Improve `openssl` tests to do a full handshake with the generated cert 220 | 221 | ## Release 0.8.1 - April 2, 2020 222 | 223 | - Fix non-standard-compliant SubjectKeyIdentifier X.509v3 extension format 224 | - BasicConstraints X.509v3 extension is now marked as critical 225 | - Use RFC 7093 to calculate calculate subject key identifiers 226 | - Add option to insert AuthorityKeyIdentifier X.509v3 extension into non-self-signed certificates 227 | - Update to x509-parser 0.7 228 | 229 | ## Release 0.8.0 - March 12, 2020 230 | 231 | - Update to pem 0.7 232 | - Correct number of nanoseconds per second. Contributed by [@samlich](https://github.com/samlich). 233 | - Adoption of the `non_exhaustive` feature in the API 234 | 235 | ## Release 0.7.0 - September 14, 2019 236 | 237 | - Bugfix release for ip address subject alternative names. 238 | Turns out they aren't CIDR subnets after all :) 239 | 240 | ## Release 0.6.0 - September 12, 2019 241 | 242 | - Support for email and cidr subnet (ip address) subject alternative names 243 | - Support for the extended key usage extension 244 | 245 | ## Release 0.5.1 - August 19, 2019 246 | 247 | - Update to x509-parser 0.6 248 | 249 | ## Release 0.5.0 - July 19, 2019 250 | 251 | - Update to ring 0.16 and webpki 0.21 252 | - Update to x509-parser 0.5 253 | - Expose an API to get the raw public key of a key pair 254 | 255 | ## Release 0.4.1 - June 28, 2019 256 | 257 | - Allow inspection of `DistinguishedName` via iterators and get functions 258 | - Fix a bug in `is_compatible` not saying false. Contributed by [@fzgregor](https://github.com/fzgregor). 259 | - Extend the public interface of `KeyPair`. Contributed by [@fzgregor](https://github.com/fzgregor). 260 | 261 | ## Release 0.4.0 - June 18, 2019 262 | 263 | - Support for user supplied keypairs. Contributed by [@fzgregor](https://github.com/fzgregor). 264 | - Support for signing with user supplied CA certificates. Contributed by [@fzgregor](https://github.com/fzgregor). 265 | - Correct a bug with distinguished name serialization ([PR link](https://github.com/est31/rcgen/pull/13)). Contributed by [@fzgregor](https://github.com/fzgregor). 266 | - Addition of limited (no key generation) RSA support 267 | - Proper error handling with `Result` and our own Error type 268 | - Improvements of the testsuite 269 | 270 | ## Release 0.3.1 - June 6, 2019 271 | 272 | - Ability to disable the dependency on the `pem` crate 273 | - Support for creating CSRs (Certificate Signing Requests). Contributed by [@djc](https://github.com/djc). 274 | - Ability to specify custom extensions for certificates 275 | - Ability to craft `acmeIdentifier` extensions 276 | - Update yasna to 0.3.0 277 | 278 | ## Release 0.3.0 - May 18, 2019 279 | 280 | - Support for CA certificate generation. Contributed by [@djc](https://github.com/djc). 281 | - Support for certificate signing. Contributed by [@djc](https://github.com/djc). 282 | - Support for ED25519 certificates 283 | - Support for SHA-384 certificates 284 | - API cleanups (Future proofing CertificateParams, public constant renames) 285 | 286 | ## Release 0.2.1 - April 26, 2019 287 | 288 | - Updated to pem 0.6 289 | 290 | ## Release 0.2 - January 10, 2019 291 | 292 | - Updated to ring 0.14.0 293 | 294 | ## Release 0.1 - January 7, 2019 295 | 296 | Initial release. Ability to generate self-signed ECDSA keys. 297 | -------------------------------------------------------------------------------- /rcgen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rcgen" 3 | version = "0.14.0" 4 | documentation = "https://docs.rs/rcgen" 5 | description.workspace = true 6 | repository.workspace = true 7 | readme.workspace = true 8 | license.workspace = true 9 | edition.workspace = true 10 | keywords.workspace = true 11 | 12 | [[example]] 13 | name = "rsa-irc-openssl" 14 | required-features = ["pem"] 15 | 16 | [[example]] 17 | name = "sign-leaf-with-ca" 18 | required-features = ["pem", "x509-parser"] 19 | 20 | [[example]] 21 | name = "simple" 22 | required-features = ["crypto", "pem"] 23 | 24 | [dependencies] 25 | aws-lc-rs = { workspace = true, optional = true } 26 | yasna = { version = "0.5.2", features = ["time", "std"] } 27 | ring = { workspace = true, optional = true } 28 | pem = { workspace = true, optional = true } 29 | pki-types = { workspace = true } 30 | time = { version = "0.3.6", default-features = false } 31 | x509-parser = { workspace = true, features = ["verify"], optional = true } 32 | zeroize = { version = "1.2", optional = true } 33 | 34 | [features] 35 | default = ["crypto", "pem", "ring"] 36 | crypto = [] 37 | aws_lc_rs = ["crypto", "dep:aws-lc-rs", "aws-lc-rs/aws-lc-sys"] 38 | ring = ["crypto", "dep:ring"] 39 | fips = ["crypto", "dep:aws-lc-rs", "aws-lc-rs/fips"] 40 | 41 | [package.metadata.docs.rs] 42 | features = ["x509-parser"] 43 | 44 | [package.metadata.cargo_check_external_types] 45 | allowed_external_types = [ 46 | "time::offset_date_time::OffsetDateTime", 47 | "zeroize::Zeroize", 48 | "rustls_pki_types::*", 49 | ] 50 | 51 | [dev-dependencies] 52 | pki-types = { package = "rustls-pki-types", version = "1" } 53 | x509-parser = { workspace = true, features = ["verify"] } 54 | rustls-webpki = { version = "0.103", features = ["ring", "std"] } 55 | botan = { version = "0.11", features = ["vendored"] } 56 | ring = { workspace = true } 57 | 58 | [target."cfg(unix)".dev-dependencies] 59 | openssl = "0.10" 60 | -------------------------------------------------------------------------------- /rcgen/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2025 est31 and contributors 2 | 3 | Licensed under MIT or Apache License 2.0, 4 | at your option. 5 | 6 | The full list of contributors can be obtained by looking 7 | at the VCS log (originally, this crate was git versioned, 8 | there you can do "git shortlog -sn" for this task). 9 | 10 | MIT License 11 | ----------- 12 | 13 | The MIT License (MIT) 14 | 15 | Copyright (c) 2019-2025 est31 and contributors 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | 35 | 36 | 37 | Apache License, version 2.0 38 | --------------------------- 39 | Apache License 40 | Version 2.0, January 2004 41 | http://www.apache.org/licenses/ 42 | 43 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 44 | 45 | 1. Definitions. 46 | 47 | "License" shall mean the terms and conditions for use, reproduction, 48 | and distribution as defined by Sections 1 through 9 of this document. 49 | 50 | "Licensor" shall mean the copyright owner or entity authorized by 51 | the copyright owner that is granting the License. 52 | 53 | "Legal Entity" shall mean the union of the acting entity and all 54 | other entities that control, are controlled by, or are under common 55 | control with that entity. For the purposes of this definition, 56 | "control" means (i) the power, direct or indirect, to cause the 57 | direction or management of such entity, whether by contract or 58 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 59 | outstanding shares, or (iii) beneficial ownership of such entity. 60 | 61 | "You" (or "Your") shall mean an individual or Legal Entity 62 | exercising permissions granted by this License. 63 | 64 | "Source" form shall mean the preferred form for making modifications, 65 | including but not limited to software source code, documentation 66 | source, and configuration files. 67 | 68 | "Object" form shall mean any form resulting from mechanical 69 | transformation or translation of a Source form, including but 70 | not limited to compiled object code, generated documentation, 71 | and conversions to other media types. 72 | 73 | "Work" shall mean the work of authorship, whether in Source or 74 | Object form, made available under the License, as indicated by a 75 | copyright notice that is included in or attached to the work 76 | (an example is provided in the Appendix below). 77 | 78 | "Derivative Works" shall mean any work, whether in Source or Object 79 | form, that is based on (or derived from) the Work and for which the 80 | editorial revisions, annotations, elaborations, or other modifications 81 | represent, as a whole, an original work of authorship. For the purposes 82 | of this License, Derivative Works shall not include works that remain 83 | separable from, or merely link (or bind by name) to the interfaces of, 84 | the Work and Derivative Works thereof. 85 | 86 | "Contribution" shall mean any work of authorship, including 87 | the original version of the Work and any modifications or additions 88 | to that Work or Derivative Works thereof, that is intentionally 89 | submitted to Licensor for inclusion in the Work by the copyright owner 90 | or by an individual or Legal Entity authorized to submit on behalf of 91 | the copyright owner. For the purposes of this definition, "submitted" 92 | means any form of electronic, verbal, or written communication sent 93 | to the Licensor or its representatives, including but not limited to 94 | communication on electronic mailing lists, source code control systems, 95 | and issue tracking systems that are managed by, or on behalf of, the 96 | Licensor for the purpose of discussing and improving the Work, but 97 | excluding communication that is conspicuously marked or otherwise 98 | designated in writing by the copyright owner as "Not a Contribution." 99 | 100 | "Contributor" shall mean Licensor and any individual or Legal Entity 101 | on behalf of whom a Contribution has been received by Licensor and 102 | subsequently incorporated within the Work. 103 | 104 | 2. Grant of Copyright License. Subject to the terms and conditions of 105 | this License, each Contributor hereby grants to You a perpetual, 106 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 107 | copyright license to reproduce, prepare Derivative Works of, 108 | publicly display, publicly perform, sublicense, and distribute the 109 | Work and such Derivative Works in Source or Object form. 110 | 111 | 3. Grant of Patent License. Subject to the terms and conditions of 112 | this License, each Contributor hereby grants to You a perpetual, 113 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 114 | (except as stated in this section) patent license to make, have made, 115 | use, offer to sell, sell, import, and otherwise transfer the Work, 116 | where such license applies only to those patent claims licensable 117 | by such Contributor that are necessarily infringed by their 118 | Contribution(s) alone or by combination of their Contribution(s) 119 | with the Work to which such Contribution(s) was submitted. If You 120 | institute patent litigation against any entity (including a 121 | cross-claim or counterclaim in a lawsuit) alleging that the Work 122 | or a Contribution incorporated within the Work constitutes direct 123 | or contributory patent infringement, then any patent licenses 124 | granted to You under this License for that Work shall terminate 125 | as of the date such litigation is filed. 126 | 127 | 4. Redistribution. You may reproduce and distribute copies of the 128 | Work or Derivative Works thereof in any medium, with or without 129 | modifications, and in Source or Object form, provided that You 130 | meet the following conditions: 131 | 132 | (a) You must give any other recipients of the Work or 133 | Derivative Works a copy of this License; and 134 | 135 | (b) You must cause any modified files to carry prominent notices 136 | stating that You changed the files; and 137 | 138 | (c) You must retain, in the Source form of any Derivative Works 139 | that You distribute, all copyright, patent, trademark, and 140 | attribution notices from the Source form of the Work, 141 | excluding those notices that do not pertain to any part of 142 | the Derivative Works; and 143 | 144 | (d) If the Work includes a "NOTICE" text file as part of its 145 | distribution, then any Derivative Works that You distribute must 146 | include a readable copy of the attribution notices contained 147 | within such NOTICE file, excluding those notices that do not 148 | pertain to any part of the Derivative Works, in at least one 149 | of the following places: within a NOTICE text file distributed 150 | as part of the Derivative Works; within the Source form or 151 | documentation, if provided along with the Derivative Works; or, 152 | within a display generated by the Derivative Works, if and 153 | wherever such third-party notices normally appear. The contents 154 | of the NOTICE file are for informational purposes only and 155 | do not modify the License. You may add Your own attribution 156 | notices within Derivative Works that You distribute, alongside 157 | or as an addendum to the NOTICE text from the Work, provided 158 | that such additional attribution notices cannot be construed 159 | as modifying the License. 160 | 161 | You may add Your own copyright statement to Your modifications and 162 | may provide additional or different license terms and conditions 163 | for use, reproduction, or distribution of Your modifications, or 164 | for any such Derivative Works as a whole, provided Your use, 165 | reproduction, and distribution of the Work otherwise complies with 166 | the conditions stated in this License. 167 | 168 | 5. Submission of Contributions. Unless You explicitly state otherwise, 169 | any Contribution intentionally submitted for inclusion in the Work 170 | by You to the Licensor shall be under the terms and conditions of 171 | this License, without any additional terms or conditions. 172 | Notwithstanding the above, nothing herein shall supersede or modify 173 | the terms of any separate license agreement you may have executed 174 | with Licensor regarding such Contributions. 175 | 176 | 6. Trademarks. This License does not grant permission to use the trade 177 | names, trademarks, service marks, or product names of the Licensor, 178 | except as required for reasonable and customary use in describing the 179 | origin of the Work and reproducing the content of the NOTICE file. 180 | 181 | 7. Disclaimer of Warranty. Unless required by applicable law or 182 | agreed to in writing, Licensor provides the Work (and each 183 | Contributor provides its Contributions) on an "AS IS" BASIS, 184 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 185 | implied, including, without limitation, any warranties or conditions 186 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 187 | PARTICULAR PURPOSE. You are solely responsible for determining the 188 | appropriateness of using or redistributing the Work and assume any 189 | risks associated with Your exercise of permissions under this License. 190 | 191 | 8. Limitation of Liability. In no event and under no legal theory, 192 | whether in tort (including negligence), contract, or otherwise, 193 | unless required by applicable law (such as deliberate and grossly 194 | negligent acts) or agreed to in writing, shall any Contributor be 195 | liable to You for damages, including any direct, indirect, special, 196 | incidental, or consequential damages of any character arising as a 197 | result of this License or out of the use or inability to use the 198 | Work (including but not limited to damages for loss of goodwill, 199 | work stoppage, computer failure or malfunction, or any and all 200 | other commercial damages or losses), even if such Contributor 201 | has been advised of the possibility of such damages. 202 | 203 | 9. Accepting Warranty or Additional Liability. While redistributing 204 | the Work or Derivative Works thereof, You may choose to offer, 205 | and charge a fee for, acceptance of support, warranty, indemnity, 206 | or other liability obligations and/or rights consistent with this 207 | License. However, in accepting such obligations, You may act only 208 | on Your own behalf and on Your sole responsibility, not on behalf 209 | of any other Contributor, and only if You agree to indemnify, 210 | defend, and hold each Contributor harmless for any liability 211 | incurred by, or claims asserted against, such Contributor by reason 212 | of your accepting any such warranty or additional liability. 213 | 214 | END OF TERMS AND CONDITIONS 215 | -------------------------------------------------------------------------------- /rcgen/docs/0.12-to-0.13.md: -------------------------------------------------------------------------------- 1 | # Rcgen 0.12 to 0.13 Migration Guide 2 | 3 | This document is a meant to be a helpful guide for some of the API changes made 4 | between rcgen 0.12 and 0.13. For information on other changes in 0.13 see 5 | [rcgen/CHANGELOG.md]. 6 | 7 | ## Key Pairs 8 | 9 | * Previously it was possible to have certificate generation automatically create 10 | a subject `KeyPair` for you by leaving the `key_pair` field of 11 | `CertificateParams` empty, and retrieving the generated `KeyPair` from 12 | a `Certificate` created with the `CertificateParams` by calling 13 | `Certificate::get_key_pair()`. 14 | 15 | To offer more consistency and to keep the `CertificateParams` and `Certificate` 16 | types from holding private key data, the new API requires you handle `KeyPair` 17 | creation yourself. See `CertifiedKey`, `KeyPair::generate()`, 18 | `KeyPair::generate_for()` and `KeyPair::generate_rsa_for()` for more information. 19 | 20 | * Serializing a `Certificate`'s `KeyPair` to DER or PEM was previously done by 21 | calling `Certificate::serialize_private_key_der()` or 22 | `Certificate::serialize_private_key_pem()`. This is now handled by calling 23 | `KeyPair::serialize_der()` or `KeyPair::serialize_pem()`. 24 | 25 | ## Certificates 26 | 27 | * For quick-and-easy self-signed certificate issuance, 28 | `generate_simple_self_signed` now returns a `CertifiedKey` in the success case 29 | instead of a `Certificate`. The self-signed `Certificate` can be accessed in 30 | the `cert` field of `CertifiedKey`, and the generated subject key pair in 31 | `key_pair`. 32 | 33 | * Custom self-signed certificate issuance was previously done by 34 | constructing `CertificateParams` and calling `Certificate::from_params()` to 35 | create a `Certificate`. This is now done by calling 36 | `CertificateParams::self_signed()`, providing a subject `KeyPair` of your 37 | choosing. 38 | 39 | * Custom certificate issuance signed by an issuer was previously done by 40 | constructing `CertificateParams`, calling `Certificate::from_params()` and 41 | then choosing the issuer at serialization time. This is now done ahead of 42 | serialization by calling `CertificateParams::signed_by()` and providing 43 | a subject `KeyPair` as well as an issuer `Certificate` and `KeyPair`. 44 | 45 | * Previously certificate serialization was done by calling 46 | `Certificate::serialize_der()`, `Certificate::serialize_pem()`, 47 | `Certificate::serialize_der_with_signer()` or 48 | `Certificate::serialize_pem_with_signer()`. Each time a serialization fn was 49 | called a new certificate was issued, leading to confusion when it was desired 50 | to serialize the same certificate in two formats. In the new API issuance is 51 | handled by `CertificateParams` fns and the generated `Certificate` will not change 52 | when serialized. You can serialize it to PEM by calling `Certificate::pem()`, 53 | or access the DER encoding by calling `Certificate::der()`. 54 | 55 | ## Certificate Signing Requests (CSRs) 56 | 57 | * Previously it was only possible to create a new CSR by first issuing 58 | a `Certificate` from `CertificateParams`, and calling 59 | `Certificate::serialize_request_pem()` or 60 | `Certificate::serialize_request_der()`. In the updated API you can create 61 | a `CertificateSigningRequest` directly from `CertificateParams` by calling 62 | `CertificateParams::serialize_request` and providing a subject `KeyPair`. You 63 | may serialize the CSR to DER or PEM by calling 64 | `CertificateSigningRequest::der()` or `CertificateSingingRequest::pem()`. 65 | 66 | * To load a CSR from an existing PEM/DER copy with the old API required 67 | calling `CertificateSingingRequest::from_pem()` or 68 | `CertificateSigningRequest::from_der()`. The new API introduces 69 | a `CertificateSingingRequestParams` type that can be created using 70 | `CertificateSigningRequestParams::from_pem()` or 71 | `CertificateSingingRequest::from_der()`. 72 | 73 | * To issue a certificate from an existing CSR with the old API required calling 74 | `CertificateSigningRequest::serialize_der_with_signer()` or 75 | `CertificateSigningRequest::serialize_pem_with_signer()`. In the new API, call 76 | `CertificateSigningRequestParams::signed_by()` and provide an issuer 77 | `Certificate` and `KeyPair`. 78 | 79 | ## Certificate Revocation Lists (CRLs) 80 | 81 | * Previously a `CertificateRevocationList` was created by calling 82 | `CertificateRevocationList::from_params()`. This is now done by calling 83 | `CertificateRevocationListParams::signed_by()` and providing an issuer 84 | `Certificate` and `KeyPair`. 85 | 86 | * Previously a created `CertificateRevocationList` could be serialized to DER or 87 | PEM by calling `CertificateRevocationList::serialize_der_with_signer()` or 88 | `CertificateRevocationList::serialize_pem_with_signer()`. This is now done by 89 | calling `CertificateRevocationList::der()` or 90 | `CertificateRevocationList::pem()`. 91 | -------------------------------------------------------------------------------- /rcgen/examples/rsa-irc-openssl.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | fn main() -> Result<(), Box> { 3 | use rcgen::{date_time_ymd, CertificateParams, DistinguishedName}; 4 | use std::fmt::Write; 5 | use std::fs; 6 | 7 | let mut params: CertificateParams = Default::default(); 8 | params.not_before = date_time_ymd(2021, 5, 19); 9 | params.not_after = date_time_ymd(4096, 1, 1); 10 | params.distinguished_name = DistinguishedName::new(); 11 | 12 | let pkey: openssl::pkey::PKey<_> = openssl::rsa::Rsa::generate(2048)?.try_into()?; 13 | let key_pair_pem = String::from_utf8(pkey.private_key_to_pem_pkcs8()?)?; 14 | let key_pair = rcgen::KeyPair::from_pem(&key_pair_pem)?; 15 | 16 | let cert = params.self_signed(&key_pair)?; 17 | let pem_serialized = cert.pem(); 18 | let pem = pem::parse(&pem_serialized)?; 19 | let der_serialized = pem.contents(); 20 | let hash = ring::digest::digest(&ring::digest::SHA512, der_serialized); 21 | let hash_hex = hash.as_ref().iter().fold(String::new(), |mut output, b| { 22 | let _ = write!(output, "{b:02x}"); 23 | output 24 | }); 25 | println!("sha-512 fingerprint: {hash_hex}"); 26 | println!("{pem_serialized}"); 27 | println!("{}", key_pair.serialize_pem()); 28 | std::fs::create_dir_all("certs/")?; 29 | fs::write("certs/cert.pem", pem_serialized.as_bytes())?; 30 | fs::write("certs/cert.der", der_serialized)?; 31 | fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?; 32 | fs::write("certs/key.der", key_pair.serialize_der())?; 33 | Ok(()) 34 | } 35 | 36 | #[cfg(not(unix))] 37 | fn main() -> Result<(), Box> { 38 | // Due to the support burden of running OpenSSL on Windows, 39 | // we only support the OpenSSL backend on Unix-like systems. 40 | // It should still work on Windows if you have OpenSSL installed. 41 | unimplemented!("OpenSSL backend is not supported on Windows"); 42 | } 43 | -------------------------------------------------------------------------------- /rcgen/examples/sign-leaf-with-ca.rs: -------------------------------------------------------------------------------- 1 | use rcgen::{ 2 | BasicConstraints, Certificate, CertificateParams, DnType, DnValue::PrintableString, 3 | ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, 4 | }; 5 | use time::{Duration, OffsetDateTime}; 6 | 7 | /// Example demonstrating signing end-entity certificate with ca 8 | fn main() { 9 | let (ca_params, ca, ca_key) = new_ca(); 10 | let end_entity = new_end_entity(&ca_params, &ca_key); 11 | 12 | let end_entity_pem = end_entity.pem(); 13 | println!("directly signed end-entity certificate: {end_entity_pem}"); 14 | 15 | let ca_cert_pem = ca.pem(); 16 | println!("ca certificate: {ca_cert_pem}"); 17 | } 18 | 19 | fn new_ca() -> (CertificateParams, Certificate, KeyPair) { 20 | let mut params = 21 | CertificateParams::new(Vec::default()).expect("empty subject alt name can't produce error"); 22 | let (yesterday, tomorrow) = validity_period(); 23 | params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 24 | params.distinguished_name.push( 25 | DnType::CountryName, 26 | PrintableString("BR".try_into().unwrap()), 27 | ); 28 | params 29 | .distinguished_name 30 | .push(DnType::OrganizationName, "Crab widgits SE"); 31 | params.key_usages.push(KeyUsagePurpose::DigitalSignature); 32 | params.key_usages.push(KeyUsagePurpose::KeyCertSign); 33 | params.key_usages.push(KeyUsagePurpose::CrlSign); 34 | 35 | params.not_before = yesterday; 36 | params.not_after = tomorrow; 37 | 38 | let key_pair = KeyPair::generate().unwrap(); 39 | let cert = params.self_signed(&key_pair).unwrap(); 40 | (params, cert, key_pair) 41 | } 42 | 43 | fn new_end_entity(ca: &CertificateParams, ca_key: &KeyPair) -> Certificate { 44 | let name = "entity.other.host"; 45 | let mut params = CertificateParams::new(vec![name.into()]).expect("we know the name is valid"); 46 | let (yesterday, tomorrow) = validity_period(); 47 | params.distinguished_name.push(DnType::CommonName, name); 48 | params.use_authority_key_identifier_extension = true; 49 | params.key_usages.push(KeyUsagePurpose::DigitalSignature); 50 | params 51 | .extended_key_usages 52 | .push(ExtendedKeyUsagePurpose::ServerAuth); 53 | params.not_before = yesterday; 54 | params.not_after = tomorrow; 55 | 56 | let key_pair = KeyPair::generate().unwrap(); 57 | params.signed_by(&key_pair, ca, ca_key).unwrap() 58 | } 59 | 60 | fn validity_period() -> (OffsetDateTime, OffsetDateTime) { 61 | let day = Duration::new(86400, 0); 62 | let yesterday = OffsetDateTime::now_utc().checked_sub(day).unwrap(); 63 | let tomorrow = OffsetDateTime::now_utc().checked_add(day).unwrap(); 64 | (yesterday, tomorrow) 65 | } 66 | -------------------------------------------------------------------------------- /rcgen/examples/simple.rs: -------------------------------------------------------------------------------- 1 | use rcgen::{date_time_ymd, CertificateParams, DistinguishedName, DnType, KeyPair, SanType}; 2 | use std::fs; 3 | 4 | fn main() -> Result<(), Box> { 5 | let mut params: CertificateParams = Default::default(); 6 | params.not_before = date_time_ymd(1975, 1, 1); 7 | params.not_after = date_time_ymd(4096, 1, 1); 8 | params.distinguished_name = DistinguishedName::new(); 9 | params 10 | .distinguished_name 11 | .push(DnType::OrganizationName, "Crab widgits SE"); 12 | params 13 | .distinguished_name 14 | .push(DnType::CommonName, "Master Cert"); 15 | params.subject_alt_names = vec![ 16 | SanType::DnsName("crabs.crabs".try_into()?), 17 | SanType::DnsName("localhost".try_into()?), 18 | ]; 19 | 20 | let key_pair = KeyPair::generate()?; 21 | let cert = params.self_signed(&key_pair)?; 22 | 23 | let pem_serialized = cert.pem(); 24 | let pem = pem::parse(&pem_serialized)?; 25 | let der_serialized = pem.contents(); 26 | println!("{pem_serialized}"); 27 | println!("{}", key_pair.serialize_pem()); 28 | fs::create_dir_all("certs/")?; 29 | fs::write("certs/cert.pem", pem_serialized.as_bytes())?; 30 | fs::write("certs/cert.der", der_serialized)?; 31 | fs::write("certs/key.pem", key_pair.serialize_pem().as_bytes())?; 32 | fs::write("certs/key.der", key_pair.serialize_der())?; 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /rcgen/src/crl.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "pem")] 2 | use pem::Pem; 3 | use pki_types::CertificateRevocationListDer; 4 | use time::OffsetDateTime; 5 | use yasna::DERWriter; 6 | use yasna::Tag; 7 | 8 | use crate::key_pair::sign_der; 9 | #[cfg(feature = "pem")] 10 | use crate::ENCODE_CONFIG; 11 | use crate::{ 12 | oid, write_distinguished_name, write_dt_utc_or_generalized, 13 | write_x509_authority_key_identifier, write_x509_extension, CertificateParams, Error, Issuer, 14 | KeyIdMethod, KeyUsagePurpose, SerialNumber, SigningKey, 15 | }; 16 | 17 | /// A certificate revocation list (CRL) 18 | /// 19 | /// ## Example 20 | /// 21 | /// ``` 22 | /// extern crate rcgen; 23 | /// use rcgen::*; 24 | /// 25 | /// #[cfg(not(feature = "crypto"))] 26 | /// struct MyKeyPair { public_key: Vec } 27 | /// #[cfg(not(feature = "crypto"))] 28 | /// impl SigningKey for MyKeyPair { 29 | /// fn sign(&self, _: &[u8]) -> Result, rcgen::Error> { Ok(vec![]) } 30 | /// } 31 | /// #[cfg(not(feature = "crypto"))] 32 | /// impl PublicKeyData for MyKeyPair { 33 | /// fn der_bytes(&self) -> &[u8] { &self.public_key } 34 | /// fn algorithm(&self) -> &'static SignatureAlgorithm { &PKCS_ED25519 } 35 | /// } 36 | /// # fn main () { 37 | /// // Generate a CRL issuer. 38 | /// let mut issuer_params = CertificateParams::new(vec!["crl.issuer.example.com".to_string()]).unwrap(); 39 | /// issuer_params.serial_number = Some(SerialNumber::from(9999)); 40 | /// issuer_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 41 | /// issuer_params.key_usages = vec![KeyUsagePurpose::KeyCertSign, KeyUsagePurpose::DigitalSignature, KeyUsagePurpose::CrlSign]; 42 | /// #[cfg(feature = "crypto")] 43 | /// let key_pair = KeyPair::generate().unwrap(); 44 | /// #[cfg(not(feature = "crypto"))] 45 | /// let key_pair = MyKeyPair { public_key: vec![] }; 46 | /// let issuer = issuer_params.self_signed(&key_pair).unwrap(); 47 | /// // Describe a revoked certificate. 48 | /// let revoked_cert = RevokedCertParams{ 49 | /// serial_number: SerialNumber::from(9999), 50 | /// revocation_time: date_time_ymd(2024, 06, 17), 51 | /// reason_code: Some(RevocationReason::KeyCompromise), 52 | /// invalidity_date: None, 53 | /// }; 54 | /// // Create a CRL signed by the issuer, revoking revoked_cert. 55 | /// let crl = CertificateRevocationListParams{ 56 | /// this_update: date_time_ymd(2023, 06, 17), 57 | /// next_update: date_time_ymd(2024, 06, 17), 58 | /// crl_number: SerialNumber::from(1234), 59 | /// issuing_distribution_point: None, 60 | /// revoked_certs: vec![revoked_cert], 61 | /// #[cfg(feature = "crypto")] 62 | /// key_identifier_method: KeyIdMethod::Sha256, 63 | /// #[cfg(not(feature = "crypto"))] 64 | /// key_identifier_method: KeyIdMethod::PreSpecified(vec![]), 65 | /// }.signed_by(&issuer_params, &key_pair).unwrap(); 66 | ///# } 67 | #[derive(Clone, Debug)] 68 | pub struct CertificateRevocationList { 69 | der: CertificateRevocationListDer<'static>, 70 | } 71 | 72 | impl CertificateRevocationList { 73 | /// Get the CRL in PEM encoded format. 74 | #[cfg(feature = "pem")] 75 | pub fn pem(&self) -> Result { 76 | let p = Pem::new("X509 CRL", &*self.der); 77 | Ok(pem::encode_config(&p, ENCODE_CONFIG)) 78 | } 79 | 80 | /// Get the CRL in DER encoded format. 81 | /// 82 | /// [`CertificateRevocationListDer`] implements `Deref` and `AsRef<[u8]>`, 83 | /// so you can easily extract the DER bytes from the return value. 84 | pub fn der(&self) -> &CertificateRevocationListDer<'static> { 85 | &self.der 86 | } 87 | } 88 | 89 | impl From for CertificateRevocationListDer<'static> { 90 | fn from(crl: CertificateRevocationList) -> Self { 91 | crl.der 92 | } 93 | } 94 | 95 | /// A certificate revocation list (CRL) distribution point, to be included in a certificate's 96 | /// [distribution points extension](https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.13) or 97 | /// a CRL's [issuing distribution point extension](https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5) 98 | #[derive(Debug, PartialEq, Eq, Clone)] 99 | pub struct CrlDistributionPoint { 100 | /// One or more URI distribution point names, indicating a place the current CRL can 101 | /// be retrieved. When present, SHOULD include at least one LDAP or HTTP URI. 102 | pub uris: Vec, 103 | } 104 | 105 | impl CrlDistributionPoint { 106 | pub(crate) fn write_der(&self, writer: DERWriter) { 107 | // DistributionPoint SEQUENCE 108 | writer.write_sequence(|writer| { 109 | write_distribution_point_name_uris(writer.next(), &self.uris); 110 | }); 111 | } 112 | } 113 | 114 | fn write_distribution_point_name_uris<'a>( 115 | writer: DERWriter, 116 | uris: impl IntoIterator, 117 | ) { 118 | // distributionPoint DistributionPointName 119 | writer.write_tagged_implicit(Tag::context(0), |writer| { 120 | writer.write_sequence(|writer| { 121 | // fullName GeneralNames 122 | writer 123 | .next() 124 | .write_tagged_implicit(Tag::context(0), |writer| { 125 | // GeneralNames 126 | writer.write_sequence(|writer| { 127 | for uri in uris.into_iter() { 128 | // uniformResourceIdentifier [6] IA5String, 129 | writer 130 | .next() 131 | .write_tagged_implicit(Tag::context(6), |writer| { 132 | writer.write_ia5_string(uri) 133 | }); 134 | } 135 | }) 136 | }); 137 | }); 138 | }); 139 | } 140 | 141 | /// Identifies the reason a certificate was revoked. 142 | /// See [RFC 5280 §5.3.1][1] 143 | /// 144 | /// [1]: 145 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 146 | #[allow(missing_docs)] // Not much to add above the code name. 147 | pub enum RevocationReason { 148 | Unspecified = 0, 149 | KeyCompromise = 1, 150 | CaCompromise = 2, 151 | AffiliationChanged = 3, 152 | Superseded = 4, 153 | CessationOfOperation = 5, 154 | CertificateHold = 6, 155 | // 7 is not defined. 156 | RemoveFromCrl = 8, 157 | PrivilegeWithdrawn = 9, 158 | AaCompromise = 10, 159 | } 160 | 161 | /// Parameters used for certificate revocation list (CRL) generation 162 | #[derive(Clone, Debug)] 163 | pub struct CertificateRevocationListParams { 164 | /// Issue date of the CRL. 165 | pub this_update: OffsetDateTime, 166 | /// The date by which the next CRL will be issued. 167 | pub next_update: OffsetDateTime, 168 | /// A monotonically increasing sequence number for a given CRL scope and issuer. 169 | pub crl_number: SerialNumber, 170 | /// An optional CRL extension identifying the CRL distribution point and scope for a 171 | /// particular CRL as described in RFC 5280 Section 5.2.5[^1]. 172 | /// 173 | /// [^1]: 174 | pub issuing_distribution_point: Option, 175 | /// A list of zero or more parameters describing revoked certificates included in the CRL. 176 | pub revoked_certs: Vec, 177 | /// Method to generate key identifiers from public keys 178 | /// 179 | /// Defaults to SHA-256. 180 | pub key_identifier_method: KeyIdMethod, 181 | } 182 | 183 | impl CertificateRevocationListParams { 184 | /// Serializes the certificate revocation list (CRL). 185 | /// 186 | /// Including a signature from the issuing certificate authority's key. 187 | pub fn signed_by( 188 | &self, 189 | issuer: &CertificateParams, 190 | issuer_key: &impl SigningKey, 191 | ) -> Result { 192 | if self.next_update.le(&self.this_update) { 193 | return Err(Error::InvalidCrlNextUpdate); 194 | } 195 | 196 | let issuer = Issuer::new(issuer, issuer_key); 197 | if !issuer.key_usages.is_empty() && !issuer.key_usages.contains(&KeyUsagePurpose::CrlSign) { 198 | return Err(Error::IssuerNotCrlSigner); 199 | } 200 | 201 | Ok(CertificateRevocationList { 202 | der: self.serialize_der(issuer)?.into(), 203 | }) 204 | } 205 | 206 | fn serialize_der(&self, issuer: Issuer<'_, impl SigningKey>) -> Result, Error> { 207 | sign_der(issuer.signing_key, |writer| { 208 | // Write CRL version. 209 | // RFC 5280 §5.1.2.1: 210 | // This optional field describes the version of the encoded CRL. When 211 | // extensions are used, as required by this profile, this field MUST be 212 | // present and MUST specify version 2 (the integer value is 1). 213 | // RFC 5280 §5.2: 214 | // Conforming CRL issuers are REQUIRED to include the authority key 215 | // identifier (Section 5.2.1) and the CRL number (Section 5.2.3) 216 | // extensions in all CRLs issued. 217 | writer.next().write_u8(1); 218 | 219 | // Write algorithm identifier. 220 | // RFC 5280 §5.1.2.2: 221 | // This field MUST contain the same algorithm identifier as the 222 | // signatureAlgorithm field in the sequence CertificateList 223 | issuer 224 | .signing_key 225 | .algorithm() 226 | .write_alg_ident(writer.next()); 227 | 228 | // Write issuer. 229 | // RFC 5280 §5.1.2.3: 230 | // The issuer field MUST contain a non-empty X.500 distinguished name (DN). 231 | write_distinguished_name(writer.next(), issuer.distinguished_name); 232 | 233 | // Write thisUpdate date. 234 | // RFC 5280 §5.1.2.4: 235 | // This field indicates the issue date of this CRL. thisUpdate may be 236 | // encoded as UTCTime or GeneralizedTime. 237 | write_dt_utc_or_generalized(writer.next(), self.this_update); 238 | 239 | // Write nextUpdate date. 240 | // While OPTIONAL in the ASN.1 module, RFC 5280 §5.1.2.5 says: 241 | // Conforming CRL issuers MUST include the nextUpdate field in all CRLs. 242 | write_dt_utc_or_generalized(writer.next(), self.next_update); 243 | 244 | // Write revokedCertificates. 245 | // RFC 5280 §5.1.2.6: 246 | // When there are no revoked certificates, the revoked certificates list 247 | // MUST be absent 248 | if !self.revoked_certs.is_empty() { 249 | writer.next().write_sequence(|writer| { 250 | for revoked_cert in &self.revoked_certs { 251 | revoked_cert.write_der(writer.next()); 252 | } 253 | }); 254 | } 255 | 256 | // Write crlExtensions. 257 | // RFC 5280 §5.1.2.7: 258 | // This field may only appear if the version is 2 (Section 5.1.2.1). If 259 | // present, this field is a sequence of one or more CRL extensions. 260 | // RFC 5280 §5.2: 261 | // Conforming CRL issuers are REQUIRED to include the authority key 262 | // identifier (Section 5.2.1) and the CRL number (Section 5.2.3) 263 | // extensions in all CRLs issued. 264 | writer.next().write_tagged(Tag::context(0), |writer| { 265 | writer.write_sequence(|writer| { 266 | // Write authority key identifier. 267 | write_x509_authority_key_identifier( 268 | writer.next(), 269 | self.key_identifier_method 270 | .derive(issuer.signing_key.subject_public_key_info()), 271 | ); 272 | 273 | // Write CRL number. 274 | write_x509_extension(writer.next(), oid::CRL_NUMBER, false, |writer| { 275 | writer.write_bigint_bytes(self.crl_number.as_ref(), true); 276 | }); 277 | 278 | // Write issuing distribution point (if present). 279 | if let Some(issuing_distribution_point) = &self.issuing_distribution_point { 280 | write_x509_extension( 281 | writer.next(), 282 | oid::CRL_ISSUING_DISTRIBUTION_POINT, 283 | true, 284 | |writer| { 285 | issuing_distribution_point.write_der(writer); 286 | }, 287 | ); 288 | } 289 | }); 290 | }); 291 | 292 | Ok(()) 293 | }) 294 | } 295 | } 296 | 297 | /// A certificate revocation list (CRL) issuing distribution point, to be included in a CRL's 298 | /// [issuing distribution point extension](https://datatracker.ietf.org/doc/html/rfc5280#section-5.2.5). 299 | #[derive(Clone, Debug)] 300 | pub struct CrlIssuingDistributionPoint { 301 | /// The CRL's distribution point, containing a sequence of URIs the CRL can be retrieved from. 302 | pub distribution_point: CrlDistributionPoint, 303 | /// An optional description of the CRL's scope. If omitted, the CRL may contain 304 | /// both user certs and CA certs. 305 | pub scope: Option, 306 | } 307 | 308 | impl CrlIssuingDistributionPoint { 309 | fn write_der(&self, writer: DERWriter) { 310 | // IssuingDistributionPoint SEQUENCE 311 | writer.write_sequence(|writer| { 312 | // distributionPoint [0] DistributionPointName OPTIONAL 313 | write_distribution_point_name_uris(writer.next(), &self.distribution_point.uris); 314 | 315 | // -- at most one of onlyContainsUserCerts, onlyContainsCACerts, 316 | // -- and onlyContainsAttributeCerts may be set to TRUE. 317 | if let Some(scope) = self.scope { 318 | let tag = match scope { 319 | // onlyContainsUserCerts [1] BOOLEAN DEFAULT FALSE, 320 | CrlScope::UserCertsOnly => Tag::context(1), 321 | // onlyContainsCACerts [2] BOOLEAN DEFAULT FALSE, 322 | CrlScope::CaCertsOnly => Tag::context(2), 323 | }; 324 | writer.next().write_tagged_implicit(tag, |writer| { 325 | writer.write_bool(true); 326 | }); 327 | } 328 | }); 329 | } 330 | } 331 | 332 | /// Describes the scope of a CRL for an issuing distribution point extension. 333 | #[derive(Debug, Clone, Copy, Eq, PartialEq)] 334 | pub enum CrlScope { 335 | /// The CRL contains only end-entity user certificates. 336 | UserCertsOnly, 337 | /// The CRL contains only CA certificates. 338 | CaCertsOnly, 339 | } 340 | 341 | /// Parameters used for describing a revoked certificate included in a [`CertificateRevocationList`]. 342 | #[derive(Clone, Debug)] 343 | pub struct RevokedCertParams { 344 | /// Serial number identifying the revoked certificate. 345 | pub serial_number: SerialNumber, 346 | /// The date at which the CA processed the revocation. 347 | pub revocation_time: OffsetDateTime, 348 | /// An optional reason code identifying why the certificate was revoked. 349 | pub reason_code: Option, 350 | /// An optional field describing the date on which it was known or suspected that the 351 | /// private key was compromised or the certificate otherwise became invalid. This date 352 | /// may be earlier than the [`RevokedCertParams::revocation_time`]. 353 | pub invalidity_date: Option, 354 | } 355 | 356 | impl RevokedCertParams { 357 | fn write_der(&self, writer: DERWriter) { 358 | writer.write_sequence(|writer| { 359 | // Write serial number. 360 | // RFC 5280 §4.1.2.2: 361 | // Certificate users MUST be able to handle serialNumber values up to 20 octets. 362 | // Conforming CAs MUST NOT use serialNumber values longer than 20 octets. 363 | // 364 | // Note: Non-conforming CAs may issue certificates with serial numbers 365 | // that are negative or zero. Certificate users SHOULD be prepared to 366 | // gracefully handle such certificates. 367 | writer 368 | .next() 369 | .write_bigint_bytes(self.serial_number.as_ref(), true); 370 | 371 | // Write revocation date. 372 | write_dt_utc_or_generalized(writer.next(), self.revocation_time); 373 | 374 | // Write extensions if applicable. 375 | // RFC 5280 §5.3: 376 | // Support for the CRL entry extensions defined in this specification is 377 | // optional for conforming CRL issuers and applications. However, CRL 378 | // issuers SHOULD include reason codes (Section 5.3.1) and invalidity 379 | // dates (Section 5.3.2) whenever this information is available. 380 | let has_reason_code = 381 | matches!(self.reason_code, Some(reason) if reason != RevocationReason::Unspecified); 382 | let has_invalidity_date = self.invalidity_date.is_some(); 383 | if has_reason_code || has_invalidity_date { 384 | writer.next().write_sequence(|writer| { 385 | // Write reason code if present. 386 | if let Some(reason_code) = self.reason_code { 387 | write_x509_extension(writer.next(), oid::CRL_REASONS, false, |writer| { 388 | writer.write_enum(reason_code as i64); 389 | }); 390 | } 391 | 392 | // Write invalidity date if present. 393 | if let Some(invalidity_date) = self.invalidity_date { 394 | write_x509_extension( 395 | writer.next(), 396 | oid::CRL_INVALIDITY_DATE, 397 | false, 398 | |writer| { 399 | write_dt_utc_or_generalized(writer, invalidity_date); 400 | }, 401 | ) 402 | } 403 | }); 404 | } 405 | }) 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /rcgen/src/csr.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash; 2 | 3 | #[cfg(feature = "pem")] 4 | use pem::Pem; 5 | use pki_types::CertificateSigningRequestDer; 6 | 7 | #[cfg(feature = "pem")] 8 | use crate::ENCODE_CONFIG; 9 | use crate::{ 10 | Certificate, CertificateParams, Error, Issuer, PublicKeyData, SignatureAlgorithm, SigningKey, 11 | }; 12 | #[cfg(feature = "x509-parser")] 13 | use crate::{DistinguishedName, SanType}; 14 | 15 | /// A public key, extracted from a CSR 16 | #[derive(Clone, Debug, PartialEq, Eq, Hash)] 17 | pub struct PublicKey { 18 | raw: Vec, 19 | alg: &'static SignatureAlgorithm, 20 | } 21 | 22 | impl PublicKey { 23 | /// The algorithm used to generate the public key and sign the CSR. 24 | pub fn algorithm(&self) -> &SignatureAlgorithm { 25 | self.alg 26 | } 27 | } 28 | 29 | impl PublicKeyData for PublicKey { 30 | fn der_bytes(&self) -> &[u8] { 31 | &self.raw 32 | } 33 | 34 | fn algorithm(&self) -> &'static SignatureAlgorithm { 35 | self.alg 36 | } 37 | } 38 | 39 | /// A certificate signing request (CSR) that can be encoded to PEM or DER. 40 | #[derive(Clone, Debug)] 41 | pub struct CertificateSigningRequest { 42 | pub(crate) der: CertificateSigningRequestDer<'static>, 43 | } 44 | 45 | impl CertificateSigningRequest { 46 | /// Get the PEM-encoded bytes of the certificate signing request. 47 | #[cfg(feature = "pem")] 48 | pub fn pem(&self) -> Result { 49 | let p = Pem::new("CERTIFICATE REQUEST", &*self.der); 50 | Ok(pem::encode_config(&p, ENCODE_CONFIG)) 51 | } 52 | 53 | /// Get the DER-encoded bytes of the certificate signing request. 54 | /// 55 | /// [`CertificateSigningRequestDer`] implements `Deref` and `AsRef<[u8]>`, 56 | /// so you can easily extract the DER bytes from the return value. 57 | pub fn der(&self) -> &CertificateSigningRequestDer<'static> { 58 | &self.der 59 | } 60 | } 61 | 62 | impl From for CertificateSigningRequestDer<'static> { 63 | fn from(csr: CertificateSigningRequest) -> Self { 64 | csr.der 65 | } 66 | } 67 | 68 | /// Parameters for a certificate signing request 69 | #[derive(Clone, Debug)] 70 | pub struct CertificateSigningRequestParams { 71 | /// Parameters for the certificate to be signed. 72 | pub params: CertificateParams, 73 | /// Public key to include in the certificate signing request. 74 | pub public_key: PublicKey, 75 | } 76 | 77 | impl CertificateSigningRequestParams { 78 | /// Parse a certificate signing request from the ASCII PEM format 79 | /// 80 | /// See [`from_der`](Self::from_der) for more details. 81 | #[cfg(all(feature = "pem", feature = "x509-parser"))] 82 | pub fn from_pem(pem_str: &str) -> Result { 83 | let csr = pem::parse(pem_str).map_err(|_| Error::CouldNotParseCertificationRequest)?; 84 | Self::from_der(&csr.contents().into()) 85 | } 86 | 87 | /// Parse a certificate signing request from DER-encoded bytes 88 | /// 89 | /// Currently, this only supports the `Subject Alternative Name` extension. 90 | /// On encountering other extensions, this function will return an error. 91 | /// 92 | /// [`rustls_pemfile::csr()`] is often used to obtain a [`CertificateSigningRequestDer`] from 93 | /// PEM input. If you already have a byte slice containing DER, it can trivially be converted 94 | /// into [`CertificateSigningRequestDer`] using the [`Into`] trait. 95 | /// 96 | /// [`rustls_pemfile::csr()`]: https://docs.rs/rustls-pemfile/latest/rustls_pemfile/fn.csr.html 97 | #[cfg(feature = "x509-parser")] 98 | pub fn from_der(csr: &CertificateSigningRequestDer<'_>) -> Result { 99 | use crate::KeyUsagePurpose; 100 | use x509_parser::prelude::FromDer; 101 | 102 | let csr = x509_parser::certification_request::X509CertificationRequest::from_der(csr) 103 | .map_err(|_| Error::CouldNotParseCertificationRequest)? 104 | .1; 105 | csr.verify_signature().map_err(|_| Error::RingUnspecified)?; 106 | let alg_oid = csr 107 | .signature_algorithm 108 | .algorithm 109 | .iter() 110 | .ok_or(Error::CouldNotParseCertificationRequest)? 111 | .collect::>(); 112 | let alg = SignatureAlgorithm::from_oid(&alg_oid)?; 113 | 114 | let info = &csr.certification_request_info; 115 | let mut params = CertificateParams { 116 | distinguished_name: DistinguishedName::from_name(&info.subject)?, 117 | ..CertificateParams::default() 118 | }; 119 | let raw = info.subject_pki.subject_public_key.data.to_vec(); 120 | 121 | if let Some(extensions) = csr.requested_extensions() { 122 | for ext in extensions { 123 | match ext { 124 | x509_parser::extensions::ParsedExtension::KeyUsage(key_usage) => { 125 | // This x509 parser stores flags in reversed bit BIT STRING order 126 | params.key_usages = 127 | KeyUsagePurpose::from_u16(key_usage.flags.reverse_bits()); 128 | }, 129 | x509_parser::extensions::ParsedExtension::SubjectAlternativeName(san) => { 130 | for name in &san.general_names { 131 | params 132 | .subject_alt_names 133 | .push(SanType::try_from_general(name)?); 134 | } 135 | }, 136 | x509_parser::extensions::ParsedExtension::ExtendedKeyUsage(eku) => { 137 | if eku.any { 138 | params.insert_extended_key_usage(crate::ExtendedKeyUsagePurpose::Any); 139 | } 140 | if eku.server_auth { 141 | params.insert_extended_key_usage( 142 | crate::ExtendedKeyUsagePurpose::ServerAuth, 143 | ); 144 | } 145 | if eku.client_auth { 146 | params.insert_extended_key_usage( 147 | crate::ExtendedKeyUsagePurpose::ClientAuth, 148 | ); 149 | } 150 | if eku.code_signing { 151 | params.insert_extended_key_usage( 152 | crate::ExtendedKeyUsagePurpose::CodeSigning, 153 | ); 154 | } 155 | if eku.email_protection { 156 | params.insert_extended_key_usage( 157 | crate::ExtendedKeyUsagePurpose::EmailProtection, 158 | ); 159 | } 160 | if eku.time_stamping { 161 | params.insert_extended_key_usage( 162 | crate::ExtendedKeyUsagePurpose::TimeStamping, 163 | ); 164 | } 165 | if eku.ocsp_signing { 166 | params.insert_extended_key_usage( 167 | crate::ExtendedKeyUsagePurpose::OcspSigning, 168 | ); 169 | } 170 | if !eku.other.is_empty() { 171 | return Err(Error::UnsupportedExtension); 172 | } 173 | }, 174 | _ => return Err(Error::UnsupportedExtension), 175 | } 176 | } 177 | } 178 | 179 | // Not yet handled: 180 | // * is_ca 181 | // * extended_key_usages 182 | // * name_constraints 183 | // and any other extensions. 184 | 185 | Ok(Self { 186 | params, 187 | public_key: PublicKey { alg, raw }, 188 | }) 189 | } 190 | 191 | /// Generate a new certificate based on the requested parameters, signed by the provided 192 | /// issuer. 193 | /// 194 | /// The returned certificate will have its issuer field set to the subject of the provided 195 | /// `issuer`, and the authority key identifier extension will be populated using the subject 196 | /// public key of `issuer`. It will be signed by `issuer_key`. 197 | /// 198 | /// Note that no validation of the `issuer` certificate is performed. Rcgen will not require 199 | /// the certificate to be a CA certificate, or have key usage extensions that allow signing. 200 | /// 201 | /// The returned [`Certificate`] may be serialized using [`Certificate::der`] and 202 | /// [`Certificate::pem`]. 203 | pub fn signed_by( 204 | &self, 205 | issuer: &CertificateParams, 206 | issuer_key: &impl SigningKey, 207 | ) -> Result { 208 | let issuer = Issuer::new(issuer, issuer_key); 209 | Ok(Certificate { 210 | der: self 211 | .params 212 | .serialize_der_with_signer(&self.public_key, issuer)?, 213 | }) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /rcgen/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | #[derive(Clone, Debug, PartialEq, Eq)] 4 | #[non_exhaustive] 5 | /// The error type of the rcgen crate 6 | pub enum Error { 7 | /// The given certificate couldn't be parsed 8 | CouldNotParseCertificate, 9 | /// The given certificate signing request couldn't be parsed 10 | CouldNotParseCertificationRequest, 11 | /// The given key pair couldn't be parsed 12 | CouldNotParseKeyPair, 13 | #[cfg(feature = "x509-parser")] 14 | /// Invalid subject alternative name type 15 | InvalidNameType, 16 | /// Invalid ASN.1 string 17 | InvalidAsn1String(InvalidAsn1String), 18 | /// An IP address was provided as a byte array, but the byte array was an invalid length. 19 | InvalidIpAddressOctetLength(usize), 20 | /// There is no support for generating 21 | /// keys for the given algorithm 22 | KeyGenerationUnavailable, 23 | #[cfg(feature = "x509-parser")] 24 | /// Unsupported extension requested in CSR 25 | UnsupportedExtension, 26 | /// The requested signature algorithm is not supported 27 | UnsupportedSignatureAlgorithm, 28 | /// Unspecified `ring` error 29 | RingUnspecified, 30 | /// The `ring` library rejected the key upon loading 31 | RingKeyRejected(String), 32 | /// Time conversion related errors 33 | Time, 34 | #[cfg(feature = "pem")] 35 | /// Error from the pem crate 36 | PemError(String), 37 | /// Error generated by a remote key operation 38 | RemoteKeyError, 39 | /// Unsupported field when generating a CSR 40 | UnsupportedInCsr, 41 | /// Invalid certificate revocation list (CRL) next update. 42 | InvalidCrlNextUpdate, 43 | /// CRL issuer specifies Key Usages that don't include cRLSign. 44 | IssuerNotCrlSigner, 45 | #[cfg(not(feature = "crypto"))] 46 | /// Missing serial number 47 | MissingSerialNumber, 48 | /// X509 parsing error 49 | #[cfg(feature = "x509-parser")] 50 | X509(String), 51 | } 52 | 53 | impl fmt::Display for Error { 54 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 55 | use self::Error::*; 56 | match self { 57 | CouldNotParseCertificate => write!(f, "Could not parse certificate")?, 58 | CouldNotParseCertificationRequest => write!( 59 | f, 60 | "Could not parse certificate signing \ 61 | request" 62 | )?, 63 | CouldNotParseKeyPair => write!(f, "Could not parse key pair")?, 64 | #[cfg(feature = "x509-parser")] 65 | InvalidNameType => write!(f, "Invalid subject alternative name type")?, 66 | InvalidAsn1String(e) => write!(f, "{}", e)?, 67 | InvalidIpAddressOctetLength(actual) => { 68 | write!(f, "Invalid IP address octet length of {actual} bytes")? 69 | }, 70 | KeyGenerationUnavailable => write!( 71 | f, 72 | "There is no support for generating \ 73 | keys for the given algorithm" 74 | )?, 75 | UnsupportedSignatureAlgorithm => write!( 76 | f, 77 | "The requested signature algorithm \ 78 | is not supported" 79 | )?, 80 | #[cfg(feature = "x509-parser")] 81 | UnsupportedExtension => write!(f, "Unsupported extension requested in CSR")?, 82 | RingUnspecified => write!(f, "Unspecified ring error")?, 83 | RingKeyRejected(e) => write!(f, "Key rejected by ring: {}", e)?, 84 | 85 | Time => write!(f, "Time error")?, 86 | RemoteKeyError => write!(f, "Remote key error")?, 87 | #[cfg(feature = "pem")] 88 | PemError(e) => write!(f, "PEM error: {}", e)?, 89 | UnsupportedInCsr => write!(f, "Certificate parameter unsupported in CSR")?, 90 | InvalidCrlNextUpdate => write!(f, "Invalid CRL next update parameter")?, 91 | IssuerNotCrlSigner => write!( 92 | f, 93 | "CRL issuer must specify no key usage, or key usage including cRLSign" 94 | )?, 95 | #[cfg(not(feature = "crypto"))] 96 | MissingSerialNumber => write!(f, "A serial number must be specified")?, 97 | #[cfg(feature = "x509-parser")] 98 | X509(e) => write!(f, "X.509 parsing error: {e}")?, 99 | }; 100 | Ok(()) 101 | } 102 | } 103 | 104 | impl std::error::Error for Error {} 105 | 106 | /// Invalid ASN.1 string type 107 | #[derive(Clone, Debug, PartialEq, Eq)] 108 | #[non_exhaustive] 109 | pub enum InvalidAsn1String { 110 | /// Invalid PrintableString type 111 | PrintableString(String), 112 | /// Invalid UniversalString type 113 | UniversalString(String), 114 | /// Invalid Ia5String type 115 | Ia5String(String), 116 | /// Invalid TeletexString type 117 | TeletexString(String), 118 | /// Invalid BmpString type 119 | BmpString(String), 120 | } 121 | 122 | impl fmt::Display for InvalidAsn1String { 123 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 124 | use InvalidAsn1String::*; 125 | match self { 126 | PrintableString(s) => write!(f, "Invalid PrintableString: '{}'", s)?, 127 | Ia5String(s) => write!(f, "Invalid IA5String: '{}'", s)?, 128 | BmpString(s) => write!(f, "Invalid BMPString: '{}'", s)?, 129 | UniversalString(s) => write!(f, "Invalid UniversalString: '{}'", s)?, 130 | TeletexString(s) => write!(f, "Invalid TeletexString: '{}'", s)?, 131 | }; 132 | Ok(()) 133 | } 134 | } 135 | 136 | /// A trait describing an error that can be converted into an `rcgen::Error`. 137 | /// 138 | /// We use this trait to avoid leaking external error types into the public API 139 | /// through a `From for Error` implementation. 140 | #[cfg(any(feature = "crypto", feature = "pem"))] 141 | pub(crate) trait ExternalError: Sized { 142 | fn _err(self) -> Result; 143 | } 144 | -------------------------------------------------------------------------------- /rcgen/src/oid.rs: -------------------------------------------------------------------------------- 1 | /// pkcs-9-at-extensionRequest in [RFC 2985](https://www.rfc-editor.org/rfc/rfc2985#appendix-A) 2 | pub(crate) const PKCS_9_AT_EXTENSION_REQUEST: &[u64] = &[1, 2, 840, 113549, 1, 9, 14]; 3 | 4 | /// id-at-countryName in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 5 | pub(crate) const COUNTRY_NAME: &[u64] = &[2, 5, 4, 6]; 6 | /// id-at-localityName in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 7 | pub(crate) const LOCALITY_NAME: &[u64] = &[2, 5, 4, 7]; 8 | /// id-at-stateOrProvinceName in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 9 | pub(crate) const STATE_OR_PROVINCE_NAME: &[u64] = &[2, 5, 4, 8]; 10 | /// id-at-organizationName in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 11 | pub(crate) const ORG_NAME: &[u64] = &[2, 5, 4, 10]; 12 | /// id-at-organizationalUnitName in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 13 | pub(crate) const ORG_UNIT_NAME: &[u64] = &[2, 5, 4, 11]; 14 | /// id-at-commonName in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 15 | pub(crate) const COMMON_NAME: &[u64] = &[2, 5, 4, 3]; 16 | 17 | /// id-ecPublicKey in [RFC 5480](https://datatracker.ietf.org/doc/html/rfc5480#appendix-A) 18 | pub(crate) const EC_PUBLIC_KEY: &[u64] = &[1, 2, 840, 10045, 2, 1]; 19 | /// secp256r1 in [RFC 5480](https://datatracker.ietf.org/doc/html/rfc5480#appendix-A) 20 | pub(crate) const EC_SECP_256_R1: &[u64] = &[1, 2, 840, 10045, 3, 1, 7]; 21 | /// secp384r1 in [RFC 5480](https://datatracker.ietf.org/doc/html/rfc5480#appendix-A) 22 | pub(crate) const EC_SECP_384_R1: &[u64] = &[1, 3, 132, 0, 34]; 23 | /// secp521r1 in [RFC 5480](https://datatracker.ietf.org/doc/html/rfc5480#appendix-A) 24 | /// Currently this is only supported with the `aws_lc_rs` feature 25 | #[cfg(feature = "aws_lc_rs")] 26 | pub(crate) const EC_SECP_521_R1: &[u64] = &[1, 3, 132, 0, 35]; 27 | 28 | /// rsaEncryption in [RFC 4055](https://www.rfc-editor.org/rfc/rfc4055#section-6) 29 | pub(crate) const RSA_ENCRYPTION: &[u64] = &[1, 2, 840, 113549, 1, 1, 1]; 30 | 31 | /// id-RSASSA-PSS in [RFC 4055](https://www.rfc-editor.org/rfc/rfc4055#section-6) 32 | pub(crate) const RSASSA_PSS: &[u64] = &[1, 2, 840, 113549, 1, 1, 10]; 33 | 34 | /// id-ce-keyUsage in [RFC 5280](https://tools.ietf.org/html/rfc5280#appendix-A.2) 35 | pub(crate) const KEY_USAGE: &[u64] = &[2, 5, 29, 15]; 36 | 37 | /// id-ce-subjectAltName in [RFC 5280](https://tools.ietf.org/html/rfc5280#appendix-A.2) 38 | pub(crate) const SUBJECT_ALT_NAME: &[u64] = &[2, 5, 29, 17]; 39 | 40 | /// id-ce-basicConstraints in [RFC 5280](https://tools.ietf.org/html/rfc5280#appendix-A.2) 41 | pub(crate) const BASIC_CONSTRAINTS: &[u64] = &[2, 5, 29, 19]; 42 | 43 | /// id-ce-subjectKeyIdentifier in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 44 | pub(crate) const SUBJECT_KEY_IDENTIFIER: &[u64] = &[2, 5, 29, 14]; 45 | 46 | /// id-ce-authorityKeyIdentifier in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 47 | pub(crate) const AUTHORITY_KEY_IDENTIFIER: &[u64] = &[2, 5, 29, 35]; 48 | 49 | /// id-ce-extKeyUsage in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 50 | pub(crate) const EXT_KEY_USAGE: &[u64] = &[2, 5, 29, 37]; 51 | 52 | /// id-ce-nameConstraints in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 53 | pub(crate) const NAME_CONSTRAINTS: &[u64] = &[2, 5, 29, 30]; 54 | 55 | /// id-ce-cRLDistributionPoints in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 56 | pub(crate) const CRL_DISTRIBUTION_POINTS: &[u64] = &[2, 5, 29, 31]; 57 | 58 | /// id-pe-acmeIdentifier in 59 | /// [IANA SMI Numbers registry](https://www.iana.org/assignments/smi-numbers/smi-numbers.xhtml#smi-numbers-1.3.6.1.5.5.7.1) 60 | pub(crate) const PE_ACME: &[u64] = &[1, 3, 6, 1, 5, 5, 7, 1, 31]; 61 | 62 | /// id-ce-cRLNumber in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 63 | pub(crate) const CRL_NUMBER: &[u64] = &[2, 5, 29, 20]; 64 | 65 | /// id-ce-cRLReasons in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 66 | pub(crate) const CRL_REASONS: &[u64] = &[2, 5, 29, 21]; 67 | 68 | /// id-ce-invalidityDate in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 69 | pub(crate) const CRL_INVALIDITY_DATE: &[u64] = &[2, 5, 29, 24]; 70 | 71 | /// id-ce-issuingDistributionPoint in [RFC 5280](https://www.rfc-editor.org/rfc/rfc5280#appendix-A) 72 | pub(crate) const CRL_ISSUING_DISTRIBUTION_POINT: &[u64] = &[2, 5, 29, 28]; 73 | -------------------------------------------------------------------------------- /rcgen/src/ring_like.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(feature = "crypto", feature = "aws_lc_rs"))] 2 | pub(crate) use aws_lc_rs::*; 3 | #[cfg(all(feature = "crypto", feature = "ring", not(feature = "aws_lc_rs")))] 4 | pub(crate) use ring::*; 5 | 6 | #[cfg(feature = "crypto")] 7 | use crate::error::ExternalError; 8 | #[cfg(feature = "crypto")] 9 | use crate::Error; 10 | 11 | #[cfg(feature = "crypto")] 12 | pub(crate) fn ecdsa_from_pkcs8( 13 | alg: &'static signature::EcdsaSigningAlgorithm, 14 | pkcs8: &[u8], 15 | _rng: &dyn rand::SecureRandom, 16 | ) -> Result { 17 | #[cfg(all(feature = "ring", not(feature = "aws_lc_rs")))] 18 | { 19 | signature::EcdsaKeyPair::from_pkcs8(alg, pkcs8, _rng)._err() 20 | } 21 | 22 | #[cfg(feature = "aws_lc_rs")] 23 | { 24 | signature::EcdsaKeyPair::from_pkcs8(alg, pkcs8)._err() 25 | } 26 | } 27 | 28 | #[cfg(all(feature = "crypto", feature = "aws_lc_rs"))] 29 | pub(crate) fn ecdsa_from_private_key_der( 30 | alg: &'static signature::EcdsaSigningAlgorithm, 31 | key: &[u8], 32 | ) -> Result { 33 | signature::EcdsaKeyPair::from_private_key_der(alg, key)._err() 34 | } 35 | 36 | #[cfg(feature = "crypto")] 37 | pub(crate) fn rsa_key_pair_public_modulus_len(kp: &signature::RsaKeyPair) -> usize { 38 | #[cfg(all(feature = "ring", not(feature = "aws_lc_rs")))] 39 | { 40 | kp.public().modulus_len() 41 | } 42 | 43 | #[cfg(feature = "aws_lc_rs")] 44 | { 45 | kp.public_modulus_len() 46 | } 47 | } 48 | 49 | #[cfg(all(feature = "crypto", not(any(feature = "ring", feature = "aws_lc_rs"))))] 50 | compile_error!("At least one of the 'ring' or 'aws_lc_rs' features must be activated when the 'crypto' feature is enabled"); 51 | -------------------------------------------------------------------------------- /rcgen/src/sign_algo.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::hash::{Hash, Hasher}; 3 | 4 | use yasna::models::ObjectIdentifier; 5 | use yasna::DERWriter; 6 | use yasna::Tag; 7 | 8 | #[cfg(feature = "crypto")] 9 | use crate::ring_like::signature::{self, EcdsaSigningAlgorithm, EdDSAParameters, RsaEncoding}; 10 | use crate::Error; 11 | 12 | #[cfg(feature = "crypto")] 13 | #[derive(Clone, Copy, Debug)] 14 | pub(crate) enum SignAlgo { 15 | EcDsa(&'static EcdsaSigningAlgorithm), 16 | EdDsa(&'static EdDSAParameters), 17 | Rsa(&'static dyn RsaEncoding), 18 | } 19 | 20 | #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] 21 | pub(crate) enum SignatureAlgorithmParams { 22 | /// Omit the parameters 23 | None, 24 | /// Write null parameters 25 | Null, 26 | /// RSASSA-PSS-params as per RFC 4055 27 | RsaPss { 28 | hash_algorithm: &'static [u64], 29 | salt_length: u64, 30 | }, 31 | } 32 | 33 | /// Signature algorithm type 34 | #[derive(Clone)] 35 | pub struct SignatureAlgorithm { 36 | oids_sign_alg: &'static [&'static [u64]], 37 | #[cfg(feature = "crypto")] 38 | pub(crate) sign_alg: SignAlgo, 39 | oid_components: &'static [u64], 40 | params: SignatureAlgorithmParams, 41 | } 42 | 43 | impl fmt::Debug for SignatureAlgorithm { 44 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 45 | use algo::*; 46 | if self == &PKCS_RSA_SHA256 { 47 | write!(f, "PKCS_RSA_SHA256") 48 | } else if self == &PKCS_RSA_SHA384 { 49 | write!(f, "PKCS_RSA_SHA384") 50 | } else if self == &PKCS_RSA_SHA512 { 51 | write!(f, "PKCS_RSA_SHA512") 52 | } else if self == &PKCS_RSA_PSS_SHA256 { 53 | write!(f, "PKCS_RSA_PSS_SHA256") 54 | } else if self == &PKCS_ECDSA_P256_SHA256 { 55 | write!(f, "PKCS_ECDSA_P256_SHA256") 56 | } else if self == &PKCS_ECDSA_P384_SHA384 { 57 | write!(f, "PKCS_ECDSA_P384_SHA384") 58 | } else if self == &PKCS_ED25519 { 59 | write!(f, "PKCS_ED25519") 60 | } else { 61 | #[cfg(feature = "aws_lc_rs")] 62 | if self == &PKCS_ECDSA_P521_SHA512 { 63 | return write!(f, "PKCS_ECDSA_P521_SHA512"); 64 | } 65 | 66 | write!(f, "Unknown") 67 | } 68 | } 69 | } 70 | 71 | impl PartialEq for SignatureAlgorithm { 72 | fn eq(&self, other: &Self) -> bool { 73 | (self.oids_sign_alg, self.oid_components) == (other.oids_sign_alg, other.oid_components) 74 | } 75 | } 76 | 77 | impl Eq for SignatureAlgorithm {} 78 | 79 | /// The `Hash` trait is not derived, but implemented according to impl of the `PartialEq` trait 80 | impl Hash for SignatureAlgorithm { 81 | fn hash(&self, state: &mut H) { 82 | // see SignatureAlgorithm::eq(), just this field is compared 83 | self.oids_sign_alg.hash(state); 84 | } 85 | } 86 | impl SignatureAlgorithm { 87 | pub(crate) fn iter() -> std::slice::Iter<'static, &'static SignatureAlgorithm> { 88 | use algo::*; 89 | static ALGORITHMS: &[&SignatureAlgorithm] = &[ 90 | &PKCS_RSA_SHA256, 91 | &PKCS_RSA_SHA384, 92 | &PKCS_RSA_SHA512, 93 | //&PKCS_RSA_PSS_SHA256, 94 | &PKCS_ECDSA_P256_SHA256, 95 | &PKCS_ECDSA_P384_SHA384, 96 | #[cfg(feature = "aws_lc_rs")] 97 | &PKCS_ECDSA_P521_SHA512, 98 | &PKCS_ED25519, 99 | ]; 100 | ALGORITHMS.iter() 101 | } 102 | 103 | /// Retrieve the SignatureAlgorithm for the provided OID 104 | pub fn from_oid(oid: &[u64]) -> Result<&'static SignatureAlgorithm, Error> { 105 | for algo in Self::iter() { 106 | if algo.oid_components == oid { 107 | return Ok(algo); 108 | } 109 | } 110 | Err(Error::UnsupportedSignatureAlgorithm) 111 | } 112 | } 113 | 114 | /// The list of supported signature algorithms 115 | pub(crate) mod algo { 116 | use crate::oid::*; 117 | 118 | use super::*; 119 | 120 | /// RSA signing with PKCS#1 1.5 padding and SHA-256 hashing as per [RFC 4055](https://tools.ietf.org/html/rfc4055) 121 | pub static PKCS_RSA_SHA256: SignatureAlgorithm = SignatureAlgorithm { 122 | oids_sign_alg: &[RSA_ENCRYPTION], 123 | #[cfg(feature = "crypto")] 124 | sign_alg: SignAlgo::Rsa(&signature::RSA_PKCS1_SHA256), 125 | // sha256WithRSAEncryption in RFC 4055 126 | oid_components: &[1, 2, 840, 113549, 1, 1, 11], 127 | params: SignatureAlgorithmParams::Null, 128 | }; 129 | 130 | /// RSA signing with PKCS#1 1.5 padding and SHA-256 hashing as per [RFC 4055](https://tools.ietf.org/html/rfc4055) 131 | pub static PKCS_RSA_SHA384: SignatureAlgorithm = SignatureAlgorithm { 132 | oids_sign_alg: &[RSA_ENCRYPTION], 133 | #[cfg(feature = "crypto")] 134 | sign_alg: SignAlgo::Rsa(&signature::RSA_PKCS1_SHA384), 135 | // sha384WithRSAEncryption in RFC 4055 136 | oid_components: &[1, 2, 840, 113549, 1, 1, 12], 137 | params: SignatureAlgorithmParams::Null, 138 | }; 139 | 140 | /// RSA signing with PKCS#1 1.5 padding and SHA-512 hashing as per [RFC 4055](https://tools.ietf.org/html/rfc4055) 141 | pub static PKCS_RSA_SHA512: SignatureAlgorithm = SignatureAlgorithm { 142 | oids_sign_alg: &[RSA_ENCRYPTION], 143 | #[cfg(feature = "crypto")] 144 | sign_alg: SignAlgo::Rsa(&signature::RSA_PKCS1_SHA512), 145 | // sha512WithRSAEncryption in RFC 4055 146 | oid_components: &[1, 2, 840, 113549, 1, 1, 13], 147 | params: SignatureAlgorithmParams::Null, 148 | }; 149 | 150 | // TODO: not really sure whether the certs we generate actually work. 151 | // Both openssl and webpki reject them. It *might* be possible that openssl 152 | // accepts the certificate if the key is a proper RSA-PSS key, but ring doesn't 153 | // support those: https://github.com/briansmith/ring/issues/1353 154 | // 155 | /// RSA signing with PKCS#1 2.1 RSASSA-PSS padding and SHA-256 hashing as per [RFC 4055](https://tools.ietf.org/html/rfc4055) 156 | pub(crate) static PKCS_RSA_PSS_SHA256: SignatureAlgorithm = SignatureAlgorithm { 157 | // We could also use RSA_ENCRYPTION here, but it's recommended 158 | // to use ID-RSASSA-PSS if possible. 159 | oids_sign_alg: &[RSASSA_PSS], 160 | #[cfg(feature = "crypto")] 161 | sign_alg: SignAlgo::Rsa(&signature::RSA_PSS_SHA256), 162 | oid_components: RSASSA_PSS, //&[1, 2, 840, 113549, 1, 1, 13], 163 | // rSASSA-PSS-SHA256-Params in RFC 4055 164 | params: SignatureAlgorithmParams::RsaPss { 165 | // id-sha256 in https://datatracker.ietf.org/doc/html/rfc4055#section-2.1 166 | hash_algorithm: &[2, 16, 840, 1, 101, 3, 4, 2, 1], 167 | salt_length: 20, 168 | }, 169 | }; 170 | 171 | /// ECDSA signing using the P-256 curves and SHA-256 hashing as per [RFC 5758](https://tools.ietf.org/html/rfc5758#section-3.2) 172 | pub static PKCS_ECDSA_P256_SHA256: SignatureAlgorithm = SignatureAlgorithm { 173 | oids_sign_alg: &[EC_PUBLIC_KEY, EC_SECP_256_R1], 174 | #[cfg(feature = "crypto")] 175 | sign_alg: SignAlgo::EcDsa(&signature::ECDSA_P256_SHA256_ASN1_SIGNING), 176 | // ecdsa-with-SHA256 in RFC 5758 177 | oid_components: &[1, 2, 840, 10045, 4, 3, 2], 178 | params: SignatureAlgorithmParams::None, 179 | }; 180 | 181 | /// ECDSA signing using the P-384 curves and SHA-384 hashing as per [RFC 5758](https://tools.ietf.org/html/rfc5758#section-3.2) 182 | pub static PKCS_ECDSA_P384_SHA384: SignatureAlgorithm = SignatureAlgorithm { 183 | oids_sign_alg: &[EC_PUBLIC_KEY, EC_SECP_384_R1], 184 | #[cfg(feature = "crypto")] 185 | sign_alg: SignAlgo::EcDsa(&signature::ECDSA_P384_SHA384_ASN1_SIGNING), 186 | // ecdsa-with-SHA384 in RFC 5758 187 | oid_components: &[1, 2, 840, 10045, 4, 3, 3], 188 | params: SignatureAlgorithmParams::None, 189 | }; 190 | /// ECDSA signing using the P-521 curves and SHA-512 hashing as per [RFC 5758](https://tools.ietf.org/html/rfc5758#section-3.2) 191 | /// Currently this is only supported with the `aws_lc_rs` feature 192 | #[cfg(feature = "aws_lc_rs")] 193 | pub static PKCS_ECDSA_P521_SHA512: SignatureAlgorithm = SignatureAlgorithm { 194 | oids_sign_alg: &[EC_PUBLIC_KEY, EC_SECP_521_R1], 195 | #[cfg(feature = "crypto")] 196 | sign_alg: SignAlgo::EcDsa(&signature::ECDSA_P521_SHA512_ASN1_SIGNING), 197 | // ecdsa-with-SHA512 in RFC 5758 198 | oid_components: &[1, 2, 840, 10045, 4, 3, 4], 199 | params: SignatureAlgorithmParams::None, 200 | }; 201 | 202 | /// ED25519 curve signing as per [RFC 8410](https://tools.ietf.org/html/rfc8410) 203 | pub static PKCS_ED25519: SignatureAlgorithm = SignatureAlgorithm { 204 | // id-Ed25519 in RFC 8410 205 | oids_sign_alg: &[&[1, 3, 101, 112]], 206 | #[cfg(feature = "crypto")] 207 | sign_alg: SignAlgo::EdDsa(&signature::ED25519), 208 | // id-Ed25519 in RFC 8410 209 | oid_components: &[1, 3, 101, 112], 210 | params: SignatureAlgorithmParams::None, 211 | }; 212 | } 213 | // Signature algorithm IDs as per https://tools.ietf.org/html/rfc4055 214 | impl SignatureAlgorithm { 215 | fn alg_ident_oid(&self) -> ObjectIdentifier { 216 | ObjectIdentifier::from_slice(self.oid_components) 217 | } 218 | fn write_params(&self, writer: &mut yasna::DERWriterSeq) { 219 | match self.params { 220 | SignatureAlgorithmParams::None => (), 221 | SignatureAlgorithmParams::Null => { 222 | writer.next().write_null(); 223 | }, 224 | SignatureAlgorithmParams::RsaPss { 225 | hash_algorithm, 226 | salt_length, 227 | } => { 228 | writer.next().write_sequence(|writer| { 229 | // https://datatracker.ietf.org/doc/html/rfc4055#section-3.1 230 | 231 | let oid = ObjectIdentifier::from_slice(hash_algorithm); 232 | // hashAlgorithm 233 | writer.next().write_tagged(Tag::context(0), |writer| { 234 | writer.write_sequence(|writer| { 235 | writer.next().write_oid(&oid); 236 | }); 237 | }); 238 | // maskGenAlgorithm 239 | writer.next().write_tagged(Tag::context(1), |writer| { 240 | writer.write_sequence(|writer| { 241 | // id-mgf1 in RFC 4055 242 | const ID_MGF1: &[u64] = &[1, 2, 840, 113549, 1, 1, 8]; 243 | let oid = ObjectIdentifier::from_slice(ID_MGF1); 244 | writer.next().write_oid(&oid); 245 | writer.next().write_sequence(|writer| { 246 | let oid = ObjectIdentifier::from_slice(hash_algorithm); 247 | writer.next().write_oid(&oid); 248 | writer.next().write_null(); 249 | }); 250 | }); 251 | }); 252 | // saltLength 253 | writer.next().write_tagged(Tag::context(2), |writer| { 254 | writer.write_u64(salt_length); 255 | }); 256 | // We *must* omit the trailerField element as per RFC 4055 section 3.1 257 | }) 258 | }, 259 | } 260 | } 261 | /// Writes the algorithm identifier as it appears inside a signature 262 | pub(crate) fn write_alg_ident(&self, writer: DERWriter) { 263 | writer.write_sequence(|writer| { 264 | writer.next().write_oid(&self.alg_ident_oid()); 265 | self.write_params(writer); 266 | }); 267 | } 268 | /// Writes the algorithm identifier as it appears inside subjectPublicKeyInfo 269 | pub(crate) fn write_oids_sign_alg(&self, writer: DERWriter) { 270 | writer.write_sequence(|writer| { 271 | for oid in self.oids_sign_alg { 272 | let oid = ObjectIdentifier::from_slice(oid); 273 | writer.next().write_oid(&oid); 274 | } 275 | self.write_params(writer); 276 | }); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /rcgen/src/string.rs: -------------------------------------------------------------------------------- 1 | //! ASN.1 string types 2 | 3 | use std::{fmt, str::FromStr}; 4 | 5 | use crate::{Error, InvalidAsn1String}; 6 | 7 | /// ASN.1 `PrintableString` type. 8 | /// 9 | /// Supports a subset of the ASCII printable characters (described below). 10 | /// 11 | /// For the full ASCII character set, use 12 | /// [`Ia5String`][`crate::Ia5String`]. 13 | /// 14 | /// # Examples 15 | /// 16 | /// You can create a `PrintableString` from [a literal string][`&str`] with [`PrintableString::try_from`]: 17 | /// 18 | /// ``` 19 | /// use rcgen::string::PrintableString; 20 | /// let hello = PrintableString::try_from("hello").unwrap(); 21 | /// ``` 22 | /// 23 | /// # Supported characters 24 | /// 25 | /// PrintableString is a subset of the [ASCII printable characters]. 26 | /// For instance, `'@'` is a printable character as per ASCII but can't be part of [ASN.1's `PrintableString`]. 27 | /// 28 | /// The following ASCII characters/ranges are supported: 29 | /// 30 | /// - `A..Z` 31 | /// - `a..z` 32 | /// - `0..9` 33 | /// - "` `" (i.e. space) 34 | /// - `\` 35 | /// - `(` 36 | /// - `)` 37 | /// - `+` 38 | /// - `,` 39 | /// - `-` 40 | /// - `.` 41 | /// - `/` 42 | /// - `:` 43 | /// - `=` 44 | /// - `?` 45 | /// 46 | /// [ASCII printable characters]: https://en.wikipedia.org/wiki/ASCII#Printable_characters 47 | /// [ASN.1's `PrintableString`]: https://en.wikipedia.org/wiki/PrintableString 48 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 49 | pub struct PrintableString(String); 50 | 51 | impl PrintableString { 52 | /// Extracts a string slice containing the entire `PrintableString`. 53 | pub fn as_str(&self) -> &str { 54 | &self.0 55 | } 56 | } 57 | 58 | impl TryFrom<&str> for PrintableString { 59 | type Error = Error; 60 | 61 | /// Converts a `&str` to a [`PrintableString`]. 62 | /// 63 | /// Any character not in the [`PrintableString`] charset will be rejected. 64 | /// See [`PrintableString`] documentation for more information. 65 | /// 66 | /// The result is allocated on the heap. 67 | fn try_from(input: &str) -> Result { 68 | input.to_string().try_into() 69 | } 70 | } 71 | 72 | impl TryFrom for PrintableString { 73 | type Error = Error; 74 | 75 | /// Converts a [`String`][`std::string::String`] into a [`PrintableString`] 76 | /// 77 | /// Any character not in the [`PrintableString`] charset will be rejected. 78 | /// See [`PrintableString`] documentation for more information. 79 | /// 80 | /// This conversion does not allocate or copy memory. 81 | fn try_from(value: String) -> Result { 82 | for &c in value.as_bytes() { 83 | match c { 84 | b'A'..=b'Z' 85 | | b'a'..=b'z' 86 | | b'0'..=b'9' 87 | | b' ' 88 | | b'\'' 89 | | b'(' 90 | | b')' 91 | | b'+' 92 | | b',' 93 | | b'-' 94 | | b'.' 95 | | b'/' 96 | | b':' 97 | | b'=' 98 | | b'?' => (), 99 | _ => { 100 | return Err(Error::InvalidAsn1String( 101 | InvalidAsn1String::PrintableString(value), 102 | )) 103 | }, 104 | } 105 | } 106 | Ok(Self(value)) 107 | } 108 | } 109 | 110 | impl FromStr for PrintableString { 111 | type Err = Error; 112 | 113 | fn from_str(s: &str) -> Result { 114 | s.try_into() 115 | } 116 | } 117 | 118 | impl AsRef for PrintableString { 119 | fn as_ref(&self) -> &str { 120 | &self.0 121 | } 122 | } 123 | 124 | impl fmt::Display for PrintableString { 125 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 126 | fmt::Display::fmt(self.as_str(), f) 127 | } 128 | } 129 | 130 | impl PartialEq for PrintableString { 131 | fn eq(&self, other: &str) -> bool { 132 | self.as_str() == other 133 | } 134 | } 135 | 136 | impl PartialEq for PrintableString { 137 | fn eq(&self, other: &String) -> bool { 138 | self.as_str() == other.as_str() 139 | } 140 | } 141 | 142 | impl PartialEq<&str> for PrintableString { 143 | fn eq(&self, other: &&str) -> bool { 144 | self.as_str() == *other 145 | } 146 | } 147 | 148 | impl PartialEq<&String> for PrintableString { 149 | fn eq(&self, other: &&String) -> bool { 150 | self.as_str() == other.as_str() 151 | } 152 | } 153 | 154 | /// ASN.1 `IA5String` type. 155 | /// 156 | /// # Examples 157 | /// 158 | /// You can create a `Ia5String` from [a literal string][`&str`] with [`Ia5String::try_from`]: 159 | /// 160 | /// ``` 161 | /// use rcgen::string::Ia5String; 162 | /// let hello = Ia5String::try_from("hello").unwrap(); 163 | /// ``` 164 | /// 165 | /// # Supported characters 166 | /// 167 | /// Supports the [International Alphabet No. 5 (IA5)] character encoding, i.e. 168 | /// the 128 characters of the ASCII alphabet. (Note: IA5 is now 169 | /// technically known as the International Reference Alphabet or IRA as 170 | /// specified in the ITU-T's T.50 recommendation). 171 | /// 172 | /// For UTF-8, use [`String`][`std::string::String`]. 173 | /// 174 | /// [International Alphabet No. 5 (IA5)]: https://en.wikipedia.org/wiki/T.50_(standard) 175 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 176 | pub struct Ia5String(String); 177 | 178 | impl Ia5String { 179 | /// Extracts a string slice containing the entire `Ia5String`. 180 | pub fn as_str(&self) -> &str { 181 | &self.0 182 | } 183 | } 184 | 185 | impl TryFrom<&str> for Ia5String { 186 | type Error = Error; 187 | 188 | /// Converts a `&str` to a [`Ia5String`]. 189 | /// 190 | /// Any character not in the [`Ia5String`] charset will be rejected. 191 | /// See [`Ia5String`] documentation for more information. 192 | /// 193 | /// The result is allocated on the heap. 194 | fn try_from(input: &str) -> Result { 195 | input.to_string().try_into() 196 | } 197 | } 198 | 199 | impl TryFrom for Ia5String { 200 | type Error = Error; 201 | 202 | /// Converts a [`String`][`std::string::String`] into a [`Ia5String`] 203 | /// 204 | /// Any character not in the [`Ia5String`] charset will be rejected. 205 | /// See [`Ia5String`] documentation for more information. 206 | fn try_from(input: String) -> Result { 207 | if !input.is_ascii() { 208 | return Err(Error::InvalidAsn1String(InvalidAsn1String::Ia5String( 209 | input, 210 | ))); 211 | } 212 | Ok(Self(input)) 213 | } 214 | } 215 | 216 | impl FromStr for Ia5String { 217 | type Err = Error; 218 | 219 | fn from_str(s: &str) -> Result { 220 | s.try_into() 221 | } 222 | } 223 | 224 | impl AsRef for Ia5String { 225 | fn as_ref(&self) -> &str { 226 | &self.0 227 | } 228 | } 229 | 230 | impl fmt::Display for Ia5String { 231 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 232 | fmt::Display::fmt(self.as_str(), f) 233 | } 234 | } 235 | 236 | impl PartialEq for Ia5String { 237 | fn eq(&self, other: &str) -> bool { 238 | self.as_str() == other 239 | } 240 | } 241 | 242 | impl PartialEq for Ia5String { 243 | fn eq(&self, other: &String) -> bool { 244 | self.as_str() == other.as_str() 245 | } 246 | } 247 | 248 | impl PartialEq<&str> for Ia5String { 249 | fn eq(&self, other: &&str) -> bool { 250 | self.as_str() == *other 251 | } 252 | } 253 | 254 | impl PartialEq<&String> for Ia5String { 255 | fn eq(&self, other: &&String) -> bool { 256 | self.as_str() == other.as_str() 257 | } 258 | } 259 | 260 | /// ASN.1 `TeletexString` type. 261 | /// 262 | /// # Examples 263 | /// 264 | /// You can create a `TeletexString` from [a literal string][`&str`] with [`TeletexString::try_from`]: 265 | /// 266 | /// ``` 267 | /// use rcgen::string::TeletexString; 268 | /// let hello = TeletexString::try_from("hello").unwrap(); 269 | /// ``` 270 | /// 271 | /// # Supported characters 272 | /// 273 | /// The standard defines a complex character set allowed in this type. However, quoting the ASN.1 274 | /// [mailing list], "a sizable volume of software in the world treats TeletexString (T61String) as a 275 | /// simple 8-bit string with mostly Windows Latin 1 (superset of iso-8859-1) encoding". 276 | /// 277 | /// `TeletexString` is included for backward compatibility, [RFC 5280] say it 278 | /// SHOULD NOT be used for certificates for new subjects. 279 | /// 280 | /// [mailing list]: https://www.mail-archive.com/asn1@asn1.org/msg00460.html 281 | /// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25 282 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 283 | pub struct TeletexString(String); 284 | 285 | impl TeletexString { 286 | /// Extracts a string slice containing the entire `TeletexString`. 287 | pub fn as_str(&self) -> &str { 288 | &self.0 289 | } 290 | 291 | /// Returns a byte slice of this `TeletexString`’s contents. 292 | pub fn as_bytes(&self) -> &[u8] { 293 | self.0.as_bytes() 294 | } 295 | } 296 | 297 | impl TryFrom<&str> for TeletexString { 298 | type Error = Error; 299 | 300 | /// Converts a `&str` to a [`TeletexString`]. 301 | /// 302 | /// Any character not in the [`TeletexString`] charset will be rejected. 303 | /// See [`TeletexString`] documentation for more information. 304 | /// 305 | /// The result is allocated on the heap. 306 | fn try_from(input: &str) -> Result { 307 | input.to_string().try_into() 308 | } 309 | } 310 | 311 | impl TryFrom for TeletexString { 312 | type Error = Error; 313 | 314 | /// Converts a [`String`][`std::string::String`] into a [`TeletexString`] 315 | /// 316 | /// Any character not in the [`TeletexString`] charset will be rejected. 317 | /// See [`TeletexString`] documentation for more information. 318 | /// 319 | /// This conversion does not allocate or copy memory. 320 | fn try_from(input: String) -> Result { 321 | // Check all bytes are visible 322 | if !input.as_bytes().iter().all(|b| (0x20..=0x7f).contains(b)) { 323 | return Err(Error::InvalidAsn1String(InvalidAsn1String::TeletexString( 324 | input, 325 | ))); 326 | } 327 | Ok(Self(input)) 328 | } 329 | } 330 | 331 | impl FromStr for TeletexString { 332 | type Err = Error; 333 | 334 | fn from_str(s: &str) -> Result { 335 | s.try_into() 336 | } 337 | } 338 | 339 | impl AsRef for TeletexString { 340 | fn as_ref(&self) -> &str { 341 | &self.0 342 | } 343 | } 344 | 345 | impl fmt::Display for TeletexString { 346 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 347 | fmt::Display::fmt(self.as_str(), f) 348 | } 349 | } 350 | 351 | impl PartialEq for TeletexString { 352 | fn eq(&self, other: &str) -> bool { 353 | self.as_str() == other 354 | } 355 | } 356 | 357 | impl PartialEq for TeletexString { 358 | fn eq(&self, other: &String) -> bool { 359 | self.as_str() == other.as_str() 360 | } 361 | } 362 | 363 | impl PartialEq<&str> for TeletexString { 364 | fn eq(&self, other: &&str) -> bool { 365 | self.as_str() == *other 366 | } 367 | } 368 | 369 | impl PartialEq<&String> for TeletexString { 370 | fn eq(&self, other: &&String) -> bool { 371 | self.as_str() == other.as_str() 372 | } 373 | } 374 | 375 | /// ASN.1 `BMPString` type. 376 | /// 377 | /// # Examples 378 | /// 379 | /// You can create a `BmpString` from [a literal string][`&str`] with [`BmpString::try_from`]: 380 | /// 381 | /// ``` 382 | /// use rcgen::string::BmpString; 383 | /// let hello = BmpString::try_from("hello").unwrap(); 384 | /// ``` 385 | /// 386 | /// # Supported characters 387 | /// 388 | /// Encodes Basic Multilingual Plane (BMP) subset of Unicode (ISO 10646), 389 | /// a.k.a. UCS-2. 390 | /// 391 | /// Bytes are encoded as UTF-16 big-endian. 392 | /// 393 | /// `BMPString` is included for backward compatibility, [RFC 5280] say it 394 | /// SHOULD NOT be used for certificates for new subjects. 395 | /// 396 | /// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25 397 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 398 | pub struct BmpString(Vec); 399 | 400 | impl BmpString { 401 | /// Returns a byte slice of this `BmpString`'s contents. 402 | /// 403 | /// The inverse of this method is [`from_utf16be`]. 404 | /// 405 | /// [`from_utf16be`]: BmpString::from_utf16be 406 | /// 407 | /// # Examples 408 | /// 409 | /// ``` 410 | /// use rcgen::string::BmpString; 411 | /// let s = BmpString::try_from("hello").unwrap(); 412 | /// 413 | /// assert_eq!(&[0, 104, 0, 101, 0, 108, 0, 108, 0, 111], s.as_bytes()); 414 | /// ``` 415 | pub fn as_bytes(&self) -> &[u8] { 416 | &self.0 417 | } 418 | 419 | /// Decode a UTF-16BE–encoded vector `vec` into a `BmpString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data. 420 | pub fn from_utf16be(vec: Vec) -> Result { 421 | if vec.len() % 2 != 0 { 422 | return Err(Error::InvalidAsn1String(InvalidAsn1String::BmpString( 423 | "Invalid UTF-16 encoding".to_string(), 424 | ))); 425 | } 426 | 427 | // FIXME: Update this when `array_chunks` is stabilized. 428 | for maybe_char in char::decode_utf16( 429 | vec.chunks_exact(2) 430 | .map(|chunk| u16::from_be_bytes([chunk[0], chunk[1]])), 431 | ) { 432 | // We check we only use the BMP subset of Unicode (the first 65 536 code points) 433 | match maybe_char { 434 | // Character is in the Basic Multilingual Plane 435 | Ok(c) if (c as u64) < u64::from(u16::MAX) => (), 436 | // Characters outside Basic Multilingual Plane or unpaired surrogates 437 | _ => { 438 | return Err(Error::InvalidAsn1String(InvalidAsn1String::BmpString( 439 | "Invalid UTF-16 encoding".to_string(), 440 | ))); 441 | }, 442 | } 443 | } 444 | Ok(Self(vec.to_vec())) 445 | } 446 | } 447 | 448 | impl TryFrom<&str> for BmpString { 449 | type Error = Error; 450 | 451 | /// Converts a `&str` to a [`BmpString`]. 452 | /// 453 | /// Any character not in the [`BmpString`] charset will be rejected. 454 | /// See [`BmpString`] documentation for more information. 455 | /// 456 | /// The result is allocated on the heap. 457 | fn try_from(value: &str) -> Result { 458 | let capacity = value.len().checked_mul(2).ok_or_else(|| { 459 | Error::InvalidAsn1String(InvalidAsn1String::BmpString(value.to_string())) 460 | })?; 461 | 462 | let mut bytes = Vec::with_capacity(capacity); 463 | 464 | for code_point in value.encode_utf16() { 465 | bytes.extend(code_point.to_be_bytes()); 466 | } 467 | 468 | BmpString::from_utf16be(bytes) 469 | } 470 | } 471 | 472 | impl TryFrom for BmpString { 473 | type Error = Error; 474 | 475 | /// Converts a [`String`][`std::string::String`] into a [`BmpString`] 476 | /// 477 | /// Any character not in the [`BmpString`] charset will be rejected. 478 | /// See [`BmpString`] documentation for more information. 479 | /// 480 | /// Parsing a `BmpString` allocates memory since the UTF-8 to UTF-16 conversion requires a memory allocation. 481 | fn try_from(value: String) -> Result { 482 | value.as_str().try_into() 483 | } 484 | } 485 | 486 | impl FromStr for BmpString { 487 | type Err = Error; 488 | 489 | fn from_str(s: &str) -> Result { 490 | s.try_into() 491 | } 492 | } 493 | 494 | /// ASN.1 `UniversalString` type. 495 | /// 496 | /// # Examples 497 | /// 498 | /// You can create a `UniversalString` from [a literal string][`&str`] with [`UniversalString::try_from`]: 499 | /// 500 | /// ``` 501 | /// use rcgen::string::UniversalString; 502 | /// let hello = UniversalString::try_from("hello").unwrap(); 503 | /// ``` 504 | /// 505 | /// # Supported characters 506 | /// 507 | /// The characters which can appear in the `UniversalString` type are any of the characters allowed by 508 | /// ISO/IEC 10646 (Unicode). 509 | /// 510 | /// Bytes are encoded like UTF-32 big-endian. 511 | /// 512 | /// `UniversalString` is included for backward compatibility, [RFC 5280] say it 513 | /// SHOULD NOT be used for certificates for new subjects. 514 | /// 515 | /// [RFC 5280]: https://datatracker.ietf.org/doc/html/rfc5280#page-25 516 | #[derive(Debug, PartialEq, Eq, Hash, Clone)] 517 | pub struct UniversalString(Vec); 518 | 519 | impl UniversalString { 520 | /// Returns a byte slice of this `UniversalString`'s contents. 521 | /// 522 | /// The inverse of this method is [`from_utf32be`]. 523 | /// 524 | /// [`from_utf32be`]: UniversalString::from_utf32be 525 | /// 526 | /// # Examples 527 | /// 528 | /// ``` 529 | /// use rcgen::string::UniversalString; 530 | /// let s = UniversalString::try_from("hello").unwrap(); 531 | /// 532 | /// assert_eq!(&[0, 0, 0, 104, 0, 0, 0, 101, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 111], s.as_bytes()); 533 | /// ``` 534 | pub fn as_bytes(&self) -> &[u8] { 535 | &self.0 536 | } 537 | 538 | /// Decode a UTF-32BE–encoded vector `vec` into a `UniversalString`, returning [Err](`std::result::Result::Err`) if `vec` contains any invalid data. 539 | pub fn from_utf32be(vec: Vec) -> Result { 540 | if vec.len() % 4 != 0 { 541 | return Err(Error::InvalidAsn1String( 542 | InvalidAsn1String::UniversalString("Invalid UTF-32 encoding".to_string()), 543 | )); 544 | } 545 | 546 | // FIXME: Update this when `array_chunks` is stabilized. 547 | for maybe_char in vec 548 | .chunks_exact(4) 549 | .map(|chunk| u32::from_be_bytes([chunk[0], chunk[1], chunk[2], chunk[3]])) 550 | { 551 | if core::char::from_u32(maybe_char).is_none() { 552 | return Err(Error::InvalidAsn1String( 553 | InvalidAsn1String::UniversalString("Invalid UTF-32 encoding".to_string()), 554 | )); 555 | } 556 | } 557 | 558 | Ok(Self(vec)) 559 | } 560 | } 561 | 562 | impl TryFrom<&str> for UniversalString { 563 | type Error = Error; 564 | 565 | /// Converts a `&str` to a [`UniversalString`]. 566 | /// 567 | /// Any character not in the [`UniversalString`] charset will be rejected. 568 | /// See [`UniversalString`] documentation for more information. 569 | /// 570 | /// The result is allocated on the heap. 571 | fn try_from(value: &str) -> Result { 572 | let capacity = value.len().checked_mul(4).ok_or_else(|| { 573 | Error::InvalidAsn1String(InvalidAsn1String::UniversalString(value.to_string())) 574 | })?; 575 | 576 | let mut bytes = Vec::with_capacity(capacity); 577 | 578 | // A `char` is any ‘Unicode code point’ other than a surrogate code point. 579 | // The code units for UTF-32 correspond exactly to Unicode code points. 580 | // (https://www.unicode.org/reports/tr19/tr19-9.html#Introduction) 581 | // So any `char` is a valid UTF-32, we just cast it to perform the convertion. 582 | for char in value.chars().map(|char| char as u32) { 583 | bytes.extend(char.to_be_bytes()) 584 | } 585 | 586 | UniversalString::from_utf32be(bytes) 587 | } 588 | } 589 | 590 | impl TryFrom for UniversalString { 591 | type Error = Error; 592 | 593 | /// Converts a [`String`][`std::string::String`] into a [`UniversalString`] 594 | /// 595 | /// Any character not in the [`UniversalString`] charset will be rejected. 596 | /// See [`UniversalString`] documentation for more information. 597 | /// 598 | /// Parsing a `UniversalString` allocates memory since the UTF-8 to UTF-32 conversion requires a memory allocation. 599 | fn try_from(value: String) -> Result { 600 | value.as_str().try_into() 601 | } 602 | } 603 | 604 | #[cfg(test)] 605 | #[allow(clippy::unwrap_used)] 606 | mod tests { 607 | 608 | use crate::{BmpString, Ia5String, PrintableString, TeletexString, UniversalString}; 609 | 610 | #[test] 611 | fn printable_string() { 612 | const EXAMPLE_UTF8: &str = "CertificateTemplate"; 613 | let printable_string = PrintableString::try_from(EXAMPLE_UTF8).unwrap(); 614 | assert_eq!(printable_string, EXAMPLE_UTF8); 615 | assert!(PrintableString::try_from("@").is_err()); 616 | assert!(PrintableString::try_from("*").is_err()); 617 | } 618 | 619 | #[test] 620 | fn ia5_string() { 621 | const EXAMPLE_UTF8: &str = "CertificateTemplate"; 622 | let ia5_string = Ia5String::try_from(EXAMPLE_UTF8).unwrap(); 623 | assert_eq!(ia5_string, EXAMPLE_UTF8); 624 | assert!(Ia5String::try_from(String::from('\u{7F}')).is_ok()); 625 | assert!(Ia5String::try_from(String::from('\u{8F}')).is_err()); 626 | } 627 | 628 | #[test] 629 | fn teletext_string() { 630 | const EXAMPLE_UTF8: &str = "CertificateTemplate"; 631 | let teletext_string = TeletexString::try_from(EXAMPLE_UTF8).unwrap(); 632 | assert_eq!(teletext_string, EXAMPLE_UTF8); 633 | assert!(Ia5String::try_from(String::from('\u{7F}')).is_ok()); 634 | assert!(Ia5String::try_from(String::from('\u{8F}')).is_err()); 635 | } 636 | 637 | #[test] 638 | fn bmp_string() { 639 | const EXPECTED_BYTES: &[u8] = &[ 640 | 0x00, 0x43, 0x00, 0x65, 0x00, 0x72, 0x00, 0x74, 0x00, 0x69, 0x00, 0x66, 0x00, 0x69, 641 | 0x00, 0x63, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 0x00, 0x54, 0x00, 0x65, 0x00, 0x6d, 642 | 0x00, 0x70, 0x00, 0x6c, 0x00, 0x61, 0x00, 0x74, 0x00, 0x65, 643 | ]; 644 | const EXAMPLE_UTF8: &str = "CertificateTemplate"; 645 | let bmp_string = BmpString::try_from(EXAMPLE_UTF8).unwrap(); 646 | assert_eq!(bmp_string.as_bytes(), EXPECTED_BYTES); 647 | assert!(BmpString::try_from(String::from('\u{FFFE}')).is_ok()); 648 | assert!(BmpString::try_from(String::from('\u{FFFF}')).is_err()); 649 | } 650 | 651 | #[test] 652 | fn universal_string() { 653 | const EXPECTED_BYTES: &[u8] = &[ 654 | 0x00, 0x00, 0x00, 0x43, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x72, 0x00, 0x00, 655 | 0x00, 0x74, 0x00, 0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x66, 0x00, 0x00, 0x00, 0x69, 656 | 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 0x00, 0x74, 0x00, 0x00, 657 | 0x00, 0x65, 0x00, 0x00, 0x00, 0x54, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6d, 658 | 0x00, 0x00, 0x00, 0x70, 0x00, 0x00, 0x00, 0x6c, 0x00, 0x00, 0x00, 0x61, 0x00, 0x00, 659 | 0x00, 0x74, 0x00, 0x00, 0x00, 0x65, 660 | ]; 661 | const EXAMPLE_UTF8: &str = "CertificateTemplate"; 662 | let universal_string = UniversalString::try_from(EXAMPLE_UTF8).unwrap(); 663 | assert_eq!(universal_string.as_bytes(), EXPECTED_BYTES); 664 | } 665 | } 666 | -------------------------------------------------------------------------------- /rcgen/tests/botan.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(feature = "crypto", feature = "x509-parser"))] 2 | 3 | use time::{Duration, OffsetDateTime}; 4 | 5 | use rcgen::{BasicConstraints, Certificate, CertificateParams, DnType, IsCa}; 6 | use rcgen::{CertificateRevocationListParams, RevocationReason, RevokedCertParams}; 7 | use rcgen::{DnValue, KeyPair}; 8 | use rcgen::{KeyUsagePurpose, SerialNumber}; 9 | 10 | mod util; 11 | 12 | fn default_params() -> (CertificateParams, KeyPair) { 13 | let (mut params, key_pair) = util::default_params(); 14 | // Botan has a sanity check that enforces a maximum expiration date 15 | params.not_after = rcgen::date_time_ymd(3016, 1, 1); 16 | (params, key_pair) 17 | } 18 | 19 | fn check_cert(cert_der: &[u8], cert: &Certificate) { 20 | println!("{}", cert.pem()); 21 | check_cert_ca(cert_der, cert, cert_der); 22 | } 23 | 24 | fn check_cert_ca(cert_der: &[u8], _cert: &Certificate, ca_der: &[u8]) { 25 | println!( 26 | "botan version: {}", 27 | botan::Version::current().unwrap().string 28 | ); 29 | let trust_anchor = botan::Certificate::load(ca_der).unwrap(); 30 | let end_entity_cert = botan::Certificate::load(cert_der).unwrap(); 31 | 32 | // Set time to Jan 10, 2004 33 | const REFERENCE_TIME: Option = Some(0x40_00_00_00); 34 | 35 | // Verify the certificate 36 | end_entity_cert 37 | .verify( 38 | &[], 39 | &[&trust_anchor], 40 | None, 41 | Some("crabs.crabs"), 42 | REFERENCE_TIME, 43 | ) 44 | .unwrap(); 45 | 46 | // TODO perform a full handshake 47 | } 48 | 49 | #[test] 50 | fn test_botan() { 51 | let (params, key_pair) = default_params(); 52 | let cert = params.self_signed(&key_pair).unwrap(); 53 | 54 | // Now verify the certificate. 55 | check_cert(cert.der(), &cert); 56 | } 57 | 58 | #[test] 59 | fn test_botan_256() { 60 | let (params, _) = default_params(); 61 | let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap(); 62 | let cert = params.self_signed(&key_pair).unwrap(); 63 | 64 | // Now verify the certificate. 65 | check_cert(cert.der(), &cert); 66 | } 67 | 68 | #[test] 69 | fn test_botan_384() { 70 | let (params, _) = default_params(); 71 | let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P384_SHA384).unwrap(); 72 | let cert = params.self_signed(&key_pair).unwrap(); 73 | 74 | // Now verify the certificate. 75 | check_cert(cert.der(), &cert); 76 | } 77 | 78 | #[test] 79 | #[cfg(feature = "aws_lc_rs")] 80 | fn test_botan_521() { 81 | let (params, _) = default_params(); 82 | let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P521_SHA512).unwrap(); 83 | let cert = params.self_signed(&key_pair).unwrap(); 84 | 85 | // Now verify the certificate. 86 | check_cert(cert.der(), &cert); 87 | } 88 | 89 | #[test] 90 | fn test_botan_25519() { 91 | let (params, _) = default_params(); 92 | let key_pair = KeyPair::generate_for(&rcgen::PKCS_ED25519).unwrap(); 93 | let cert = params.self_signed(&key_pair).unwrap(); 94 | 95 | // Now verify the certificate. 96 | check_cert(cert.der(), &cert); 97 | } 98 | 99 | #[test] 100 | fn test_botan_25519_v1_given() { 101 | let (params, _) = default_params(); 102 | let key_pair = KeyPair::from_pem(util::ED25519_TEST_KEY_PAIR_PEM_V1).unwrap(); 103 | let cert = params.self_signed(&key_pair).unwrap(); 104 | 105 | // Now verify the certificate. 106 | check_cert(cert.der(), &cert); 107 | } 108 | 109 | #[test] 110 | fn test_botan_25519_v2_given() { 111 | let (params, _) = default_params(); 112 | let key_pair = KeyPair::from_pem(util::ED25519_TEST_KEY_PAIR_PEM_V2).unwrap(); 113 | let cert = params.self_signed(&key_pair).unwrap(); 114 | 115 | // Now verify the certificate. 116 | check_cert(cert.der(), &cert); 117 | } 118 | 119 | #[test] 120 | fn test_botan_rsa_given() { 121 | let (params, _) = default_params(); 122 | let key_pair = KeyPair::from_pem(util::RSA_TEST_KEY_PAIR_PEM).unwrap(); 123 | let cert = params.self_signed(&key_pair).unwrap(); 124 | 125 | // Now verify the certificate. 126 | check_cert(cert.der(), &cert); 127 | } 128 | 129 | #[test] 130 | fn test_botan_separate_ca() { 131 | let (mut ca_params, ca_key) = default_params(); 132 | ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 133 | let ca_cert = ca_params.self_signed(&ca_key).unwrap(); 134 | 135 | let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); 136 | params 137 | .distinguished_name 138 | .push(DnType::OrganizationName, "Crab widgits SE"); 139 | params 140 | .distinguished_name 141 | .push(DnType::CommonName, "Dev domain"); 142 | // Botan has a sanity check that enforces a maximum expiration date 143 | params.not_after = rcgen::date_time_ymd(3016, 1, 1); 144 | 145 | let key_pair = KeyPair::generate().unwrap(); 146 | let cert = params.signed_by(&key_pair, &ca_params, &ca_key).unwrap(); 147 | check_cert_ca(cert.der(), &cert, ca_cert.der()); 148 | } 149 | 150 | #[cfg(feature = "x509-parser")] 151 | #[test] 152 | fn test_botan_imported_ca() { 153 | let (mut params, ca_key) = default_params(); 154 | params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 155 | let ca_cert = params.self_signed(&ca_key).unwrap(); 156 | 157 | let ca_cert_der = ca_cert.der(); 158 | 159 | let imported_ca_cert_params = CertificateParams::from_ca_cert_der(ca_cert_der).unwrap(); 160 | 161 | let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); 162 | params 163 | .distinguished_name 164 | .push(DnType::OrganizationName, "Crab widgits SE"); 165 | params 166 | .distinguished_name 167 | .push(DnType::CommonName, "Dev domain"); 168 | // Botan has a sanity check that enforces a maximum expiration date 169 | params.not_after = rcgen::date_time_ymd(3016, 1, 1); 170 | 171 | let key_pair = KeyPair::generate().unwrap(); 172 | let cert = params 173 | .signed_by(&key_pair, &imported_ca_cert_params, &ca_key) 174 | .unwrap(); 175 | check_cert_ca(cert.der(), &cert, ca_cert_der); 176 | } 177 | 178 | #[cfg(feature = "x509-parser")] 179 | #[test] 180 | fn test_botan_imported_ca_with_printable_string() { 181 | let (mut params, imported_ca_key) = default_params(); 182 | params.distinguished_name.push( 183 | DnType::CountryName, 184 | DnValue::PrintableString("US".try_into().unwrap()), 185 | ); 186 | params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 187 | let ca_cert = params.self_signed(&imported_ca_key).unwrap(); 188 | 189 | let ca_cert_der = ca_cert.der(); 190 | 191 | let imported_ca_cert_params = CertificateParams::from_ca_cert_der(ca_cert_der).unwrap(); 192 | 193 | let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); 194 | params 195 | .distinguished_name 196 | .push(DnType::OrganizationName, "Crab widgits SE"); 197 | params 198 | .distinguished_name 199 | .push(DnType::CommonName, "Dev domain"); 200 | // Botan has a sanity check that enforces a maximum expiration date 201 | params.not_after = rcgen::date_time_ymd(3016, 1, 1); 202 | let key_pair = KeyPair::generate().unwrap(); 203 | let cert = params 204 | .signed_by(&key_pair, &imported_ca_cert_params, &imported_ca_key) 205 | .unwrap(); 206 | 207 | check_cert_ca(cert.der(), &cert, ca_cert_der); 208 | } 209 | 210 | #[test] 211 | fn test_botan_crl_parse() { 212 | // Create an issuer CA. 213 | let alg = &rcgen::PKCS_ECDSA_P256_SHA256; 214 | let (mut issuer, _) = util::default_params(); 215 | issuer.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 216 | issuer.key_usages = vec![ 217 | KeyUsagePurpose::KeyCertSign, 218 | KeyUsagePurpose::DigitalSignature, 219 | KeyUsagePurpose::CrlSign, 220 | ]; 221 | let issuer_key = KeyPair::generate_for(alg).unwrap(); 222 | 223 | // Create an end entity cert issued by the issuer. 224 | let (mut ee, _) = util::default_params(); 225 | ee.is_ca = IsCa::NoCa; 226 | ee.serial_number = Some(SerialNumber::from(99999)); 227 | // Botan has a sanity check that enforces a maximum expiration date 228 | ee.not_after = rcgen::date_time_ymd(3016, 1, 1); 229 | let ee_key = KeyPair::generate_for(alg).unwrap(); 230 | let ee_cert = ee.signed_by(&ee_key, &issuer, &issuer_key).unwrap(); 231 | let botan_ee = botan::Certificate::load(ee_cert.der()).unwrap(); 232 | 233 | // Generate a CRL with the issuer that revokes the EE cert. 234 | let now = OffsetDateTime::now_utc(); 235 | let crl = CertificateRevocationListParams { 236 | this_update: now, 237 | next_update: now + Duration::weeks(1), 238 | crl_number: rcgen::SerialNumber::from(1234), 239 | issuing_distribution_point: None, 240 | revoked_certs: vec![RevokedCertParams { 241 | serial_number: ee.serial_number.clone().unwrap(), 242 | revocation_time: now, 243 | reason_code: Some(RevocationReason::KeyCompromise), 244 | invalidity_date: None, 245 | }], 246 | key_identifier_method: rcgen::KeyIdMethod::Sha256, 247 | }; 248 | 249 | let crl = crl.signed_by(&issuer, &issuer_key).unwrap(); 250 | 251 | // We should be able to load the CRL in both serializations. 252 | botan::CRL::load(crl.pem().unwrap().as_ref()).unwrap(); 253 | let crl = botan::CRL::load(crl.der()).unwrap(); 254 | 255 | // We should find the EE cert revoked. 256 | assert!(crl.is_revoked(&botan_ee).unwrap()); 257 | } 258 | -------------------------------------------------------------------------------- /rcgen/tests/generic.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "crypto")] 2 | 3 | mod util; 4 | 5 | #[cfg(feature = "pem")] 6 | mod test_key_params_mismatch { 7 | use std::collections::hash_map::DefaultHasher; 8 | use std::hash::{Hash, Hasher}; 9 | 10 | fn generate_hash(subject: &T) -> u64 { 11 | let mut hasher = DefaultHasher::new(); 12 | subject.hash(&mut hasher); 13 | hasher.finish() 14 | } 15 | 16 | #[test] 17 | fn test_key_params_mismatch() { 18 | let available_key_params = [ 19 | &rcgen::PKCS_RSA_SHA256, 20 | &rcgen::PKCS_ECDSA_P256_SHA256, 21 | &rcgen::PKCS_ECDSA_P384_SHA384, 22 | #[cfg(feature = "aws_lc_rs")] 23 | &rcgen::PKCS_ECDSA_P521_SHA512, 24 | &rcgen::PKCS_ED25519, 25 | ]; 26 | for (i, kalg_1) in available_key_params.iter().enumerate() { 27 | for (j, kalg_2) in available_key_params.iter().enumerate() { 28 | if i == j { 29 | assert_eq!(*kalg_1, *kalg_2); 30 | assert_eq!(generate_hash(*kalg_1), generate_hash(*kalg_2)); 31 | continue; 32 | } 33 | 34 | assert_ne!(*kalg_1, *kalg_2); 35 | assert_ne!(generate_hash(*kalg_1), generate_hash(*kalg_2)); 36 | } 37 | } 38 | } 39 | } 40 | 41 | #[cfg(feature = "x509-parser")] 42 | mod test_convert_x509_subject_alternative_name { 43 | use rcgen::{BasicConstraints, CertificateParams, IsCa, SanType}; 44 | use std::net::{IpAddr, Ipv4Addr}; 45 | 46 | #[test] 47 | fn converts_from_ip() { 48 | let ip = Ipv4Addr::new(2, 4, 6, 8); 49 | let ip_san = SanType::IpAddress(IpAddr::V4(ip)); 50 | 51 | let (mut params, ca_key) = super::util::default_params(); 52 | 53 | // Add the SAN we want to test the parsing for 54 | params.subject_alt_names.push(ip_san.clone()); 55 | 56 | // Because we're using a function for CA certificates 57 | params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 58 | 59 | let cert = params.self_signed(&ca_key).unwrap(); 60 | 61 | // Serialize our cert that has our chosen san, so we can testing parsing/deserializing it. 62 | let ca_der = cert.der(); 63 | 64 | let actual = CertificateParams::from_ca_cert_der(ca_der).unwrap(); 65 | assert!(actual.subject_alt_names.contains(&ip_san)); 66 | } 67 | } 68 | 69 | #[cfg(feature = "x509-parser")] 70 | mod test_x509_custom_ext { 71 | use crate::util; 72 | 73 | use rcgen::CustomExtension; 74 | use x509_parser::oid_registry::asn1_rs; 75 | use x509_parser::prelude::{ 76 | FromDer, ParsedCriAttribute, X509Certificate, X509CertificationRequest, 77 | }; 78 | 79 | #[test] 80 | fn custom_ext() { 81 | // Create an imaginary critical custom extension for testing. 82 | let test_oid = asn1_rs::Oid::from(&[2, 5, 29, 999999]).unwrap(); 83 | let test_ext = yasna::construct_der(|writer| { 84 | writer.write_utf8_string("🦀 greetz to ferris 🦀"); 85 | }); 86 | let mut custom_ext = CustomExtension::from_oid_content( 87 | test_oid.iter().unwrap().collect::>().as_slice(), 88 | test_ext.clone(), 89 | ); 90 | custom_ext.set_criticality(true); 91 | 92 | // Generate a certificate with the custom extension, parse it with x509-parser. 93 | let (mut params, test_key) = util::default_params(); 94 | params.custom_extensions = vec![custom_ext]; 95 | // Ensure the custom exts. being omitted into a CSR doesn't require SAN ext being present. 96 | // See https://github.com/rustls/rcgen/issues/122 97 | params.subject_alt_names = Vec::default(); 98 | let test_cert = params.self_signed(&test_key).unwrap(); 99 | let (_, x509_test_cert) = X509Certificate::from_der(test_cert.der()).unwrap(); 100 | 101 | // We should be able to find the extension by OID, with expected criticality and value. 102 | let favorite_drink_ext = x509_test_cert 103 | .get_extension_unique(&test_oid) 104 | .expect("invalid extensions") 105 | .expect("missing custom extension"); 106 | assert!(favorite_drink_ext.critical); 107 | assert_eq!(favorite_drink_ext.value, test_ext); 108 | 109 | // Generate a CSR with the custom extension, parse it with x509-parser. 110 | let test_cert_csr = params.serialize_request(&test_key).unwrap(); 111 | let (_, x509_csr) = X509CertificationRequest::from_der(test_cert_csr.der()).unwrap(); 112 | 113 | // We should find that the CSR contains requested extensions. 114 | // Note: we can't use `x509_csr.requested_extensions()` here because it maps the raw extension 115 | // request extensions to their parsed form, and of course x509-parser doesn't parse our custom extension. 116 | let exts = x509_csr 117 | .certification_request_info 118 | .iter_attributes() 119 | .find_map(|attr| { 120 | if let ParsedCriAttribute::ExtensionRequest(requested) = &attr.parsed_attribute() { 121 | Some(requested.extensions.iter().collect::>()) 122 | } else { 123 | None 124 | } 125 | }) 126 | .expect("missing requested extensions"); 127 | 128 | // We should find the custom extension with expected criticality and value. 129 | let custom_ext = exts 130 | .iter() 131 | .find(|ext| ext.oid == test_oid) 132 | .expect("missing requested custom extension"); 133 | assert!(custom_ext.critical); 134 | assert_eq!(custom_ext.value, test_ext); 135 | } 136 | } 137 | 138 | #[cfg(feature = "x509-parser")] 139 | mod test_csr_custom_attributes { 140 | use rcgen::{Attribute, CertificateParams, KeyPair}; 141 | use x509_parser::{ 142 | der_parser::Oid, 143 | prelude::{FromDer, X509CertificationRequest}, 144 | }; 145 | 146 | /// Test serializing a CSR with custom attributes. 147 | /// This test case uses `challengePassword` from [RFC 2985], a simple 148 | /// ATTRIBUTE that contains a single UTF8String. 149 | /// 150 | /// [RFC 2985]: 151 | #[test] 152 | fn test_csr_custom_attributes() { 153 | // OID for challengePassword 154 | const CHALLENGE_PWD_OID: &[u64] = &[1, 2, 840, 113549, 1, 9, 7]; 155 | 156 | // Attribute values for challengePassword 157 | let challenge_pwd_values = yasna::try_construct_der::<_, ()>(|writer| { 158 | // Reminder: CSR attribute values are contained in a SET 159 | writer.write_set(|writer| { 160 | // Challenge passwords only have one value, a UTF8String 161 | writer 162 | .next() 163 | .write_utf8_string("nobody uses challenge passwords anymore"); 164 | Ok(()) 165 | }) 166 | }) 167 | .unwrap(); 168 | 169 | // Challenge password attribute 170 | let challenge_password_attribute = Attribute { 171 | oid: CHALLENGE_PWD_OID, 172 | values: challenge_pwd_values.clone(), 173 | }; 174 | 175 | // Serialize a DER-encoded CSR 176 | let params = CertificateParams::default(); 177 | let key_pair = KeyPair::generate().unwrap(); 178 | let csr = params 179 | .serialize_request_with_attributes(&key_pair, vec![challenge_password_attribute]) 180 | .unwrap(); 181 | 182 | // Parse the CSR 183 | let (_, x509_csr) = X509CertificationRequest::from_der(csr.der()).unwrap(); 184 | let parsed_attribute_value = x509_csr 185 | .certification_request_info 186 | .attributes_map() 187 | .unwrap() 188 | .get(&Oid::from(CHALLENGE_PWD_OID).unwrap()) 189 | .unwrap() 190 | .value; 191 | assert_eq!(parsed_attribute_value, challenge_pwd_values); 192 | } 193 | } 194 | 195 | #[cfg(feature = "x509-parser")] 196 | mod test_x509_parser_crl { 197 | use crate::util; 198 | use x509_parser::extensions::{DistributionPointName, ParsedExtension}; 199 | use x509_parser::num_bigint::BigUint; 200 | use x509_parser::prelude::{FromDer, GeneralName, IssuingDistributionPoint, X509Certificate}; 201 | use x509_parser::revocation_list::CertificateRevocationList; 202 | use x509_parser::x509::X509Version; 203 | 204 | #[test] 205 | fn parse_crl() { 206 | // Create a CRL with one revoked cert, and an issuer to sign the CRL. 207 | let (crl_params, crl, issuer) = util::test_crl(); 208 | let revoked_cert = crl_params.revoked_certs.first().unwrap(); 209 | let revoked_cert_serial = BigUint::from_bytes_be(revoked_cert.serial_number.as_ref()); 210 | let (_, x509_issuer) = X509Certificate::from_der(issuer.der()).unwrap(); 211 | 212 | // We should be able to parse the CRL with x509-parser without error. 213 | let (_, x509_crl) = 214 | CertificateRevocationList::from_der(crl.der()).expect("failed to parse CRL DER"); 215 | 216 | // The properties of the CRL should match expected. 217 | assert_eq!(x509_crl.version().unwrap(), X509Version(1)); 218 | assert_eq!(x509_crl.issuer(), x509_issuer.subject()); 219 | assert_eq!( 220 | x509_crl.last_update().to_datetime().unix_timestamp(), 221 | crl_params.this_update.unix_timestamp() 222 | ); 223 | assert_eq!( 224 | x509_crl 225 | .next_update() 226 | .unwrap() 227 | .to_datetime() 228 | .unix_timestamp(), 229 | crl_params.next_update.unix_timestamp() 230 | ); 231 | let crl_number = BigUint::from_bytes_be(crl_params.crl_number.as_ref()); 232 | assert_eq!(x509_crl.crl_number().unwrap(), &crl_number); 233 | 234 | // We should find the expected revoked certificate serial with the correct reason code. 235 | let x509_revoked_cert = x509_crl 236 | .iter_revoked_certificates() 237 | .next() 238 | .expect("failed to find revoked cert in CRL"); 239 | assert_eq!(x509_revoked_cert.user_certificate, revoked_cert_serial); 240 | let (_, reason_code) = x509_revoked_cert.reason_code().unwrap(); 241 | assert_eq!(reason_code.0, revoked_cert.reason_code.unwrap() as u8); 242 | 243 | // The issuing distribution point extension should be present and marked critical. 244 | let issuing_dp_ext = x509_crl 245 | .extensions() 246 | .iter() 247 | .find(|ext| { 248 | ext.oid == x509_parser::oid_registry::OID_X509_EXT_ISSUER_DISTRIBUTION_POINT 249 | }) 250 | .expect("failed to find issuing distribution point extension"); 251 | assert!(issuing_dp_ext.critical); 252 | 253 | // The parsed issuing distribution point extension should match expected. 254 | let ParsedExtension::IssuingDistributionPoint(idp) = issuing_dp_ext.parsed_extension() 255 | else { 256 | panic!("missing parsed CRL IDP ext"); 257 | }; 258 | assert_eq!( 259 | idp, 260 | &IssuingDistributionPoint { 261 | only_contains_user_certs: true, 262 | only_contains_ca_certs: false, 263 | only_contains_attribute_certs: false, 264 | indirect_crl: false, 265 | only_some_reasons: None, 266 | distribution_point: Some(DistributionPointName::FullName(vec![GeneralName::URI( 267 | "http://example.com/crl", 268 | )])), 269 | } 270 | ); 271 | 272 | // We should be able to verify the CRL signature with the issuer. 273 | assert!(x509_crl.verify_signature(x509_issuer.public_key()).is_ok()); 274 | } 275 | } 276 | 277 | #[cfg(feature = "x509-parser")] 278 | mod test_parse_crl_dps { 279 | use crate::util; 280 | use x509_parser::extensions::{DistributionPointName, ParsedExtension}; 281 | 282 | #[test] 283 | fn parse_crl_dps() { 284 | // Generate and parse a certificate that includes two CRL distribution points. 285 | let der = util::cert_with_crl_dps(); 286 | let (_, parsed_cert) = x509_parser::parse_x509_certificate(&der).unwrap(); 287 | 288 | // We should find a CRL DP extension was parsed. 289 | let crl_dps = parsed_cert 290 | .get_extension_unique(&x509_parser::oid_registry::OID_X509_EXT_CRL_DISTRIBUTION_POINTS) 291 | .expect("malformed CRL distribution points extension") 292 | .expect("missing CRL distribution points extension"); 293 | 294 | // The extension should not be critical. 295 | assert!(!crl_dps.critical); 296 | 297 | // We should be able to parse the definition. 298 | let crl_dps = match crl_dps.parsed_extension() { 299 | ParsedExtension::CRLDistributionPoints(crl_dps) => crl_dps, 300 | _ => panic!("unexpected parsed extension type"), 301 | }; 302 | 303 | // There should be two DPs. 304 | assert_eq!(crl_dps.points.len(), 2); 305 | 306 | // Each distribution point should only include a distribution point name holding a sequence 307 | // of general names. 308 | let general_names = crl_dps 309 | .points 310 | .iter() 311 | .flat_map(|dp| { 312 | // We shouldn't find a cRLIssuer or onlySomeReasons field. 313 | assert!(dp.crl_issuer.is_none()); 314 | assert!(dp.reasons.is_none()); 315 | 316 | match dp 317 | .distribution_point 318 | .as_ref() 319 | .expect("missing distribution point name") 320 | { 321 | DistributionPointName::FullName(general_names) => general_names.iter(), 322 | DistributionPointName::NameRelativeToCRLIssuer(_) => { 323 | panic!("unexpected name relative to cRL issuer") 324 | }, 325 | } 326 | }) 327 | .collect::>(); 328 | 329 | // All of the general names should be URIs. 330 | let uris = general_names 331 | .iter() 332 | .map(|general_name| match general_name { 333 | x509_parser::extensions::GeneralName::URI(uri) => *uri, 334 | _ => panic!("unexpected general name type"), 335 | }) 336 | .collect::>(); 337 | 338 | // We should find the expected URIs. 339 | assert_eq!( 340 | uris, 341 | &[ 342 | "http://example.com/crl.der", 343 | "http://crls.example.com/1234", 344 | "ldap://example.com/crl.der" 345 | ] 346 | ); 347 | } 348 | } 349 | 350 | #[cfg(feature = "x509-parser")] 351 | mod test_parse_ia5string_subject { 352 | use crate::util; 353 | use rcgen::DnType::CustomDnType; 354 | use rcgen::{CertificateParams, DistinguishedName, DnValue}; 355 | 356 | #[test] 357 | fn parse_ia5string_subject() { 358 | // Create and serialize a certificate with a subject containing an IA5String email address. 359 | let email_address_dn_type = CustomDnType(vec![1, 2, 840, 113549, 1, 9, 1]); // id-emailAddress 360 | let email_address_dn_value = DnValue::Ia5String("foo@bar.com".try_into().unwrap()); 361 | let (mut params, key_pair) = util::default_params(); 362 | params.distinguished_name = DistinguishedName::new(); 363 | params.distinguished_name.push( 364 | email_address_dn_type.clone(), 365 | email_address_dn_value.clone(), 366 | ); 367 | let cert = params.self_signed(&key_pair).unwrap(); 368 | let cert_der = cert.der(); 369 | 370 | // We should be able to parse the certificate with x509-parser. 371 | assert!(x509_parser::parse_x509_certificate(cert_der).is_ok()); 372 | 373 | // We should be able to reconstitute params from the DER using x509-parser. 374 | let params_from_cert = CertificateParams::from_ca_cert_der(cert_der).unwrap(); 375 | 376 | // We should find the expected distinguished name in the reconstituted params. 377 | let expected_names = &[(&email_address_dn_type, &email_address_dn_value)]; 378 | let names = params_from_cert 379 | .distinguished_name 380 | .iter() 381 | .collect::>(); 382 | assert_eq!(names, expected_names); 383 | } 384 | } 385 | 386 | #[cfg(feature = "x509-parser")] 387 | mod test_parse_other_name_alt_name { 388 | use rcgen::{CertificateParams, KeyPair, SanType}; 389 | 390 | #[test] 391 | fn parse_other_name_alt_name() { 392 | // Create and serialize a certificate with an alternative name containing an "OtherName". 393 | let mut params = CertificateParams::default(); 394 | let other_name = SanType::OtherName((vec![1, 2, 3, 4], "Foo".into())); 395 | params.subject_alt_names.push(other_name.clone()); 396 | let key_pair = KeyPair::generate().unwrap(); 397 | 398 | let cert = params.self_signed(&key_pair).unwrap(); 399 | 400 | let cert_der = cert.der(); 401 | 402 | // We should be able to parse the certificate with x509-parser. 403 | assert!(x509_parser::parse_x509_certificate(cert_der).is_ok()); 404 | 405 | // We should be able to reconstitute params from the DER using x509-parser. 406 | let params_from_cert = CertificateParams::from_ca_cert_der(cert_der).unwrap(); 407 | 408 | // We should find the expected distinguished name in the reconstituted params. 409 | let expected_alt_names = &[&other_name]; 410 | let subject_alt_names = params_from_cert 411 | .subject_alt_names 412 | .iter() 413 | .collect::>(); 414 | assert_eq!(subject_alt_names, expected_alt_names); 415 | } 416 | } 417 | 418 | #[cfg(feature = "x509-parser")] 419 | mod test_csr_extension_request { 420 | use rcgen::{CertificateParams, ExtendedKeyUsagePurpose, KeyPair, KeyUsagePurpose}; 421 | use x509_parser::prelude::{FromDer, ParsedExtension, X509CertificationRequest}; 422 | 423 | #[test] 424 | fn dont_write_sans_extension_if_no_sans_are_present() { 425 | let mut params = CertificateParams::default(); 426 | params.key_usages.push(KeyUsagePurpose::DigitalSignature); 427 | let key_pair = KeyPair::generate().unwrap(); 428 | let csr = params.serialize_request(&key_pair).unwrap(); 429 | let (_, parsed_csr) = X509CertificationRequest::from_der(csr.der()).unwrap(); 430 | assert!(!parsed_csr 431 | .requested_extensions() 432 | .unwrap() 433 | .any(|ext| matches!(ext, ParsedExtension::SubjectAlternativeName(_)))); 434 | } 435 | 436 | #[test] 437 | fn write_extension_request_if_ekus_are_present() { 438 | let mut params = CertificateParams::default(); 439 | params 440 | .extended_key_usages 441 | .push(ExtendedKeyUsagePurpose::ClientAuth); 442 | let key_pair = KeyPair::generate().unwrap(); 443 | let csr = params.serialize_request(&key_pair).unwrap(); 444 | let (_, parsed_csr) = X509CertificationRequest::from_der(csr.der()).unwrap(); 445 | let requested_extensions = parsed_csr 446 | .requested_extensions() 447 | .unwrap() 448 | .collect::>(); 449 | assert!(matches!( 450 | requested_extensions.first().unwrap(), 451 | ParsedExtension::ExtendedKeyUsage(_) 452 | )); 453 | } 454 | } 455 | 456 | #[cfg(feature = "x509-parser")] 457 | mod test_csr { 458 | use rcgen::{ 459 | CertificateParams, CertificateSigningRequestParams, ExtendedKeyUsagePurpose, KeyPair, 460 | KeyUsagePurpose, 461 | }; 462 | 463 | #[test] 464 | fn test_csr_roundtrip() { 465 | // We should be able to serialize a CSR, and then parse the CSR. 466 | let params = CertificateParams::default(); 467 | generate_and_test_parsed_csr(¶ms); 468 | } 469 | 470 | #[test] 471 | fn test_csr_with_key_usages_roundtrip() { 472 | let mut params = CertificateParams::default(); 473 | params.key_usages = vec![ 474 | KeyUsagePurpose::DigitalSignature, 475 | KeyUsagePurpose::ContentCommitment, 476 | KeyUsagePurpose::KeyEncipherment, 477 | KeyUsagePurpose::DataEncipherment, 478 | KeyUsagePurpose::KeyAgreement, 479 | KeyUsagePurpose::KeyCertSign, 480 | KeyUsagePurpose::CrlSign, 481 | // It doesn't make sense to have both encipher and decipher only 482 | // So we'll take this opportunity to test omitting a key usage 483 | // KeyUsagePurpose::EncipherOnly, 484 | KeyUsagePurpose::DecipherOnly, 485 | ]; 486 | generate_and_test_parsed_csr(¶ms); 487 | } 488 | 489 | #[test] 490 | fn test_csr_with_extended_key_usages_roundtrip() { 491 | let mut params = CertificateParams::default(); 492 | params.extended_key_usages = vec![ 493 | ExtendedKeyUsagePurpose::ServerAuth, 494 | ExtendedKeyUsagePurpose::ClientAuth, 495 | ]; 496 | generate_and_test_parsed_csr(¶ms); 497 | } 498 | 499 | #[test] 500 | fn test_csr_with_key_usgaes_and_extended_key_usages_roundtrip() { 501 | let mut params = CertificateParams::default(); 502 | params.key_usages = vec![KeyUsagePurpose::DigitalSignature]; 503 | params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; 504 | generate_and_test_parsed_csr(¶ms); 505 | } 506 | 507 | fn generate_and_test_parsed_csr(params: &CertificateParams) { 508 | // Generate a key pair for the CSR 509 | let key_pair = KeyPair::generate().unwrap(); 510 | // Serialize the CSR into DER from the given parameters 511 | let csr = params.serialize_request(&key_pair).unwrap(); 512 | // Parse the CSR we just serialized 513 | let csrp = CertificateSigningRequestParams::from_der(csr.der()).unwrap(); 514 | 515 | // Ensure algorithms match. 516 | assert_eq!(key_pair.algorithm(), csrp.public_key.algorithm()); 517 | // Assert that our parsed parameters match our initial parameters 518 | assert_eq!(*params, csrp.params); 519 | } 520 | } 521 | 522 | #[cfg(feature = "x509-parser")] 523 | mod test_subject_alternative_name_criticality { 524 | use x509_parser::certificate::X509Certificate; 525 | use x509_parser::extensions::X509Extension; 526 | use x509_parser::{oid_registry, parse_x509_certificate}; 527 | 528 | use crate::util::default_params; 529 | 530 | #[test] 531 | fn with_subject_sans_not_critical() { 532 | let (params, keypair) = default_params(); 533 | assert!( 534 | !params 535 | .distinguished_name 536 | .iter() 537 | .collect::>() 538 | .is_empty(), 539 | "non-empty subject required for test" 540 | ); 541 | 542 | let cert = params.self_signed(&keypair).unwrap(); 543 | let cert = cert.der(); 544 | let (_, parsed) = parse_x509_certificate(cert).unwrap(); 545 | assert!( 546 | !san_ext(&parsed).critical, 547 | "with subject, SAN ext should not be critical" 548 | ); 549 | } 550 | 551 | #[test] 552 | fn without_subject_sans_critical() { 553 | let (mut params, keypair) = default_params(); 554 | params.distinguished_name = Default::default(); 555 | 556 | let cert = params.self_signed(&keypair).unwrap(); 557 | let cert = cert.der(); 558 | let (_, parsed) = parse_x509_certificate(cert).unwrap(); 559 | assert!( 560 | san_ext(&parsed).critical, 561 | "without subject, SAN ext should be critical" 562 | ); 563 | } 564 | 565 | fn san_ext<'cert>(cert: &'cert X509Certificate) -> &'cert X509Extension<'cert> { 566 | cert.extensions() 567 | .iter() 568 | .find(|ext| ext.oid == oid_registry::OID_X509_EXT_SUBJECT_ALT_NAME) 569 | .expect("missing SAN extension") 570 | } 571 | } 572 | -------------------------------------------------------------------------------- /rcgen/tests/openssl.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(unix, feature = "pem"))] 2 | 3 | use std::cell::RefCell; 4 | use std::io::{Error, ErrorKind, Read, Result as ioResult, Write}; 5 | use std::rc::Rc; 6 | 7 | use openssl::asn1::{Asn1Integer, Asn1Time}; 8 | use openssl::bn::BigNum; 9 | use openssl::pkey::PKey; 10 | use openssl::ssl::{HandshakeError, SslAcceptor, SslConnector, SslMethod}; 11 | use openssl::stack::Stack; 12 | use openssl::x509::store::{X509Store, X509StoreBuilder}; 13 | use openssl::x509::{CrlStatus, X509Crl, X509Req, X509StoreContext, X509}; 14 | 15 | use rcgen::{ 16 | BasicConstraints, Certificate, CertificateParams, DnType, DnValue, GeneralSubtree, IsCa, 17 | KeyPair, NameConstraints, 18 | }; 19 | 20 | mod util; 21 | 22 | fn verify_cert_basic(cert: &Certificate) { 23 | let cert_pem = cert.pem(); 24 | println!("{cert_pem}"); 25 | 26 | let x509 = X509::from_pem(cert_pem.as_bytes()).unwrap(); 27 | let mut builder = X509StoreBuilder::new().unwrap(); 28 | builder.add_cert(x509.clone()).unwrap(); 29 | 30 | let store: X509Store = builder.build(); 31 | let mut ctx = X509StoreContext::new().unwrap(); 32 | let mut stack = Stack::new().unwrap(); 33 | stack.push(x509.clone()).unwrap(); 34 | ctx.init(&store, &x509, stack.as_ref(), |ctx| { 35 | ctx.verify_cert().unwrap(); 36 | Ok(()) 37 | }) 38 | .unwrap(); 39 | } 40 | 41 | // TODO implement Debug manually instead of 42 | // deriving it 43 | #[derive(Clone, Debug)] 44 | struct PipeInner([Vec; 2]); 45 | 46 | #[derive(Clone, Debug)] 47 | struct PipeEnd { 48 | read_pos: usize, 49 | /// Which end of the pipe 50 | end_idx: usize, 51 | inner: Rc>, 52 | } 53 | 54 | fn create_pipe() -> (PipeEnd, PipeEnd) { 55 | let pipe_inner = PipeInner([Vec::new(), Vec::new()]); 56 | let inner = Rc::new(RefCell::new(pipe_inner)); 57 | ( 58 | PipeEnd { 59 | read_pos: 0, 60 | end_idx: 0, 61 | inner: inner.clone(), 62 | }, 63 | PipeEnd { 64 | read_pos: 0, 65 | end_idx: 1, 66 | inner, 67 | }, 68 | ) 69 | } 70 | 71 | impl Write for PipeEnd { 72 | fn write(&mut self, buf: &[u8]) -> ioResult { 73 | self.inner.borrow_mut().0[self.end_idx].extend_from_slice(buf); 74 | Ok(buf.len()) 75 | } 76 | fn flush(&mut self) -> ioResult<()> { 77 | Ok(()) 78 | } 79 | } 80 | 81 | impl Read for PipeEnd { 82 | fn read(&mut self, mut buf: &mut [u8]) -> ioResult { 83 | let inner = self.inner.borrow_mut(); 84 | let r_sl = &inner.0[1 - self.end_idx][self.read_pos..]; 85 | if r_sl.is_empty() { 86 | return Err(Error::new(ErrorKind::WouldBlock, "oh no!")); 87 | } 88 | let r = buf.len().min(r_sl.len()); 89 | std::io::copy(&mut &r_sl[..r], &mut buf)?; 90 | self.read_pos += r; 91 | Ok(r) 92 | } 93 | } 94 | 95 | fn verify_cert(cert: &Certificate, key_pair: &KeyPair) { 96 | verify_cert_basic(cert); 97 | let key = key_pair.serialize_der(); 98 | verify_cert_ca(&cert.pem(), &key, &cert.pem()); 99 | } 100 | 101 | fn verify_cert_ca(cert_pem: &str, key: &[u8], ca_cert_pem: &str) { 102 | println!("{cert_pem}"); 103 | println!("{ca_cert_pem}"); 104 | 105 | let x509 = X509::from_pem(cert_pem.as_bytes()).unwrap(); 106 | 107 | let ca_x509 = X509::from_pem(ca_cert_pem.as_bytes()).unwrap(); 108 | 109 | let mut builder = X509StoreBuilder::new().unwrap(); 110 | builder.add_cert(ca_x509).unwrap(); 111 | 112 | let store: X509Store = builder.build(); 113 | 114 | let srv = SslMethod::tls_server(); 115 | let mut ssl_srv_ctx = SslAcceptor::mozilla_modern(srv).unwrap(); 116 | //let key = cert.serialize_private_key_der(); 117 | let pkey = PKey::private_key_from_der(key).unwrap(); 118 | ssl_srv_ctx.set_private_key(&pkey).unwrap(); 119 | 120 | ssl_srv_ctx.set_certificate(&x509).unwrap(); 121 | 122 | let cln = SslMethod::tls_client(); 123 | let mut ssl_cln_ctx = SslConnector::builder(cln).unwrap(); 124 | ssl_cln_ctx.set_cert_store(store); 125 | 126 | let ssl_srv_ctx = ssl_srv_ctx.build(); 127 | let ssl_cln_ctx = ssl_cln_ctx.build(); 128 | 129 | let (pipe_end_1, pipe_end_2) = create_pipe(); 130 | 131 | let (mut ssl_srv_stream, mut ssl_cln_stream) = { 132 | let mut srv_res = ssl_srv_ctx.accept(pipe_end_1); 133 | let mut cln_res = ssl_cln_ctx.connect("crabs.crabs", pipe_end_2); 134 | let mut ready = 0u8; 135 | let mut iter_budget = 100; 136 | loop { 137 | match cln_res { 138 | Ok(_) => ready |= 2, 139 | Err(HandshakeError::WouldBlock(mh)) => cln_res = mh.handshake(), 140 | Err(e) => panic!("Error: {:?}", e), 141 | } 142 | match srv_res { 143 | Ok(_) => ready |= 1, 144 | Err(HandshakeError::WouldBlock(mh)) => srv_res = mh.handshake(), 145 | Err(e) => panic!("Error: {:?}", e), 146 | } 147 | if ready == 3 { 148 | break (cln_res.unwrap(), srv_res.unwrap()); 149 | } 150 | if iter_budget == 0 { 151 | panic!("iter budget exhausted"); 152 | } 153 | iter_budget -= 1; 154 | } 155 | }; 156 | 157 | const HELLO_FROM_SRV: &[u8] = b"hello from server"; 158 | const HELLO_FROM_CLN: &[u8] = b"hello from client"; 159 | 160 | ssl_srv_stream.ssl_write(HELLO_FROM_SRV).unwrap(); 161 | ssl_cln_stream.ssl_write(HELLO_FROM_CLN).unwrap(); 162 | 163 | // TODO read the data we just wrote from the streams 164 | } 165 | 166 | fn verify_csr(params: &CertificateParams, key_pair: &KeyPair) { 167 | let csr = params 168 | .serialize_request(key_pair) 169 | .and_then(|csr| csr.pem()) 170 | .unwrap(); 171 | println!("{csr}"); 172 | let key = key_pair.serialize_der(); 173 | let pkey = PKey::private_key_from_der(&key).unwrap(); 174 | 175 | let req = X509Req::from_pem(csr.as_bytes()).unwrap(); 176 | req.verify(&pkey).unwrap(); 177 | } 178 | 179 | #[test] 180 | fn test_openssl() { 181 | let (params, key_pair) = util::default_params(); 182 | let cert = params.self_signed(&key_pair).unwrap(); 183 | verify_cert(&cert, &key_pair); 184 | } 185 | 186 | #[test] 187 | fn test_request() { 188 | let (params, key_pair) = util::default_params(); 189 | verify_csr(¶ms, &key_pair); 190 | } 191 | 192 | #[test] 193 | fn test_openssl_256() { 194 | let (params, _) = util::default_params(); 195 | let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap(); 196 | let cert = params.self_signed(&key_pair).unwrap(); 197 | 198 | // Now verify the certificate. 199 | verify_cert(&cert, &key_pair); 200 | verify_csr(¶ms, &key_pair); 201 | } 202 | 203 | #[test] 204 | fn test_openssl_384() { 205 | let (params, _) = util::default_params(); 206 | let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P384_SHA384).unwrap(); 207 | let cert = params.self_signed(&key_pair).unwrap(); 208 | 209 | // Now verify the certificate. 210 | verify_cert(&cert, &key_pair); 211 | verify_csr(¶ms, &key_pair); 212 | } 213 | 214 | #[test] 215 | #[cfg(feature = "aws_lc_rs")] 216 | fn test_openssl_521() { 217 | let (params, _) = util::default_params(); 218 | let key_pair = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P521_SHA512).unwrap(); 219 | let cert = params.self_signed(&key_pair).unwrap(); 220 | 221 | // Now verify the certificate. 222 | verify_cert(&cert, &key_pair); 223 | verify_csr(¶ms, &key_pair); 224 | } 225 | 226 | #[test] 227 | fn test_openssl_25519() { 228 | let (params, _) = util::default_params(); 229 | let key_pair = KeyPair::generate_for(&rcgen::PKCS_ED25519).unwrap(); 230 | let cert = params.self_signed(&key_pair).unwrap(); 231 | 232 | // Now verify the certificate. 233 | // TODO openssl doesn't support v2 keys (yet) 234 | // https://github.com/est31/rcgen/issues/11 235 | // https://github.com/openssl/openssl/issues/10468 236 | verify_cert_basic(&cert); 237 | //verify_csr(&cert); 238 | } 239 | 240 | #[test] 241 | fn test_openssl_25519_v1_given() { 242 | let (params, _) = util::default_params(); 243 | let key_pair = rcgen::KeyPair::from_pem(util::ED25519_TEST_KEY_PAIR_PEM_V1).unwrap(); 244 | let cert = params.self_signed(&key_pair).unwrap(); 245 | 246 | // Now verify the certificate as well as CSR, 247 | // but only on OpenSSL >= 1.1.1 248 | // On prior versions, only do basic verification 249 | #[allow(clippy::unusual_byte_groupings)] 250 | if openssl::version::number() >= 0x1_01_01_00_f { 251 | verify_cert(&cert, &key_pair); 252 | verify_csr(¶ms, &key_pair); 253 | } else { 254 | verify_cert_basic(&cert); 255 | } 256 | } 257 | 258 | #[test] 259 | fn test_openssl_25519_v2_given() { 260 | let (params, _) = util::default_params(); 261 | let key_pair = rcgen::KeyPair::from_pem(util::ED25519_TEST_KEY_PAIR_PEM_V2).unwrap(); 262 | let cert = params.self_signed(&key_pair).unwrap(); 263 | 264 | // Now verify the certificate. 265 | // TODO openssl doesn't support v2 keys (yet) 266 | // https://github.com/est31/rcgen/issues/11 267 | // https://github.com/openssl/openssl/issues/10468 268 | verify_cert_basic(&cert); 269 | //verify_csr(&cert); 270 | } 271 | 272 | #[test] 273 | fn test_openssl_rsa_given() { 274 | let (params, _) = util::default_params(); 275 | let key_pair = KeyPair::from_pem(util::RSA_TEST_KEY_PAIR_PEM).unwrap(); 276 | let cert = params.self_signed(&key_pair).unwrap(); 277 | 278 | // Now verify the certificate. 279 | verify_cert(&cert, &key_pair); 280 | verify_csr(¶ms, &key_pair); 281 | } 282 | 283 | #[test] 284 | fn test_openssl_rsa_combinations_given() { 285 | let alg_list = [ 286 | &rcgen::PKCS_RSA_SHA256, 287 | &rcgen::PKCS_RSA_SHA384, 288 | &rcgen::PKCS_RSA_SHA512, 289 | //&rcgen::PKCS_RSA_PSS_SHA256, 290 | ]; 291 | for (i, alg) in alg_list.iter().enumerate() { 292 | let (params, _) = util::default_params(); 293 | let key_pair = 294 | KeyPair::from_pkcs8_pem_and_sign_algo(util::RSA_TEST_KEY_PAIR_PEM, alg).unwrap(); 295 | let cert = params.self_signed(&key_pair).unwrap(); 296 | 297 | // Now verify the certificate. 298 | if i >= 4 { 299 | verify_cert(&cert, &key_pair); 300 | verify_csr(¶ms, &key_pair); 301 | } else { 302 | // The PSS key types are not fully supported. 303 | // An attempt to use them gives a handshake error. 304 | verify_cert_basic(&cert); 305 | } 306 | } 307 | } 308 | 309 | #[test] 310 | fn test_openssl_separate_ca() { 311 | let (mut ca_params, ca_key) = util::default_params(); 312 | ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 313 | let ca_cert = ca_params.self_signed(&ca_key).unwrap(); 314 | let ca_cert_pem = ca_cert.pem(); 315 | 316 | let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); 317 | params 318 | .distinguished_name 319 | .push(DnType::OrganizationName, "Crab widgits SE"); 320 | params 321 | .distinguished_name 322 | .push(DnType::CommonName, "Dev domain"); 323 | let cert_key = KeyPair::generate().unwrap(); 324 | let cert = params.signed_by(&cert_key, &ca_params, &ca_key).unwrap(); 325 | let key = cert_key.serialize_der(); 326 | 327 | verify_cert_ca(&cert.pem(), &key, &ca_cert_pem); 328 | } 329 | 330 | #[test] 331 | fn test_openssl_separate_ca_with_printable_string() { 332 | let (mut ca_params, ca_key) = util::default_params(); 333 | ca_params.distinguished_name.push( 334 | DnType::CountryName, 335 | DnValue::PrintableString("US".try_into().unwrap()), 336 | ); 337 | ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 338 | let ca_cert = ca_params.self_signed(&ca_key).unwrap(); 339 | 340 | let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); 341 | params 342 | .distinguished_name 343 | .push(DnType::OrganizationName, "Crab widgits SE"); 344 | params 345 | .distinguished_name 346 | .push(DnType::CommonName, "Dev domain"); 347 | let cert_key = KeyPair::generate().unwrap(); 348 | let cert = params.signed_by(&cert_key, &ca_params, &ca_key).unwrap(); 349 | let key = cert_key.serialize_der(); 350 | 351 | verify_cert_ca(&cert.pem(), &key, &ca_cert.pem()); 352 | } 353 | 354 | #[test] 355 | fn test_openssl_separate_ca_with_other_signing_alg() { 356 | let (mut ca_params, _) = util::default_params(); 357 | ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 358 | let ca_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P256_SHA256).unwrap(); 359 | let ca_cert = ca_params.self_signed(&ca_key).unwrap(); 360 | 361 | let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); 362 | params 363 | .distinguished_name 364 | .push(DnType::OrganizationName, "Crab widgits SE"); 365 | params 366 | .distinguished_name 367 | .push(DnType::CommonName, "Dev domain"); 368 | let cert_key = KeyPair::generate_for(&rcgen::PKCS_ECDSA_P384_SHA384).unwrap(); 369 | let cert = params.signed_by(&cert_key, &ca_params, &ca_key).unwrap(); 370 | let key = cert_key.serialize_der(); 371 | 372 | verify_cert_ca(&cert.pem(), &key, &ca_cert.pem()); 373 | } 374 | 375 | #[test] 376 | fn test_openssl_separate_ca_name_constraints() { 377 | let (mut ca_params, ca_key) = util::default_params(); 378 | ca_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 379 | 380 | println!("openssl version: {:x}", openssl::version::number()); 381 | 382 | ca_params.name_constraints = Some(NameConstraints { 383 | permitted_subtrees: vec![GeneralSubtree::DnsName("crabs.crabs".to_string())], 384 | //permitted_subtrees : vec![GeneralSubtree::DnsName("".to_string())], 385 | //permitted_subtrees : Vec::new(), 386 | //excluded_subtrees : vec![GeneralSubtree::DnsName(".v".to_string())], 387 | excluded_subtrees: Vec::new(), 388 | }); 389 | let ca_cert = ca_params.self_signed(&ca_key).unwrap(); 390 | 391 | let mut params = CertificateParams::new(vec!["crabs.crabs".to_string()]).unwrap(); 392 | params 393 | .distinguished_name 394 | .push(DnType::OrganizationName, "Crab widgits SE"); 395 | params 396 | .distinguished_name 397 | .push(DnType::CommonName, "Dev domain"); 398 | let cert_key = KeyPair::generate().unwrap(); 399 | let cert = params.signed_by(&cert_key, &ca_params, &ca_key).unwrap(); 400 | let key = cert_key.serialize_der(); 401 | 402 | verify_cert_ca(&cert.pem(), &key, &ca_cert.pem()); 403 | } 404 | 405 | #[test] 406 | fn test_openssl_crl_parse() { 407 | // Create a CRL with one revoked cert, and an issuer to sign the CRL. 408 | let (crl_params, crl, issuer) = util::test_crl(); 409 | let revoked_cert = crl_params.revoked_certs.first().unwrap(); 410 | let revoked_cert_serial = &revoked_cert.serial_number; 411 | 412 | // Serialize the CRL signed by the issuer in both PEM and DER. 413 | let crl_pem = crl.pem().unwrap(); 414 | 415 | // We should be able to parse the PEM form without error. 416 | assert!(X509Crl::from_pem(crl_pem.as_bytes()).is_ok()); 417 | 418 | // We should also be able to parse the DER form without error. 419 | let openssl_crl = X509Crl::from_der(crl.der()).expect("failed to parse CRL DER"); 420 | 421 | // The properties of the CRL should match expected. 422 | let openssl_issuer = X509::from_der(issuer.der()).unwrap(); 423 | // Asn1Time::from_unix takes i64 or i32 (depending on CPU architecture) 424 | #[allow(clippy::useless_conversion)] 425 | let expected_last_update = 426 | Asn1Time::from_unix(crl_params.this_update.unix_timestamp().try_into().unwrap()).unwrap(); 427 | assert!(openssl_crl.last_update().eq(&expected_last_update)); 428 | // Asn1Time::from_unix takes i64 or i32 (depending on CPU architecture) 429 | #[allow(clippy::useless_conversion)] 430 | let expected_next_update = 431 | Asn1Time::from_unix(crl_params.next_update.unix_timestamp().try_into().unwrap()).unwrap(); 432 | assert!(openssl_crl.next_update().unwrap().eq(&expected_next_update)); 433 | assert!(matches!( 434 | openssl_crl 435 | .issuer_name() 436 | .try_cmp(openssl_issuer.issuer_name()) 437 | .unwrap(), 438 | core::cmp::Ordering::Equal 439 | )); 440 | 441 | // We should find the revoked certificate is revoked. 442 | let openssl_serial = BigNum::from_slice(revoked_cert_serial.as_ref()).unwrap(); 443 | let openssl_serial = Asn1Integer::from_bn(&openssl_serial).unwrap(); 444 | let openssl_crl_status = openssl_crl.get_by_serial(&openssl_serial); 445 | assert!(matches!(openssl_crl_status, CrlStatus::Revoked(_))); 446 | 447 | // We should be able to verify the CRL signature with the issuer's public key. 448 | let issuer_pkey = openssl_issuer.public_key().unwrap(); 449 | assert!(openssl_crl 450 | .verify(&issuer_pkey) 451 | .expect("failed to verify CRL signature")); 452 | } 453 | 454 | #[test] 455 | fn test_openssl_crl_dps_parse() { 456 | // Generate and parse a certificate that includes two CRL distribution points. 457 | let der = util::cert_with_crl_dps(); 458 | let cert = X509::from_der(&der).expect("failed to parse cert DER"); 459 | 460 | // We should find the CRL DPs extension. 461 | let dps = cert 462 | .crl_distribution_points() 463 | .expect("missing crl distribution points extension"); 464 | assert!(!dps.is_empty()); 465 | 466 | // We should find two distribution points, each with a distribution point name containing 467 | // a full name sequence of general names. 468 | let general_names = dps 469 | .iter() 470 | .flat_map(|dp| { 471 | dp.distpoint() 472 | .expect("distribution point missing distribution point name") 473 | .fullname() 474 | .expect("distribution point name missing general names") 475 | .iter() 476 | }) 477 | .collect::>(); 478 | 479 | // Each general name should be a URI name. 480 | let uris = general_names 481 | .iter() 482 | .map(|general_name| { 483 | general_name 484 | .uri() 485 | .expect("general name is not a directory name") 486 | }) 487 | .collect::>(); 488 | 489 | // We should find the expected URIs. 490 | assert_eq!( 491 | uris, 492 | &[ 493 | "http://example.com/crl.der", 494 | "http://crls.example.com/1234", 495 | "ldap://example.com/crl.der" 496 | ] 497 | ); 498 | } 499 | 500 | #[test] 501 | #[cfg(all(feature = "crypto", feature = "aws_lc_rs"))] 502 | fn test_openssl_pkcs1_and_sec1_keys() { 503 | use openssl::ec::{EcGroup, EcKey}; 504 | use openssl::nid::Nid; 505 | use openssl::pkey::PKey; 506 | use openssl::rsa::Rsa; 507 | use pki_types::PrivateKeyDer; 508 | 509 | let rsa = Rsa::generate(2048).unwrap(); 510 | let rsa = PKey::from_rsa(rsa).unwrap(); 511 | 512 | let pkcs1_rsa_key_der = PrivateKeyDer::try_from(rsa.private_key_to_der().unwrap()).unwrap(); 513 | KeyPair::try_from(&pkcs1_rsa_key_der).unwrap(); 514 | 515 | let pkcs8_rsa_key_der = PrivateKeyDer::try_from(rsa.private_key_to_pkcs8().unwrap()).unwrap(); 516 | KeyPair::try_from(&pkcs8_rsa_key_der).unwrap(); 517 | 518 | let group = EcGroup::from_curve_name(Nid::SECP521R1).unwrap(); 519 | let ec_key = EcKey::generate(&group).unwrap(); 520 | 521 | let ec_key = PKey::from_ec_key(ec_key).unwrap(); 522 | 523 | let sec1_ec_key_der = PrivateKeyDer::try_from(ec_key.private_key_to_der().unwrap()).unwrap(); 524 | KeyPair::try_from(&sec1_ec_key_der).unwrap(); 525 | 526 | let pkcs8_ec_key_der = PrivateKeyDer::try_from(ec_key.private_key_to_pkcs8().unwrap()).unwrap(); 527 | KeyPair::try_from(&pkcs8_ec_key_der).unwrap(); 528 | } 529 | -------------------------------------------------------------------------------- /rcgen/tests/util.rs: -------------------------------------------------------------------------------- 1 | #![cfg(feature = "crypto")] 2 | 3 | use time::{Duration, OffsetDateTime}; 4 | 5 | use rcgen::{BasicConstraints, Certificate, CertificateParams, KeyPair}; 6 | use rcgen::{ 7 | CertificateRevocationList, CrlDistributionPoint, CrlIssuingDistributionPoint, CrlScope, 8 | }; 9 | use rcgen::{CertificateRevocationListParams, DnType, IsCa, KeyIdMethod}; 10 | use rcgen::{KeyUsagePurpose, RevocationReason, RevokedCertParams, SerialNumber}; 11 | 12 | // Generated by adding `println!("{}", cert.serialize_private_key_pem());` 13 | // to the test_webpki_25519 test and panicing explicitly. 14 | // This is a "v2" key containing the public key as well as the 15 | // private one. 16 | #[allow(unused)] 17 | pub const ED25519_TEST_KEY_PAIR_PEM_V2: &str = r#" 18 | -----BEGIN PRIVATE KEY----- 19 | MFMCAQEwBQYDK2VwBCIEIC2pHJYjFHhK8V7mj6BnHWUVMS4CRolUlDdRXKCtguDu 20 | oSMDIQDrvH/x8Nx9untsuc6ET+ce3w7PSuLY8BLWcHdXDGvkQA== 21 | -----END PRIVATE KEY----- 22 | "#; 23 | // Generated with `openssl genpkey -algorithm ED25519` 24 | // A "v1" key as it doesn't contain the public key (which can be 25 | // derived from the private one) 26 | #[allow(unused)] 27 | pub const ED25519_TEST_KEY_PAIR_PEM_V1: &str = r#" 28 | -----BEGIN PRIVATE KEY----- 29 | MC4CAQAwBQYDK2VwBCIEIDSat0MacDt2fokpnzuBaXvAQR6RJGS9rgIYOU2mZKld 30 | -----END PRIVATE KEY----- 31 | "#; 32 | 33 | /* 34 | Generated by: openssl genpkey -algorithm RSA \ 35 | -pkeyopt rsa_keygen_bits:2048 \ 36 | -pkeyopt rsa_keygen_pubexp:65537 | \ 37 | openssl pkcs8 -topk8 -nocrypt -outform pem 38 | */ 39 | #[allow(dead_code)] // Used in some but not all test compilation units. 40 | #[cfg(feature = "pem")] 41 | pub const RSA_TEST_KEY_PAIR_PEM: &str = r#" 42 | -----BEGIN PRIVATE KEY----- 43 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDYjmgyV3/LSizJ 44 | XrYrATZrrPr2Edo8yiOgBLFmi4sgeGdQ5n6nhjTGfBEIP2Ia6z+hbiGOMncabEBc 45 | zkdME+JFYVCSkS7r4ivMOzp2egxLgcPKcerBoXI8DUbHhIR9z89lHiPHDJv3+d0A 46 | c1b9bz9b8OAeZWiQmFvmjpbc2DfhQ2OFx2MwFZCYF196rrXOc6/SR2esZVRrkW22 47 | RBKFTgz6GIA5A/5VWKIISSqEB1gOcMz2iq5987I28+Ez4rcLZ2lB7cZ7TbNxkAwt 48 | 0fPL+EuyP7XOzbIj4/kSAlU5xfwNERa3BEuOFro4i5EmSDj+lR5xdRpFnx0j5zOo 49 | zUL2lHG9AgMBAAECggEARpV8DtSIOcmOeYAeXjwB8eyqy+Obv26fV/vPmr3m9glo 50 | m2zVYWMT9pHft1F5d46v6b0MwN1gBsO74sP1Zy2f9b83VN5vbcEFR4cSkiVLtpyw 51 | JV8mBkDKDBrDtCpUSPGgBrRhMvLAL35Ic2oks2w8OYp0clPZVi/i3G4jbA4pgIkt 52 | yB6k79Uhzz2nfZ0VpPORGNsBOl5UK1LkmIhTJ6S0LsLj7XSet9YHR0k0F0/NOSzz 53 | +jMUzfjOPm8M+b3wk9yAQP7qT9Iy3MHbGAad4gNXGu1LqeDRkfmM5pnoG0ASP3+B 54 | IvX2l0ZLeCtg+GRLlGvUVI1HSQHCsuiC6/g2bq7JAQKBgQD3/Eb58VjpdwJYPrg/ 55 | srfnC9sKSf5C0Q8YSmkfvOmeD6Vqe0EXRuMyhwTkkVdz04yPiB2j0fXdeB9h16Ic 56 | 9HWb/UNGWNpV7Ul1MSHbeu32Xor+5IkqCGgSoMznlt9QPR4PxfIOgO8cVL1HgNAZ 57 | JnBDzhTG0FfY75hqpCDmFGAZwQKBgQDfjhk5aM0yGLYgZfw/K9BrwjctQBWdrps2 58 | 4TtkG7Kuj0hsimCdrqJQ5JN8aUM41zDUr3Px1uN5gUAZ3dE9DoGsgj15ZwgVkAMM 59 | E54bfzOqkbh+mRpptIxL4HmHB45vgvz0YljeRoOEQvPF/OSGLti7VIkD4898PFKl 60 | cU+P9m5+/QKBgDi8XTi+AQuZEM5Duz/Hkc+opLqb5zI+RmfWTmrWe9SP29aa0G+U 61 | 5lIfFf19SzbSxavpBm7+kHPVEcj+3rYlL+s6bHPhzEIwgcfwL8DZRSxCwSZD/yXA 62 | up7Yb0jk+b6P3RravOCYmxwuPwfm7rVyV+kLczFxZUfauVJcrrI1Iy+BAoGBAJjG 63 | MEDGeSxaLOS5LYgyNg3ePPzkhaEruRDpHUBNmW+npZPfgSVhObXUb2IfQXwvu0Qt 64 | 3yuPcgcQKDFFIH/8UOwGWWKE4cZyk1KGeY9K/5D6Yr3JfX5tj08vSX3Y0SMtvhZ4 65 | u0izoZ8abiOIrtdwXlau76/D2ICLbON5Kykz/NE1AoGAId2+pO9p8jBt9l+5jZo7 66 | Rw/mb5icMaG2hqAzs37gUPbpSwQFOmGhQmNM+WvYEvUUuiTxI3AOeEK8Mj+BVB4+ 67 | uE3X/fWK/JR9iOzH9OM31Nua8/EJzr7BmUpXeRr4dAtVimeQ+5HY6IgRsFGPKKwv 68 | YPTHy8SWRA2sMII3ArhHJ8A= 69 | -----END PRIVATE KEY----- 70 | "#; 71 | 72 | pub fn default_params() -> (CertificateParams, KeyPair) { 73 | let mut params = 74 | CertificateParams::new(vec!["crabs.crabs".to_string(), "localhost".to_string()]).unwrap(); 75 | params 76 | .distinguished_name 77 | .push(DnType::OrganizationName, "Crab widgits SE"); 78 | params 79 | .distinguished_name 80 | .push(DnType::CommonName, "Master CA"); 81 | 82 | let key_pair = KeyPair::generate().unwrap(); 83 | (params, key_pair) 84 | } 85 | 86 | #[allow(unused)] // Used by openssl + x509-parser features. 87 | pub fn test_crl() -> ( 88 | CertificateRevocationListParams, 89 | CertificateRevocationList, 90 | Certificate, 91 | ) { 92 | let (mut issuer, key_pair) = default_params(); 93 | issuer.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 94 | issuer.key_usages = vec![ 95 | KeyUsagePurpose::KeyCertSign, 96 | KeyUsagePurpose::DigitalSignature, 97 | KeyUsagePurpose::CrlSign, 98 | ]; 99 | let issuer_cert = issuer.self_signed(&key_pair).unwrap(); 100 | 101 | let now = OffsetDateTime::now_utc(); 102 | let next_week = now + Duration::weeks(1); 103 | let revoked_cert = RevokedCertParams { 104 | serial_number: SerialNumber::from_slice(&[0x00, 0xC0, 0xFF, 0xEE]), 105 | revocation_time: now, 106 | reason_code: Some(RevocationReason::KeyCompromise), 107 | invalidity_date: None, 108 | }; 109 | 110 | let params = CertificateRevocationListParams { 111 | this_update: now, 112 | next_update: next_week, 113 | crl_number: SerialNumber::from(1234), 114 | issuing_distribution_point: Some(CrlIssuingDistributionPoint { 115 | distribution_point: CrlDistributionPoint { 116 | uris: vec!["http://example.com/crl".to_string()], 117 | }, 118 | scope: Some(CrlScope::UserCertsOnly), 119 | }), 120 | revoked_certs: vec![revoked_cert], 121 | key_identifier_method: KeyIdMethod::Sha256, 122 | }; 123 | 124 | let crl = params.signed_by(&issuer, &key_pair).unwrap(); 125 | (params, crl, issuer_cert) 126 | } 127 | 128 | #[allow(unused)] // Used by openssl + x509-parser features. 129 | pub fn cert_with_crl_dps() -> Vec { 130 | let (mut params, key_pair) = default_params(); 131 | params.crl_distribution_points = vec![ 132 | CrlDistributionPoint { 133 | uris: vec![ 134 | "http://example.com/crl.der".to_string(), 135 | "http://crls.example.com/1234".to_string(), 136 | ], 137 | }, 138 | CrlDistributionPoint { 139 | uris: vec!["ldap://example.com/crl.der".to_string()], 140 | }, 141 | ]; 142 | 143 | params.self_signed(&key_pair).unwrap().der().to_vec() 144 | } 145 | -------------------------------------------------------------------------------- /rustls-cert-gen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rustls-cert-gen" 3 | version = "0.1.0" 4 | description = "Rust X.509 certificate generator CLI" 5 | documentation = "https://docs.rs/rustls-cert-gen" 6 | homepage = "https://github.com/rustls/rcgen/tree/main/rustls-cert-gen" 7 | repository = "https://github.com/rustls/rcgen" 8 | license.workspace = true 9 | edition.workspace = true 10 | keywords.workspace = true 11 | 12 | [features] 13 | default = ["ring"] 14 | aws_lc_rs = ["dep:aws-lc-rs", "rcgen/aws_lc_rs", "aws-lc-rs/aws-lc-sys"] 15 | ring = ["dep:ring", "rcgen/ring"] 16 | fips = ["dep:aws-lc-rs", "rcgen/aws_lc_rs", "aws-lc-rs/fips"] 17 | 18 | [dependencies] 19 | rcgen = { version = "0.14", path = "../rcgen", default-features = false, features = ["pem"] } 20 | bpaf = { version = "0.9.5", features = ["derive"] } 21 | pem = { workspace = true } 22 | pki-types = { workspace = true } 23 | ring = { workspace = true, optional = true } 24 | anyhow = "1.0.75" 25 | aws-lc-rs = { workspace = true, optional = true } 26 | 27 | [dev-dependencies] 28 | assert_fs = "1.0.13" 29 | x509-parser = { workspace = true, features = ["verify"] } 30 | -------------------------------------------------------------------------------- /rustls-cert-gen/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019-2025 est31 and contributors 2 | 3 | Licensed under MIT or Apache License 2.0, 4 | at your option. 5 | 6 | The full list of contributors can be obtained by looking 7 | at the VCS log (originally, this crate was git versioned, 8 | there you can do "git shortlog -sn" for this task). 9 | 10 | MIT License 11 | ----------- 12 | 13 | The MIT License (MIT) 14 | 15 | Copyright (c) 2019-2025 est31 and contributors 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in all 25 | copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 33 | SOFTWARE. 34 | 35 | 36 | 37 | Apache License, version 2.0 38 | --------------------------- 39 | Apache License 40 | Version 2.0, January 2004 41 | http://www.apache.org/licenses/ 42 | 43 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 44 | 45 | 1. Definitions. 46 | 47 | "License" shall mean the terms and conditions for use, reproduction, 48 | and distribution as defined by Sections 1 through 9 of this document. 49 | 50 | "Licensor" shall mean the copyright owner or entity authorized by 51 | the copyright owner that is granting the License. 52 | 53 | "Legal Entity" shall mean the union of the acting entity and all 54 | other entities that control, are controlled by, or are under common 55 | control with that entity. For the purposes of this definition, 56 | "control" means (i) the power, direct or indirect, to cause the 57 | direction or management of such entity, whether by contract or 58 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 59 | outstanding shares, or (iii) beneficial ownership of such entity. 60 | 61 | "You" (or "Your") shall mean an individual or Legal Entity 62 | exercising permissions granted by this License. 63 | 64 | "Source" form shall mean the preferred form for making modifications, 65 | including but not limited to software source code, documentation 66 | source, and configuration files. 67 | 68 | "Object" form shall mean any form resulting from mechanical 69 | transformation or translation of a Source form, including but 70 | not limited to compiled object code, generated documentation, 71 | and conversions to other media types. 72 | 73 | "Work" shall mean the work of authorship, whether in Source or 74 | Object form, made available under the License, as indicated by a 75 | copyright notice that is included in or attached to the work 76 | (an example is provided in the Appendix below). 77 | 78 | "Derivative Works" shall mean any work, whether in Source or Object 79 | form, that is based on (or derived from) the Work and for which the 80 | editorial revisions, annotations, elaborations, or other modifications 81 | represent, as a whole, an original work of authorship. For the purposes 82 | of this License, Derivative Works shall not include works that remain 83 | separable from, or merely link (or bind by name) to the interfaces of, 84 | the Work and Derivative Works thereof. 85 | 86 | "Contribution" shall mean any work of authorship, including 87 | the original version of the Work and any modifications or additions 88 | to that Work or Derivative Works thereof, that is intentionally 89 | submitted to Licensor for inclusion in the Work by the copyright owner 90 | or by an individual or Legal Entity authorized to submit on behalf of 91 | the copyright owner. For the purposes of this definition, "submitted" 92 | means any form of electronic, verbal, or written communication sent 93 | to the Licensor or its representatives, including but not limited to 94 | communication on electronic mailing lists, source code control systems, 95 | and issue tracking systems that are managed by, or on behalf of, the 96 | Licensor for the purpose of discussing and improving the Work, but 97 | excluding communication that is conspicuously marked or otherwise 98 | designated in writing by the copyright owner as "Not a Contribution." 99 | 100 | "Contributor" shall mean Licensor and any individual or Legal Entity 101 | on behalf of whom a Contribution has been received by Licensor and 102 | subsequently incorporated within the Work. 103 | 104 | 2. Grant of Copyright License. Subject to the terms and conditions of 105 | this License, each Contributor hereby grants to You a perpetual, 106 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 107 | copyright license to reproduce, prepare Derivative Works of, 108 | publicly display, publicly perform, sublicense, and distribute the 109 | Work and such Derivative Works in Source or Object form. 110 | 111 | 3. Grant of Patent License. Subject to the terms and conditions of 112 | this License, each Contributor hereby grants to You a perpetual, 113 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 114 | (except as stated in this section) patent license to make, have made, 115 | use, offer to sell, sell, import, and otherwise transfer the Work, 116 | where such license applies only to those patent claims licensable 117 | by such Contributor that are necessarily infringed by their 118 | Contribution(s) alone or by combination of their Contribution(s) 119 | with the Work to which such Contribution(s) was submitted. If You 120 | institute patent litigation against any entity (including a 121 | cross-claim or counterclaim in a lawsuit) alleging that the Work 122 | or a Contribution incorporated within the Work constitutes direct 123 | or contributory patent infringement, then any patent licenses 124 | granted to You under this License for that Work shall terminate 125 | as of the date such litigation is filed. 126 | 127 | 4. Redistribution. You may reproduce and distribute copies of the 128 | Work or Derivative Works thereof in any medium, with or without 129 | modifications, and in Source or Object form, provided that You 130 | meet the following conditions: 131 | 132 | (a) You must give any other recipients of the Work or 133 | Derivative Works a copy of this License; and 134 | 135 | (b) You must cause any modified files to carry prominent notices 136 | stating that You changed the files; and 137 | 138 | (c) You must retain, in the Source form of any Derivative Works 139 | that You distribute, all copyright, patent, trademark, and 140 | attribution notices from the Source form of the Work, 141 | excluding those notices that do not pertain to any part of 142 | the Derivative Works; and 143 | 144 | (d) If the Work includes a "NOTICE" text file as part of its 145 | distribution, then any Derivative Works that You distribute must 146 | include a readable copy of the attribution notices contained 147 | within such NOTICE file, excluding those notices that do not 148 | pertain to any part of the Derivative Works, in at least one 149 | of the following places: within a NOTICE text file distributed 150 | as part of the Derivative Works; within the Source form or 151 | documentation, if provided along with the Derivative Works; or, 152 | within a display generated by the Derivative Works, if and 153 | wherever such third-party notices normally appear. The contents 154 | of the NOTICE file are for informational purposes only and 155 | do not modify the License. You may add Your own attribution 156 | notices within Derivative Works that You distribute, alongside 157 | or as an addendum to the NOTICE text from the Work, provided 158 | that such additional attribution notices cannot be construed 159 | as modifying the License. 160 | 161 | You may add Your own copyright statement to Your modifications and 162 | may provide additional or different license terms and conditions 163 | for use, reproduction, or distribution of Your modifications, or 164 | for any such Derivative Works as a whole, provided Your use, 165 | reproduction, and distribution of the Work otherwise complies with 166 | the conditions stated in this License. 167 | 168 | 5. Submission of Contributions. Unless You explicitly state otherwise, 169 | any Contribution intentionally submitted for inclusion in the Work 170 | by You to the Licensor shall be under the terms and conditions of 171 | this License, without any additional terms or conditions. 172 | Notwithstanding the above, nothing herein shall supersede or modify 173 | the terms of any separate license agreement you may have executed 174 | with Licensor regarding such Contributions. 175 | 176 | 6. Trademarks. This License does not grant permission to use the trade 177 | names, trademarks, service marks, or product names of the Licensor, 178 | except as required for reasonable and customary use in describing the 179 | origin of the Work and reproducing the content of the NOTICE file. 180 | 181 | 7. Disclaimer of Warranty. Unless required by applicable law or 182 | agreed to in writing, Licensor provides the Work (and each 183 | Contributor provides its Contributions) on an "AS IS" BASIS, 184 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 185 | implied, including, without limitation, any warranties or conditions 186 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 187 | PARTICULAR PURPOSE. You are solely responsible for determining the 188 | appropriateness of using or redistributing the Work and assume any 189 | risks associated with Your exercise of permissions under this License. 190 | 191 | 8. Limitation of Liability. In no event and under no legal theory, 192 | whether in tort (including negligence), contract, or otherwise, 193 | unless required by applicable law (such as deliberate and grossly 194 | negligent acts) or agreed to in writing, shall any Contributor be 195 | liable to You for damages, including any direct, indirect, special, 196 | incidental, or consequential damages of any character arising as a 197 | result of this License or out of the use or inability to use the 198 | Work (including but not limited to damages for loss of goodwill, 199 | work stoppage, computer failure or malfunction, or any and all 200 | other commercial damages or losses), even if such Contributor 201 | has been advised of the possibility of such damages. 202 | 203 | 9. Accepting Warranty or Additional Liability. While redistributing 204 | the Work or Derivative Works thereof, You may choose to offer, 205 | and charge a fee for, acceptance of support, warranty, indemnity, 206 | or other liability obligations and/or rights consistent with this 207 | License. However, in accepting such obligations, You may act only 208 | on Your own behalf and on Your sole responsibility, not on behalf 209 | of any other Contributor, and only if You agree to indemnify, 210 | defend, and hold each Contributor harmless for any liability 211 | incurred by, or claims asserted against, such Contributor by reason 212 | of your accepting any such warranty or additional liability. 213 | 214 | END OF TERMS AND CONDITIONS 215 | -------------------------------------------------------------------------------- /rustls-cert-gen/README.md: -------------------------------------------------------------------------------- 1 | # rustls-cert-gen 2 | 3 | `rustls-cert-gen` is a tool to generate TLS certificates. In its 4 | current state it will generate a Root CA and an end-entity 5 | certificate, along with private keys. The end-entity certificate will 6 | be signed by the Root CA. 7 | 8 | ## Usage 9 | Having compiled the binary you can simply pass a path to output 10 | generated files. 11 | 12 | cargo run -- -o output/dir 13 | 14 | In the output directory you will find these files: 15 | 16 | * `cert.pem` (end-entity's X.509 certificate, signed by `root-ca`'s key) 17 | * `cert.key.pem` (end-entity's private key) 18 | * `root-ca.pem` (ca's self-signed X.509 certificate) 19 | 20 | For a complete list of supported options: 21 | 22 | rustls-cert-gen --help 23 | 24 | ## FAQ 25 | 26 | #### What signature schemes are available? 27 | 28 | * `pkcs_ecdsa_p256_sha256` 29 | * `pkcs_ecdsa_p384_sha384` 30 | * `pkcs_ed25519` 31 | -------------------------------------------------------------------------------- /rustls-cert-gen/src/cert.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, fs::File, io, path::Path}; 2 | 3 | use bpaf::Bpaf; 4 | use rcgen::{ 5 | BasicConstraints, Certificate, CertificateParams, DistinguishedName, DnType, 6 | DnValue::PrintableString, ExtendedKeyUsagePurpose, IsCa, KeyPair, KeyUsagePurpose, SanType, 7 | }; 8 | 9 | #[cfg(feature = "aws_lc_rs")] 10 | use aws_lc_rs as ring_like; 11 | #[cfg(all(feature = "ring", not(feature = "aws_lc_rs")))] 12 | use ring as ring_like; 13 | 14 | #[derive(Debug, Clone)] 15 | /// PEM serialized Certificate and PEM serialized corresponding private key 16 | pub struct PemCertifiedKey { 17 | pub cert_pem: String, 18 | pub private_key_pem: String, 19 | } 20 | 21 | impl PemCertifiedKey { 22 | pub fn write(&self, dir: &Path, name: &str) -> Result<(), io::Error> { 23 | use std::io::Write; 24 | std::fs::create_dir_all(dir)?; 25 | 26 | let key_path = dir.join(format!("{name}.key.pem")); 27 | let mut key_out = File::create(key_path)?; 28 | write!(key_out, "{}", &self.private_key_pem)?; 29 | 30 | let cert_path = dir.join(format!("{name}.pem")); 31 | let mut cert_out = File::create(cert_path)?; 32 | write!(cert_out, "{}", &self.cert_pem)?; 33 | 34 | Ok(()) 35 | } 36 | } 37 | 38 | /// Builder to configure TLS [CertificateParams] to be finalized 39 | /// into either a [Ca] or an [EndEntity]. 40 | #[derive(Clone, Debug, Default)] 41 | pub struct CertificateBuilder { 42 | params: CertificateParams, 43 | alg: KeyPairAlgorithm, 44 | } 45 | 46 | impl CertificateBuilder { 47 | /// Initialize `CertificateParams` with defaults 48 | /// # Example 49 | /// ``` 50 | /// # use rustls_cert_gen::CertificateBuilder; 51 | /// let cert = CertificateBuilder::new(); 52 | /// ``` 53 | pub fn new() -> Self { 54 | let mut params = CertificateParams::default(); 55 | // override default Common Name 56 | params.distinguished_name = DistinguishedName::new(); 57 | Self { 58 | params, 59 | alg: KeyPairAlgorithm::EcdsaP256, 60 | } 61 | } 62 | /// Set signature algorithm (instead of default). 63 | pub fn signature_algorithm(mut self, alg: KeyPairAlgorithm) -> anyhow::Result { 64 | self.alg = alg; 65 | Ok(self) 66 | } 67 | /// Set options for Ca Certificates 68 | /// # Example 69 | /// ``` 70 | /// # use rustls_cert_gen::CertificateBuilder; 71 | /// let cert = CertificateBuilder::new().certificate_authority(); 72 | /// ``` 73 | pub fn certificate_authority(self) -> CaBuilder { 74 | CaBuilder::new(self.params, self.alg) 75 | } 76 | /// Set options for `EndEntity` Certificates 77 | pub fn end_entity(self) -> EndEntityBuilder { 78 | EndEntityBuilder::new(self.params, self.alg) 79 | } 80 | } 81 | 82 | /// [CertificateParams] from which an [Ca] [Certificate] can be built 83 | #[derive(Clone, Debug)] 84 | pub struct CaBuilder { 85 | params: CertificateParams, 86 | alg: KeyPairAlgorithm, 87 | } 88 | 89 | impl CaBuilder { 90 | /// Initialize `CaBuilder` 91 | pub fn new(mut params: CertificateParams, alg: KeyPairAlgorithm) -> Self { 92 | params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); 93 | params.key_usages.push(KeyUsagePurpose::DigitalSignature); 94 | params.key_usages.push(KeyUsagePurpose::KeyCertSign); 95 | params.key_usages.push(KeyUsagePurpose::CrlSign); 96 | Self { params, alg } 97 | } 98 | /// Add CountryName to `distinguished_name`. Multiple calls will 99 | /// replace previous value. 100 | pub fn country_name(mut self, country: &str) -> Result { 101 | self.params 102 | .distinguished_name 103 | .push(DnType::CountryName, PrintableString(country.try_into()?)); 104 | Ok(self) 105 | } 106 | /// Add OrganizationName to `distinguished_name`. Multiple calls will 107 | /// replace previous value. 108 | pub fn organization_name(mut self, name: &str) -> Self { 109 | self.params 110 | .distinguished_name 111 | .push(DnType::OrganizationName, name); 112 | self 113 | } 114 | /// build `Ca` Certificate. 115 | pub fn build(self) -> Result { 116 | let key_pair = self.alg.to_key_pair()?; 117 | let cert = self.params.self_signed(&key_pair)?; 118 | Ok(Ca { 119 | params: self.params, 120 | cert, 121 | key_pair, 122 | }) 123 | } 124 | } 125 | 126 | /// End-entity [Certificate] 127 | pub struct Ca { 128 | cert: Certificate, 129 | params: CertificateParams, 130 | key_pair: KeyPair, 131 | } 132 | 133 | impl Ca { 134 | /// Self-sign and serialize 135 | pub fn serialize_pem(&self) -> PemCertifiedKey { 136 | PemCertifiedKey { 137 | cert_pem: self.cert.pem(), 138 | private_key_pem: self.key_pair.serialize_pem(), 139 | } 140 | } 141 | /// Return `&Certificate` 142 | #[allow(dead_code)] 143 | pub fn cert(&self) -> &Certificate { 144 | &self.cert 145 | } 146 | } 147 | 148 | impl fmt::Debug for Ca { 149 | /// Formats the `Ca` information without revealing the key pair. 150 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 151 | // The key pair is omitted from the debug output as it contains secret information. 152 | let Ca { 153 | cert, 154 | params, 155 | key_pair, 156 | } = self; 157 | 158 | f.debug_struct("Ca") 159 | .field("cert", cert) 160 | .field("params", params) 161 | .field("key_pair", key_pair) 162 | .finish() 163 | } 164 | } 165 | 166 | /// End-entity [Certificate] 167 | pub struct EndEntity { 168 | cert: Certificate, 169 | key_pair: KeyPair, 170 | } 171 | 172 | impl EndEntity { 173 | /// Sign with `signer` and serialize. 174 | pub fn serialize_pem(&self) -> PemCertifiedKey { 175 | PemCertifiedKey { 176 | cert_pem: self.cert.pem(), 177 | private_key_pem: self.key_pair.serialize_pem(), 178 | } 179 | } 180 | } 181 | 182 | impl fmt::Debug for EndEntity { 183 | /// Formats the `EndEntity` information without revealing the key pair. 184 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 185 | // The key pair is omitted from the debug output as it contains secret information. 186 | let EndEntity { cert, key_pair } = self; 187 | 188 | f.debug_struct("EndEntity") 189 | .field("cert", cert) 190 | .field("key_pair", key_pair) 191 | .finish() 192 | } 193 | } 194 | 195 | /// [CertificateParams] from which an [EndEntity] [Certificate] can be built 196 | #[derive(Clone, Debug)] 197 | pub struct EndEntityBuilder { 198 | params: CertificateParams, 199 | alg: KeyPairAlgorithm, 200 | } 201 | 202 | impl EndEntityBuilder { 203 | /// Initialize `EndEntityBuilder` 204 | pub fn new(mut params: CertificateParams, alg: KeyPairAlgorithm) -> Self { 205 | params.is_ca = IsCa::NoCa; 206 | params.use_authority_key_identifier_extension = true; 207 | params.key_usages.push(KeyUsagePurpose::DigitalSignature); 208 | Self { params, alg } 209 | } 210 | /// Add CommonName to `distinguished_name`. Multiple calls will 211 | /// replace previous value. 212 | pub fn common_name(mut self, name: &str) -> Self { 213 | self.params 214 | .distinguished_name 215 | .push(DnType::CommonName, name); 216 | self 217 | } 218 | /// `SanTypes` that will be recorded as 219 | /// `subject_alt_names`. Multiple calls will append to previous 220 | /// values. 221 | pub fn subject_alternative_names(mut self, sans: Vec) -> Self { 222 | self.params.subject_alt_names.extend(sans); 223 | self 224 | } 225 | /// Add ClientAuth to `extended_key_usages` if it is not already present. 226 | pub fn client_auth(&mut self) -> &Self { 227 | let usage = ExtendedKeyUsagePurpose::ClientAuth; 228 | if !self.params.extended_key_usages.iter().any(|e| e == &usage) { 229 | self.params.extended_key_usages.push(usage); 230 | } 231 | self 232 | } 233 | /// Add ServerAuth to `extended_key_usages` if it is not already present. 234 | pub fn server_auth(&mut self) -> &Self { 235 | let usage = ExtendedKeyUsagePurpose::ServerAuth; 236 | if !self.params.extended_key_usages.iter().any(|e| e == &usage) { 237 | self.params.extended_key_usages.push(usage); 238 | } 239 | self 240 | } 241 | /// build `EndEntity` Certificate. 242 | pub fn build(self, issuer: &Ca) -> Result { 243 | let key_pair = self.alg.to_key_pair()?; 244 | let cert = self 245 | .params 246 | .signed_by(&key_pair, &issuer.params, &issuer.key_pair)?; 247 | Ok(EndEntity { cert, key_pair }) 248 | } 249 | } 250 | 251 | /// Supported Keypair Algorithms 252 | #[derive(Clone, Copy, Debug, Default, Bpaf, PartialEq)] 253 | pub enum KeyPairAlgorithm { 254 | Rsa, 255 | Ed25519, 256 | #[default] 257 | EcdsaP256, 258 | EcdsaP384, 259 | #[cfg(feature = "aws_lc_rs")] 260 | EcdsaP521, 261 | } 262 | 263 | impl KeyPairAlgorithm { 264 | /// Return an `rcgen::KeyPair` for the given varient 265 | fn to_key_pair(self) -> Result { 266 | match self { 267 | KeyPairAlgorithm::Rsa => rcgen::KeyPair::generate_for(&rcgen::PKCS_RSA_SHA256), 268 | KeyPairAlgorithm::Ed25519 => { 269 | use ring_like::signature::Ed25519KeyPair; 270 | 271 | let rng = ring_like::rand::SystemRandom::new(); 272 | let alg = &rcgen::PKCS_ED25519; 273 | let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rng) 274 | .map_err(|_| rcgen::Error::RingUnspecified)?; 275 | 276 | rcgen::KeyPair::from_pkcs8_der_and_sign_algo(&pkcs8_bytes.as_ref().into(), alg) 277 | }, 278 | KeyPairAlgorithm::EcdsaP256 => { 279 | use ring_like::signature::EcdsaKeyPair; 280 | use ring_like::signature::ECDSA_P256_SHA256_ASN1_SIGNING; 281 | 282 | let rng = ring_like::rand::SystemRandom::new(); 283 | let alg = &rcgen::PKCS_ECDSA_P256_SHA256; 284 | let pkcs8_bytes = 285 | EcdsaKeyPair::generate_pkcs8(&ECDSA_P256_SHA256_ASN1_SIGNING, &rng) 286 | .map_err(|_| rcgen::Error::RingUnspecified)?; 287 | rcgen::KeyPair::from_pkcs8_der_and_sign_algo(&pkcs8_bytes.as_ref().into(), alg) 288 | }, 289 | KeyPairAlgorithm::EcdsaP384 => { 290 | use ring_like::signature::EcdsaKeyPair; 291 | use ring_like::signature::ECDSA_P384_SHA384_ASN1_SIGNING; 292 | 293 | let rng = ring_like::rand::SystemRandom::new(); 294 | let alg = &rcgen::PKCS_ECDSA_P384_SHA384; 295 | let pkcs8_bytes = 296 | EcdsaKeyPair::generate_pkcs8(&ECDSA_P384_SHA384_ASN1_SIGNING, &rng) 297 | .map_err(|_| rcgen::Error::RingUnspecified)?; 298 | 299 | rcgen::KeyPair::from_pkcs8_der_and_sign_algo(&pkcs8_bytes.as_ref().into(), alg) 300 | }, 301 | #[cfg(feature = "aws_lc_rs")] 302 | KeyPairAlgorithm::EcdsaP521 => { 303 | use ring_like::signature::EcdsaKeyPair; 304 | use ring_like::signature::ECDSA_P521_SHA512_ASN1_SIGNING; 305 | 306 | let rng = ring_like::rand::SystemRandom::new(); 307 | let alg = &rcgen::PKCS_ECDSA_P521_SHA512; 308 | let pkcs8_bytes = 309 | EcdsaKeyPair::generate_pkcs8(&ECDSA_P521_SHA512_ASN1_SIGNING, &rng) 310 | .map_err(|_| rcgen::Error::RingUnspecified)?; 311 | 312 | rcgen::KeyPair::from_pkcs8_der_and_sign_algo(&pkcs8_bytes.as_ref().into(), alg) 313 | }, 314 | } 315 | } 316 | } 317 | 318 | impl fmt::Display for KeyPairAlgorithm { 319 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 320 | match self { 321 | KeyPairAlgorithm::Rsa => write!(f, "rsa"), 322 | KeyPairAlgorithm::Ed25519 => write!(f, "ed25519"), 323 | KeyPairAlgorithm::EcdsaP256 => write!(f, "ecdsa-p256"), 324 | KeyPairAlgorithm::EcdsaP384 => write!(f, "ecdsa-p384"), 325 | #[cfg(feature = "aws_lc_rs")] 326 | KeyPairAlgorithm::EcdsaP521 => write!(f, "ecdsa-p521"), 327 | } 328 | } 329 | } 330 | 331 | #[cfg(test)] 332 | mod tests { 333 | use x509_parser::prelude::{FromDer, X509Certificate}; 334 | 335 | use super::*; 336 | 337 | #[test] 338 | fn test_write_files() -> anyhow::Result<()> { 339 | use assert_fs::prelude::*; 340 | let temp = assert_fs::TempDir::new()?; 341 | let dir = temp.path(); 342 | let entity_cert = temp.child("cert.pem"); 343 | let entity_key = temp.child("cert.key.pem"); 344 | 345 | let pck = PemCertifiedKey { 346 | cert_pem: "x".into(), 347 | private_key_pem: "y".into(), 348 | }; 349 | 350 | pck.write(dir, "cert")?; 351 | 352 | // assert contents of created files 353 | entity_cert.assert("x"); 354 | entity_key.assert("y"); 355 | 356 | Ok(()) 357 | } 358 | #[test] 359 | fn init_ca() { 360 | let cert = CertificateBuilder::new().certificate_authority(); 361 | assert_eq!(cert.params.is_ca, IsCa::Ca(BasicConstraints::Unconstrained)) 362 | } 363 | #[test] 364 | fn with_sig_algo_default() -> anyhow::Result<()> { 365 | let end_entity = CertificateBuilder::new().end_entity(); 366 | 367 | assert_eq!(end_entity.alg, KeyPairAlgorithm::EcdsaP256); 368 | Ok(()) 369 | } 370 | #[test] 371 | fn serialize_end_entity_default_sig() -> anyhow::Result<()> { 372 | let ca = CertificateBuilder::new().certificate_authority().build()?; 373 | let end_entity = CertificateBuilder::new() 374 | .end_entity() 375 | .build(&ca)? 376 | .serialize_pem(); 377 | 378 | let der = pem::parse(end_entity.cert_pem)?; 379 | let (_, cert) = X509Certificate::from_der(der.contents())?; 380 | 381 | let issuer_der = pem::parse(ca.serialize_pem().cert_pem)?; 382 | let (_, issuer) = X509Certificate::from_der(issuer_der.contents())?; 383 | 384 | assert!(!cert.is_ca()); 385 | check_signature(&cert, &issuer); 386 | 387 | Ok(()) 388 | } 389 | #[test] 390 | fn serialize_end_entity_ecdsa_p384_sha384_sig() -> anyhow::Result<()> { 391 | let ca = CertificateBuilder::new().certificate_authority().build()?; 392 | let end_entity = CertificateBuilder::new() 393 | .signature_algorithm(KeyPairAlgorithm::EcdsaP384)? 394 | .end_entity() 395 | .build(&ca)? 396 | .serialize_pem(); 397 | 398 | let der = pem::parse(end_entity.cert_pem)?; 399 | let (_, cert) = X509Certificate::from_der(der.contents())?; 400 | 401 | let issuer_der = pem::parse(ca.serialize_pem().cert_pem)?; 402 | let (_, issuer) = X509Certificate::from_der(issuer_der.contents())?; 403 | 404 | check_signature(&cert, &issuer); 405 | Ok(()) 406 | } 407 | 408 | #[test] 409 | #[cfg(feature = "aws_lc_rs")] 410 | fn serialize_end_entity_ecdsa_p521_sha512_sig() -> anyhow::Result<()> { 411 | let ca = CertificateBuilder::new().certificate_authority().build()?; 412 | let end_entity = CertificateBuilder::new() 413 | .signature_algorithm(KeyPairAlgorithm::EcdsaP521)? 414 | .end_entity() 415 | .build(&ca)? 416 | .serialize_pem(); 417 | 418 | let der = pem::parse(end_entity.cert_pem)?; 419 | let (_, cert) = X509Certificate::from_der(der.contents())?; 420 | 421 | let issuer_der = pem::parse(ca.serialize_pem().cert_pem)?; 422 | let (_, issuer) = X509Certificate::from_der(issuer_der.contents())?; 423 | 424 | check_signature(&cert, &issuer); 425 | Ok(()) 426 | } 427 | 428 | #[test] 429 | fn serialize_end_entity_ed25519_sig() -> anyhow::Result<()> { 430 | let ca = CertificateBuilder::new().certificate_authority().build()?; 431 | let end_entity = CertificateBuilder::new() 432 | .signature_algorithm(KeyPairAlgorithm::Ed25519)? 433 | .end_entity() 434 | .build(&ca)? 435 | .serialize_pem(); 436 | 437 | let der = pem::parse(end_entity.cert_pem)?; 438 | let (_, cert) = X509Certificate::from_der(der.contents())?; 439 | 440 | let issuer_der = pem::parse(ca.serialize_pem().cert_pem)?; 441 | let (_, issuer) = X509Certificate::from_der(issuer_der.contents())?; 442 | 443 | check_signature(&cert, &issuer); 444 | Ok(()) 445 | } 446 | 447 | fn check_signature(cert: &X509Certificate<'_>, issuer: &X509Certificate<'_>) { 448 | let verified = cert.verify_signature(Some(issuer.public_key())).is_ok(); 449 | assert!(verified); 450 | } 451 | 452 | #[test] 453 | fn init_end_endity() { 454 | let params = CertificateParams::default(); 455 | let cert = EndEntityBuilder::new(params, KeyPairAlgorithm::default()); 456 | assert_eq!(cert.params.is_ca, IsCa::NoCa) 457 | } 458 | #[test] 459 | fn client_auth_end_entity() { 460 | let _ca = CertificateBuilder::new() 461 | .certificate_authority() 462 | .build() 463 | .unwrap(); 464 | let params = CertificateParams::default(); 465 | let mut cert = EndEntityBuilder::new(params, KeyPairAlgorithm::default()); 466 | assert_eq!(cert.params.is_ca, IsCa::NoCa); 467 | assert_eq!( 468 | cert.client_auth().params.extended_key_usages, 469 | vec![ExtendedKeyUsagePurpose::ClientAuth] 470 | ); 471 | } 472 | #[test] 473 | fn server_auth_end_entity() { 474 | let _ca = CertificateBuilder::new() 475 | .certificate_authority() 476 | .build() 477 | .unwrap(); 478 | let params = CertificateParams::default(); 479 | let mut cert = EndEntityBuilder::new(params, KeyPairAlgorithm::default()); 480 | assert_eq!(cert.params.is_ca, IsCa::NoCa); 481 | assert_eq!( 482 | cert.server_auth().params.extended_key_usages, 483 | vec![ExtendedKeyUsagePurpose::ServerAuth] 484 | ); 485 | } 486 | #[test] 487 | fn sans_end_entity() { 488 | let _ca = CertificateBuilder::new() 489 | .certificate_authority() 490 | .build() 491 | .unwrap(); 492 | let name = "unexpected.oomyoo.xyz"; 493 | let names = vec![SanType::DnsName(name.try_into().unwrap())]; 494 | let params = CertificateParams::default(); 495 | let cert = EndEntityBuilder::new(params, KeyPairAlgorithm::default()) 496 | .subject_alternative_names(names); 497 | assert_eq!( 498 | cert.params.subject_alt_names, 499 | vec![rcgen::SanType::DnsName(name.try_into().unwrap())] 500 | ); 501 | } 502 | #[test] 503 | fn sans_end_entity_empty() { 504 | let _ca = CertificateBuilder::new() 505 | .certificate_authority() 506 | .build() 507 | .unwrap(); 508 | let names = vec![]; 509 | let params = CertificateParams::default(); 510 | let cert = EndEntityBuilder::new(params, KeyPairAlgorithm::default()) 511 | .subject_alternative_names(names); 512 | assert_eq!(cert.params.subject_alt_names, vec![]); 513 | } 514 | 515 | #[test] 516 | fn key_pair_algorithm_to_keypair() -> anyhow::Result<()> { 517 | let keypair = KeyPairAlgorithm::Ed25519.to_key_pair()?; 518 | assert_eq!(format!("{:?}", keypair.algorithm()), "PKCS_ED25519"); 519 | let keypair = KeyPairAlgorithm::EcdsaP256.to_key_pair()?; 520 | assert_eq!( 521 | format!("{:?}", keypair.algorithm()), 522 | "PKCS_ECDSA_P256_SHA256" 523 | ); 524 | let keypair = KeyPairAlgorithm::EcdsaP384.to_key_pair()?; 525 | assert_eq!( 526 | format!("{:?}", keypair.algorithm()), 527 | "PKCS_ECDSA_P384_SHA384" 528 | ); 529 | 530 | #[cfg(feature = "aws_lc_rs")] 531 | { 532 | let keypair = KeyPairAlgorithm::EcdsaP521.to_key_pair()?; 533 | assert_eq!( 534 | format!("{:?}", keypair.algorithm()), 535 | "PKCS_ECDSA_P521_SHA512" 536 | ); 537 | } 538 | Ok(()) 539 | } 540 | } 541 | -------------------------------------------------------------------------------- /rustls-cert-gen/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![warn(missing_docs)] 2 | //! This library wraps [rcgen] to provide a simple API to generate TLS 3 | //! certificate-chains. Its primary intent is to ease development of 4 | //! applications that verify chains of trust. It can be used for 5 | //! whatever purpose you may need a TLS certificate-chain. 6 | 7 | #![warn(unreachable_pub)] 8 | 9 | mod cert; 10 | pub use cert::{Ca, CaBuilder, CertificateBuilder, EndEntity, EndEntityBuilder}; 11 | -------------------------------------------------------------------------------- /rustls-cert-gen/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{net::IpAddr, path::PathBuf, str::FromStr}; 2 | 3 | use bpaf::Bpaf; 4 | use rcgen::{Error, SanType}; 5 | 6 | mod cert; 7 | use cert::{key_pair_algorithm, CertificateBuilder, KeyPairAlgorithm}; 8 | 9 | fn main() -> anyhow::Result<()> { 10 | let opts = options().run(); 11 | 12 | let ca = CertificateBuilder::new() 13 | .signature_algorithm(opts.keypair_algorithm)? 14 | .certificate_authority() 15 | .country_name(&opts.country_name)? 16 | .organization_name(&opts.organization_name) 17 | .build()?; 18 | 19 | let mut entity = CertificateBuilder::new() 20 | .signature_algorithm(opts.keypair_algorithm)? 21 | .end_entity() 22 | .common_name(&opts.common_name) 23 | .subject_alternative_names(opts.san); 24 | 25 | if opts.client_auth { 26 | entity.client_auth(); 27 | }; 28 | 29 | if opts.server_auth { 30 | entity.server_auth(); 31 | }; 32 | 33 | entity 34 | .build(&ca)? 35 | .serialize_pem() 36 | .write(&opts.output, &opts.cert_file_name)?; 37 | 38 | ca.serialize_pem().write(&opts.output, &opts.ca_file_name)?; 39 | 40 | Ok(()) 41 | } 42 | 43 | #[derive(Clone, Debug, Bpaf)] 44 | #[bpaf(options)] 45 | /// rustls-cert-gen TLS Certificate Generator 46 | struct Options { 47 | /// Output directory for generated files 48 | #[bpaf(short, long, argument("output/path/"))] 49 | pub output: PathBuf, 50 | /// Keypair algorithm 51 | #[bpaf( 52 | external(key_pair_algorithm), 53 | fallback(KeyPairAlgorithm::EcdsaP256), 54 | display_fallback, 55 | group_help("Keypair Algorithm:") 56 | )] 57 | pub keypair_algorithm: KeyPairAlgorithm, 58 | /// Extended Key Usage Purpose: ClientAuth 59 | #[bpaf(long)] 60 | pub client_auth: bool, 61 | /// Extended Key Usage Purpose: ServerAuth 62 | #[bpaf(long)] 63 | pub server_auth: bool, 64 | /// Basename for end-entity cert/key 65 | #[bpaf(long, fallback("cert".into()), display_fallback)] 66 | pub cert_file_name: String, 67 | /// Basename for ca cert/key 68 | #[bpaf(long, fallback("root-ca".into()), display_fallback)] 69 | pub ca_file_name: String, 70 | /// Subject Alt Name (apply multiple times for multiple names/Ips) 71 | #[bpaf(many, long, argument::("san"), parse(parse_sans))] 72 | pub san: Vec, 73 | /// Common Name (Currently only used for end-entity) 74 | #[bpaf(long, fallback("Tls End-Entity Certificate".into()), display_fallback)] 75 | pub common_name: String, 76 | /// Country Name (Currently only used for ca) 77 | #[bpaf(long, fallback("BR".into()), display_fallback)] 78 | pub country_name: String, 79 | /// Organization Name (Currently only used for ca) 80 | #[bpaf(long, fallback("Crab widgits SE".into()), display_fallback)] 81 | pub organization_name: String, 82 | } 83 | 84 | /// Parse cli input into SanType. Try first `IpAddr`, if that fails 85 | /// declare it to be a DnsName. 86 | fn parse_sans(hosts: Vec) -> Result, Error> { 87 | hosts 88 | .into_iter() 89 | .map(|s| { 90 | Ok(match IpAddr::from_str(&s) { 91 | Ok(ip) => SanType::IpAddress(ip), 92 | Err(_) => SanType::DnsName(s.try_into()?), 93 | }) 94 | }) 95 | .collect() 96 | } 97 | 98 | #[cfg(test)] 99 | mod tests { 100 | use super::*; 101 | 102 | #[test] 103 | fn test_parse_san() { 104 | let hosts = vec![ 105 | "my.host.com", 106 | "localhost", 107 | "185.199.108.153", 108 | "2001:0db8:85a3:0000:0000:8a2e:0370:7334", 109 | ] 110 | .into_iter() 111 | .map(Into::into) 112 | .collect(); 113 | let sans: Vec = parse_sans(hosts).unwrap(); 114 | assert_eq!(SanType::DnsName("my.host.com".try_into().unwrap()), sans[0]); 115 | assert_eq!(SanType::DnsName("localhost".try_into().unwrap()), sans[1]); 116 | assert_eq!( 117 | SanType::IpAddress("185.199.108.153".parse().unwrap()), 118 | sans[2] 119 | ); 120 | assert_eq!( 121 | SanType::IpAddress("2001:0db8:85a3:0000:0000:8a2e:0370:7334".parse().unwrap()), 122 | sans[3] 123 | ); 124 | } 125 | } 126 | --------------------------------------------------------------------------------