From 373e4fe08b89e829e9d2ce6817eba19c9aed80bf Mon Sep 17 00:00:00 2001 From: aumetra Date: Mon, 16 May 2022 19:53:39 +0200 Subject: [PATCH] dsa: Add initial DSA implementation (#471) Adds an initial implementation of DSA (see #8) The following things work when tested against OpenSSL: - The generated keys are valid and can be imported and exported from/to their DER/PEM representation - Signatures generated by this library can be successfully verified - Signatures can be imported and exported from/into their DER representation - Signatures generated by OpenSSL can be successfully imported and verified --- .github/workflows/dsa.yml | 63 ++++++ Cargo.lock | 141 +++++++++++- Cargo.toml | 1 + README.md | 1 + dsa/.gitignore | 4 + dsa/Cargo.toml | 31 +++ dsa/LICENSE-APACHE | 201 +++++++++++++++++ dsa/LICENSE-MIT | 25 +++ dsa/README.md | 68 ++++++ dsa/examples/export.rs | 21 ++ dsa/examples/generate.rs | 8 + dsa/examples/sign.rs | 31 +++ dsa/src/components.rs | 103 +++++++++ dsa/src/consts.rs | 24 +++ dsa/src/generate/components.rs | 74 +++++++ dsa/src/generate/keypair.rs | 21 ++ dsa/src/generate/mod.rs | 32 +++ dsa/src/generate/secret_number.rs | 111 ++++++++++ dsa/src/lib.rs | 69 ++++++ dsa/src/private_key.rs | 200 +++++++++++++++++ dsa/src/public_key.rs | 145 +++++++++++++ dsa/src/sig.rs | 102 +++++++++ dsa/tests/components.rs | 25 +++ dsa/tests/deterministic.rs | 344 ++++++++++++++++++++++++++++++ dsa/tests/pems/params.pem | 14 ++ dsa/tests/pems/private.pem | 15 ++ dsa/tests/pems/public.pem | 20 ++ dsa/tests/private_key.rs | 72 +++++++ dsa/tests/public_key.rs | 50 +++++ dsa/tests/signature.rs | 89 ++++++++ 30 files changed, 2103 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/dsa.yml create mode 100644 dsa/.gitignore create mode 100644 dsa/Cargo.toml create mode 100644 dsa/LICENSE-APACHE create mode 100644 dsa/LICENSE-MIT create mode 100644 dsa/README.md create mode 100644 dsa/examples/export.rs create mode 100644 dsa/examples/generate.rs create mode 100644 dsa/examples/sign.rs create mode 100644 dsa/src/components.rs create mode 100644 dsa/src/consts.rs create mode 100644 dsa/src/generate/components.rs create mode 100644 dsa/src/generate/keypair.rs create mode 100644 dsa/src/generate/mod.rs create mode 100644 dsa/src/generate/secret_number.rs create mode 100644 dsa/src/lib.rs create mode 100644 dsa/src/private_key.rs create mode 100644 dsa/src/public_key.rs create mode 100644 dsa/src/sig.rs create mode 100644 dsa/tests/components.rs create mode 100644 dsa/tests/deterministic.rs create mode 100644 dsa/tests/pems/params.pem create mode 100644 dsa/tests/pems/private.pem create mode 100644 dsa/tests/pems/public.pem create mode 100644 dsa/tests/private_key.rs create mode 100644 dsa/tests/public_key.rs create mode 100644 dsa/tests/signature.rs diff --git a/.github/workflows/dsa.yml b/.github/workflows/dsa.yml new file mode 100644 index 00000000..6e8d7fc7 --- /dev/null +++ b/.github/workflows/dsa.yml @@ -0,0 +1,63 @@ +name: dsa +on: + pull_request: + paths: + - "dsa/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: dsa + +env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Dwarnings" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + target: + - thumbv7em-none-eabi + - wasm32-unknown-unknown + toolchain: + - 1.57.0 # MSRV + - stable + steps: + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + target: ${{ matrix.target }} + toolchain: ${{ matrix.toolchain }} + override: true + - run: cargo build --target ${{ matrix.target }} --release --no-default-features + + test: + strategy: + matrix: + platform: + - ubuntu-latest + - macos-latest + - windows-latest + toolchain: + - 1.57.0 # MSRV + - stable + runs-on: ${{ matrix.platform }} + steps: + - name: Enforce LF + working-directory: . + run: | + git config --global core.autocrlf false + git config --global core.eol lf + + - uses: actions/checkout@v2 + - uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.toolchain }} + override: true + - run: cargo test --release --no-default-features + - run: cargo test --release + - run: cargo test --release --all-features \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 1d86582c..bf44b02c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "base16ct" version = "0.1.1" @@ -134,6 +140,25 @@ dependencies = [ "subtle", ] +[[package]] +name = "dsa" +version = "0.0.1" +dependencies = [ + "digest 0.10.3", + "num-bigint-dig", + "num-traits", + "opaque-debug", + "paste", + "pkcs8", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rfc6979", + "sha1", + "sha2 0.10.2", + "signature", + "zeroize", +] + [[package]] name = "ecdsa" version = "0.14.1" @@ -179,7 +204,7 @@ checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" dependencies = [ "curve25519-dalek", "ed25519 1.4.0", - "rand", + "rand 0.7.3", "serde", "sha2 0.9.9", "zeroize", @@ -276,18 +301,86 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin", +] + [[package]] name = "libc" version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad5c14e80759d0939d013e6ca49930e59fc53dd8e5009132f76240c179380c09" +[[package]] +name = "libm" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33a33a362ce288760ec6a508b94caaec573ae7d3bbbd91b87aa0bad4456839db" + +[[package]] +name = "num-bigint-dig" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566d173b2f9406afbc5510a90925d5a2cd80cae4605631f1212303df265de011" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "opaque-debug" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" + [[package]] name = "pem-rfc7468" version = "0.6.0" @@ -339,11 +432,22 @@ checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" dependencies = [ "getrandom 0.1.16", "libc", - "rand_chacha", + "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -354,6 +458,16 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.3", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -430,6 +544,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + [[package]] name = "sha2" version = "0.9.9" @@ -464,6 +589,18 @@ dependencies = [ "rand_core 0.6.3", ] +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spki" version = "0.6.0" diff --git a/Cargo.toml b/Cargo.toml index e5e84f16..528cd4ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] resolver = "2" members = [ + "dsa", "ecdsa", "ed25519", "rfc6979" diff --git a/README.md b/README.md index e6216fc3..ee9e1daa 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ and can be easily used for bare-metal or lightweight WebAssembly programming. | Name | Algorithm | Crates.io | Documentation | Build | |-------------|-----------|-----------|---------------|-------| +| [`dsa`] | [DSA](https://en.wikipedia.org/wiki/Digital_Signature_Algorithm) | [![crates.io](https://img.shields.io/crates/v/dsa.svg)](https://crates.io/crates/dsa) | [![Documentation](https://docs.rs/dsa/badge.svg)](https://docs.rs/dsa) | [![dsa build](https://github.com/RustCrypto/signatures/workflows/dsa/badge.svg?branch=master&event=push)](https://github.com/RustCrypto/signatures/actions?query=workflow%3Adsa) | [`ecdsa`] | [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) | [![crates.io](https://img.shields.io/crates/v/ecdsa.svg)](https://crates.io/crates/ecdsa) | [![Documentation](https://docs.rs/ecdsa/badge.svg)](https://docs.rs/ecdsa) | [![ecdsa build](https://github.com/RustCrypto/signatures/workflows/ecdsa/badge.svg?branch=master&event=push)](https://github.com/RustCrypto/signatures/actions?query=workflow%3Aecdsa) | | [`ed25519`] | [Ed25519](https://en.wikipedia.org/wiki/EdDSA) | [![crates.io](https://img.shields.io/crates/v/ed25519.svg)](https://crates.io/crates/ed25519) | [![Documentation](https://docs.rs/ed25519/badge.svg)](https://docs.rs/ed25519) | [![ed25519 build](https://github.com/RustCrypto/signatures/workflows/ed25519/badge.svg?branch=master&event=push)](https://github.com/RustCrypto/signatures/actions?query=workflow%3Aed25519) | [`rfc6979`] | [RFC6979](https://datatracker.ietf.org/doc/html/rfc6979) | [![crates.io](https://img.shields.io/crates/v/rfc6979.svg)](https://crates.io/crates/rfc6979) | [![Documentation](https://docs.rs/rfc6979/badge.svg)](https://docs.rs/rfc6979) | [![rfc6979 build](https://github.com/RustCrypto/signatures/actions/workflows/rfc6979.yml/badge.svg)](https://github.com/RustCrypto/signatures/actions/workflows/rfc6979.yml) diff --git a/dsa/.gitignore b/dsa/.gitignore new file mode 100644 index 00000000..49068fad --- /dev/null +++ b/dsa/.gitignore @@ -0,0 +1,4 @@ +target/ +Cargo.lock +*.pem +*.der diff --git a/dsa/Cargo.toml b/dsa/Cargo.toml new file mode 100644 index 00000000..afd2fe20 --- /dev/null +++ b/dsa/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "dsa" +version = "0.0.1" +edition = "2021" +license = "Apache-2.0 OR MIT" +readme = "README.md" +categories = ["cryptography"] +keywords = ["crypto", "nist", "signature"] +rust-version = "1.57" + +[dependencies] +digest = "0.10.3" +num-bigint = { package = "num-bigint-dig", version = "0.8.1", default-features = false, features = ["prime", "rand", "zeroize"] } +num-traits = { version = "0.2.15", default-features = false } +opaque-debug = "0.3.0" +paste = "1.0.7" +pkcs8 = { version = "0.9.0", default-features = false, features = ["alloc"] } +rand = { version = "0.8.5", default-features = false } +rfc6979 = { version = "0.2.0", path = "../rfc6979" } +signature = { version = ">= 1.5.0, < 1.6.0", default-features = false, features = ["digest-preview", "rand-preview"] } +zeroize = { version = "1.5.5", default-features = false } + +[features] +default = [] + +[dev-dependencies] +pkcs8 = { version = "0.9.0", default-features = false, features = ["pem"] } +rand = "0.8.5" +rand_chacha = "0.3.1" +sha1 = "0.10.1" +sha2 = "0.10.2" diff --git a/dsa/LICENSE-APACHE b/dsa/LICENSE-APACHE new file mode 100644 index 00000000..c394d8ad --- /dev/null +++ b/dsa/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2018-2022 RustCrypto Developers + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/dsa/LICENSE-MIT b/dsa/LICENSE-MIT new file mode 100644 index 00000000..81a3d57a --- /dev/null +++ b/dsa/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018-2022 RustCrypto Developers + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without +limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software +is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions +of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/dsa/README.md b/dsa/README.md new file mode 100644 index 00000000..6b73b7a0 --- /dev/null +++ b/dsa/README.md @@ -0,0 +1,68 @@ +# [RustCrypto]: DSA + +[![crate][crate-image]][crate-link] +[![Docs][docs-image]][docs-link] +[![Build Status][build-image]][build-link] +![Apache2/MIT licensed][license-image] +![MSRV][rustc-image] +[![Project Chat][chat-image]][chat-link] + +[Digital Signature Algorithm (DSA)][1] as specified in +[FIPS 186-4][2] (Digital Signature Standard). + +[Documentation][docs-link] + +## About + +This crate provides an implementation of DSA in pure Rust. +It utilises the [`signature`] crate to provide an interface for creating and verifying signatures. + +## Minimum Supported Rust Version + +This crate requires **Rust 1.57** at a minimum. + +We may change the MSRV in the future, but it will be accompanied by a minor +version bump. + +## License + +All crates licensed under either of + + * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) + * [MIT license](http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[//]: # (badges) + +[crate-image]: https://buildstats.info/crate/dsa +[crate-link]: https://crates.io/crates/dsa +[docs-image]: https://docs.rs/dsa/badge.svg +[docs-link]: https://docs.rs/dsa/ +[build-image]: https://github.com/RustCrypto/signatures/actions/workflows/dsa.yml/badge.svg +[build-link]: https://github.com/RustCrypto/signatures/actions/workflows/dsa.yml +[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg +[rustc-image]: https://img.shields.io/badge/rustc-1.57+-blue.svg +[chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg +[chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/260048-signatures + +[//]: # (links) + +[RustCrypto]: https://github.com/RustCrypto + +[//]: # (footnotes) + +[1]: https://en.wikipedia.org/wiki/Digital_Signature_Algorithm +[2]: https://csrc.nist.gov/publications/detail/fips/186/4/final + +[//]: # (docs.rs definitions) + +[`signature`]: https://docs.rs/signature +[`signature::Signer`]: https://docs.rs/signature/latest/signature/trait.Signer.html +[`signature::Verifier`]: https://docs.rs/signature/latest/signature/trait.Verifier.html \ No newline at end of file diff --git a/dsa/examples/export.rs b/dsa/examples/export.rs new file mode 100644 index 00000000..7e9face7 --- /dev/null +++ b/dsa/examples/export.rs @@ -0,0 +1,21 @@ +use dsa::{consts::DSA_2048_256, Components, PrivateKey}; +use pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}; +use std::{fs::File, io::Write}; + +fn main() { + let mut rng = rand::thread_rng(); + let components = Components::generate(&mut rng, DSA_2048_256); + let private_key = PrivateKey::generate(&mut rng, components); + let public_key = private_key.public_key(); + + let private_key_bytes = private_key.to_pkcs8_pem(LineEnding::LF).unwrap(); + let public_key_bytes = public_key.to_public_key_pem(LineEnding::LF).unwrap(); + + let mut file = File::create("public.pem").unwrap(); + file.write_all(public_key_bytes.as_bytes()).unwrap(); + file.flush().unwrap(); + + let mut file = File::create("private.pem").unwrap(); + file.write_all(private_key_bytes.as_bytes()).unwrap(); + file.flush().unwrap(); +} diff --git a/dsa/examples/generate.rs b/dsa/examples/generate.rs new file mode 100644 index 00000000..03b47d07 --- /dev/null +++ b/dsa/examples/generate.rs @@ -0,0 +1,8 @@ +use dsa::{consts::DSA_2048_256, Components, PrivateKey}; + +fn main() { + let mut rng = rand::thread_rng(); + let components = Components::generate(&mut rng, DSA_2048_256); + let private_key = PrivateKey::generate(&mut rng, components); + let _public_key = private_key.public_key(); +} diff --git a/dsa/examples/sign.rs b/dsa/examples/sign.rs new file mode 100644 index 00000000..27c38534 --- /dev/null +++ b/dsa/examples/sign.rs @@ -0,0 +1,31 @@ +use digest::Digest; +use dsa::{consts::DSA_2048_256, Components, PrivateKey}; +use pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}; +use sha1::Sha1; +use signature::{RandomizedDigestSigner, Signature}; +use std::{fs::File, io::Write}; + +fn main() { + let mut rng = rand::thread_rng(); + let components = Components::generate(&mut rng, DSA_2048_256); + let private_key = PrivateKey::generate(&mut rng, components); + let public_key = private_key.public_key(); + + let signature = private_key + .sign_digest_with_rng(rand::thread_rng(), Sha1::new().chain_update(b"hello world")); + + let private_key_bytes = private_key.to_pkcs8_pem(LineEnding::LF).unwrap(); + let public_key_bytes = public_key.to_public_key_pem(LineEnding::LF).unwrap(); + + let mut file = File::create("public.pem").unwrap(); + file.write_all(public_key_bytes.as_bytes()).unwrap(); + file.flush().unwrap(); + + let mut file = File::create("signature.der").unwrap(); + file.write_all(signature.as_bytes()).unwrap(); + file.flush().unwrap(); + + let mut file = File::create("private.pem").unwrap(); + file.write_all(private_key_bytes.as_bytes()).unwrap(); + file.flush().unwrap(); +} diff --git a/dsa/src/components.rs b/dsa/src/components.rs new file mode 100644 index 00000000..618c945f --- /dev/null +++ b/dsa/src/components.rs @@ -0,0 +1,103 @@ +//! +//! Module containing the definition of the common components container +//! + +use crate::two; +use num_bigint::BigUint; +use num_traits::One; +use pkcs8::der::{self, asn1::UIntRef, DecodeValue, Encode, Header, Reader, Sequence}; +use rand::{CryptoRng, RngCore}; + +/// The common components of an DSA keypair +/// +/// (the prime p, quotient q and generator g) +#[derive(Clone, PartialEq, PartialOrd)] +#[must_use] +pub struct Components { + /// Prime p + p: BigUint, + + /// Quotient q + q: BigUint, + + /// Generator g + g: BigUint, +} + +opaque_debug::implement!(Components); + +impl Components { + /// Construct the common components container from its inner values (p, q and g) + /// + /// These values are not getting verified for validity + pub const fn from_components(p: BigUint, q: BigUint, g: BigUint) -> Self { + Self { p, q, g } + } + + /// Generate a new pair of common components + /// + /// Please only use the parameter sizes defined by NIST. + /// We allow you to plug in any numbers you want but just because you can doesn't mean you should! + pub fn generate(rng: &mut R, size_param: (u32, u32)) -> Self { + let (p, q, g) = crate::generate::common_components(rng, size_param); + Self::from_components(p, q, g) + } + + /// DSA prime p + #[must_use] + pub const fn p(&self) -> &BigUint { + &self.p + } + + /// DSA quotient q + #[must_use] + pub const fn q(&self) -> &BigUint { + &self.q + } + + /// DSA generator g + #[must_use] + pub const fn g(&self) -> &BigUint { + &self.g + } + + /// Check whether the components are valid + #[must_use] + pub fn is_valid(&self) -> bool { + *self.p() >= two() + && *self.q() >= two() + && *self.g() >= BigUint::one() + && self.g() < self.p() + } +} + +impl<'a> DecodeValue<'a> for Components { + fn decode_value>(reader: &mut R, _header: Header) -> der::Result { + let p = reader.decode::>()?; + let q = reader.decode::>()?; + let g = reader.decode::>()?; + + let p = BigUint::from_bytes_be(p.as_bytes()); + let q = BigUint::from_bytes_be(q.as_bytes()); + let g = BigUint::from_bytes_be(g.as_bytes()); + + Ok(Self::from_components(p, q, g)) + } +} + +impl<'a> Sequence<'a> for Components { + fn fields(&self, encoder: F) -> der::Result + where + F: FnOnce(&[&dyn Encode]) -> der::Result, + { + let p_bytes = self.p.to_bytes_be(); + let q_bytes = self.q.to_bytes_be(); + let g_bytes = self.g.to_bytes_be(); + + let p = UIntRef::new(&p_bytes)?; + let q = UIntRef::new(&q_bytes)?; + let g = UIntRef::new(&g_bytes)?; + + encoder(&[&p, &q, &g]) + } +} diff --git a/dsa/src/consts.rs b/dsa/src/consts.rs new file mode 100644 index 00000000..3f648828 --- /dev/null +++ b/dsa/src/consts.rs @@ -0,0 +1,24 @@ +//! +//! DSA-related constants (like parameter sizes) +//! + +macro_rules! define_param_size { + ($l:literal, $n:literal) => { + ::paste::paste! { + #[doc = "DSA paramter size constant; L = " $l ", N = " $n] + pub const []: (u32, u32) = ($l, $n); + } + }; + (deprecated: $l:literal, $n:literal) => { + ::paste::paste! { + #[deprecated(note="This size constant has a security strength of under 112 bits per SP 800-57 Part 1 Rev. 5")] + #[doc = "DSA paramter size constant; L = " $l ", N = " $n] + pub const []: (u32, u32) = ($l, $n); + } + }; +} + +define_param_size!(deprecated: 1024, 160); +define_param_size!(2048, 224); +define_param_size!(2048, 256); +define_param_size!(3072, 256); diff --git a/dsa/src/generate/components.rs b/dsa/src/generate/components.rs new file mode 100644 index 00000000..c0f2a13a --- /dev/null +++ b/dsa/src/generate/components.rs @@ -0,0 +1,74 @@ +//! +//! Generate DSA key components +//! + +use crate::{ + generate::{calculate_bounds, generate_prime}, + two, Components, +}; +use num_bigint::{prime::probably_prime, BigUint, RandBigInt}; +use num_traits::One; +use rand::{CryptoRng, RngCore}; + +/// Numbers of miller-rabin rounds performed to determine primality +const MR_ROUNDS: usize = 64; + +/// Generate the common components p, q, and g +/// +/// # Returns +/// +/// Tuple of three `BigUint`s. Ordered like this `(p, q, g)` +pub fn common(rng: &mut R, (l, n): (u32, u32)) -> (BigUint, BigUint, BigUint) +where + R: CryptoRng + RngCore + ?Sized, +{ + // Calculate the lower and upper bounds of p and q + let (p_min, p_max) = calculate_bounds(l); + let (q_min, q_max) = calculate_bounds(n); + + let (p, q) = 'gen_pq: loop { + let q = generate_prime(n as usize, rng); + if q < q_min || q > q_max { + continue; + } + + // Attempt to find a prime p which has a subgroup of the order q + for _ in 0..4096 { + let m = 'gen_m: loop { + let m = rng.gen_biguint(l as usize); + if m > p_min && m < p_max { + break 'gen_m m; + } + }; + let mr = &m % (two() * &q); + let p = m - mr + BigUint::one(); + + if probably_prime(&p, MR_ROUNDS) { + break 'gen_pq (p, q); + } + } + }; + + // Generate g using the unverifiable method as defined by Appendix A.2.1 + let e = (&p - BigUint::one()) / &q; + let mut h = BigUint::one(); + let g = loop { + let g = h.modpow(&e, &p); + if !g.is_one() { + break g; + } + + h += BigUint::one(); + }; + + (p, q, g) +} + +/// Calculate the public component from the common components and the private component +#[inline] +pub fn public(components: &Components, x: &BigUint) -> BigUint { + let p = components.p(); + let g = components.g(); + + g.modpow(x, p) +} diff --git a/dsa/src/generate/keypair.rs b/dsa/src/generate/keypair.rs new file mode 100644 index 00000000..75875451 --- /dev/null +++ b/dsa/src/generate/keypair.rs @@ -0,0 +1,21 @@ +//! +//! Generate a DSA keypair +//! + +use crate::{generate::components, Components, PrivateKey, PublicKey}; +use num_bigint::{BigUint, RandBigInt}; +use num_traits::One; +use rand::{CryptoRng, RngCore}; + +/// Generate a new keypair +#[inline] +pub fn keypair(rng: &mut R, components: Components) -> PrivateKey +where + R: CryptoRng + RngCore + ?Sized, +{ + let x = rng.gen_biguint_range(&BigUint::one(), components.q()); + let y = components::public(&components, &x); + + let public_key = PublicKey::from_components(components, y); + PrivateKey::from_components(public_key, x) +} diff --git a/dsa/src/generate/mod.rs b/dsa/src/generate/mod.rs new file mode 100644 index 00000000..84483a8c --- /dev/null +++ b/dsa/src/generate/mod.rs @@ -0,0 +1,32 @@ +use crate::two; +use num_bigint::{BigUint, RandPrime}; +use num_traits::Pow; +use rand::{CryptoRng, RngCore}; + +mod components; +mod keypair; +mod secret_number; + +pub use self::components::{common as common_components, public as public_component}; +pub use self::keypair::keypair; +pub use self::secret_number::{secret_number, secret_number_rfc6979}; + +/// Calculate the upper and lower bounds for generating values like p or q +#[inline] +fn calculate_bounds(size: u32) -> (BigUint, BigUint) { + let lower = two().pow(size - 1); + let upper = two().pow(size); + + (lower, upper) +} + +/// Generate a prime number using a cryptographically secure pseudo-random number generator +/// +/// This wrapper function mainly exists to enforce the [`CryptoRng`](rand::CryptoRng) requirement (I might otherwise forget it) +#[inline] +fn generate_prime(bit_length: usize, rng: &mut R) -> BigUint +where + R: CryptoRng + RngCore + ?Sized, +{ + rng.gen_prime(bit_length) +} diff --git a/dsa/src/generate/secret_number.rs b/dsa/src/generate/secret_number.rs new file mode 100644 index 00000000..8a358556 --- /dev/null +++ b/dsa/src/generate/secret_number.rs @@ -0,0 +1,111 @@ +//! +//! Generate a per-message secret number +//! + +use crate::{Components, PrivateKey}; +use alloc::{vec, vec::Vec}; +use core::cmp::min; +use digest::{ + block_buffer::Eager, + consts::U256, + core_api::{BlockSizeUser, BufferKindUser, CoreProxy, FixedOutputCore}, + typenum::{IsLess, Le, NonZero}, + FixedOutput, HashMarker, OutputSizeUser, +}; +use num_bigint::{BigUint, ModInverse, RandBigInt}; +use num_traits::{One, Zero}; +use rand::{CryptoRng, RngCore}; +use rfc6979::HmacDrbg; +use zeroize::Zeroize; + +/// Reduce the hash into an RFC-6979 appropriate form +fn reduce_hash(q: &BigUint, hash: &[u8]) -> Vec { + // Reduce the hash modulo Q + let q_byte_len = q.bits() / 8; + + let hash_len = min(hash.len(), q_byte_len); + let hash = &hash[..hash_len]; + + let hash = BigUint::from_bytes_be(hash); + let mut reduced = (hash % q).to_bytes_be(); + + while reduced.len() < q_byte_len { + reduced.insert(0, 0); + } + + reduced +} + +/// Generate a per-message secret number k deterministically using the method described in RFC 6979 +/// +/// # Returns +/// +/// Secret number k and its modular multiplicative inverse with q +#[inline] +pub fn secret_number_rfc6979(private_key: &PrivateKey, hash: &[u8]) -> (BigUint, BigUint) +where + D: CoreProxy + FixedOutput, + D::Core: BlockSizeUser + + BufferKindUser + + Clone + + Default + + FixedOutputCore + + HashMarker + + OutputSizeUser, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, +{ + let q = private_key.public_key().components().q(); + let k_size = q.bits() / 8; + let hash = reduce_hash(q, hash); + + let mut x_bytes = private_key.x().to_bytes_be(); + let mut hmac = HmacDrbg::::new(&x_bytes, &hash, &[]); + x_bytes.zeroize(); + + let mut buffer = vec![0; k_size]; + loop { + hmac.fill_bytes(&mut buffer); + + let k = BigUint::from_bytes_be(&buffer); + if let Some(inv_k) = (&k).mod_inverse(q) { + let inv_k = inv_k.to_biguint().unwrap(); + + if k > BigUint::zero() && &k < q { + return (k, inv_k); + } + } + } +} + +/// Generate a per-message secret number k according to Appendix B.2.1 +/// +/// # Returns +/// +/// Secret number k and its modular multiplicative inverse with q +#[inline] +pub fn secret_number(rng: &mut R, components: &Components) -> Option<(BigUint, BigUint)> +where + R: CryptoRng + RngCore + ?Sized, +{ + let q = components.q(); + let n = q.bits(); + + // Attempt to try a fitting secret number + // Give up after 4096 tries + for _ in 0..4096 { + let c = rng.gen_biguint(n + 64); + let k = (c % (q - BigUint::one())) + BigUint::one(); + + if let Some(inv_k) = (&k).mod_inverse(q) { + let inv_k = inv_k.to_biguint().unwrap(); + + // `k` and `k^-1` both have to be in the range `[1, q-1]` + if (inv_k > BigUint::zero() && &inv_k < q) && (k > BigUint::zero() && &k < q) { + return Some((k, inv_k)); + } + } + } + + None +} diff --git a/dsa/src/lib.rs b/dsa/src/lib.rs new file mode 100644 index 00000000..8b329ca9 --- /dev/null +++ b/dsa/src/lib.rs @@ -0,0 +1,69 @@ +#![no_std] +#![forbid(unsafe_code)] +#![warn(missing_docs, rust_2018_idioms)] +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg" +)] + +//! +//! Generate a DSA keypair +//! +//! ``` +//! # use dsa::{consts::DSA_2048_256, Components, PrivateKey}; +//! let mut csprng = rand::thread_rng(); +//! let components = Components::generate(&mut csprng, DSA_2048_256); +//! let private_key = PrivateKey::generate(&mut csprng, components); +//! let public_key = private_key.public_key(); +//! ``` +//! +//! Create keypair from existing components +//! +//! ``` +//! # use dsa::{Components, PrivateKey, PublicKey}; +//! # use num_bigint::BigUint; +//! # use num_traits::One; +//! # let read_common_parameters = || (BigUint::one(), BigUint::one(), BigUint::one()); +//! # let read_public_component = || BigUint::one(); +//! # let read_private_component = || BigUint::one(); +//! let (p, q, g) = read_common_parameters(); +//! let components = Components::from_components(p, q, g); +//! +//! let x = read_public_component(); +//! let public_key = PublicKey::from_components(components, x); +//! +//! let y = read_private_component(); +//! let private_key = PrivateKey::from_components(public_key, y); +//! ``` +//! + +extern crate alloc; + +/// DSA object identifier as defined by RFC-3279, section 2.3.2 +const DSA_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.10040.4.1"); + +pub use self::components::Components; +pub use self::private_key::PrivateKey; +pub use self::public_key::PublicKey; +pub use self::sig::Signature; + +pub use pkcs8; +pub use signature; + +pub mod consts; + +use num_bigint::BigUint; +use pkcs8::spki::ObjectIdentifier; + +mod components; +mod generate; +mod private_key; +mod public_key; +mod sig; + +/// Returns a `BigUint` with the value 2 +#[inline] +fn two() -> BigUint { + BigUint::from(2_u8) +} diff --git a/dsa/src/private_key.rs b/dsa/src/private_key.rs new file mode 100644 index 00000000..65cabde1 --- /dev/null +++ b/dsa/src/private_key.rs @@ -0,0 +1,200 @@ +//! +//! Module containing the definition of the private key container +//! + +use crate::{sig::Signature, Components, PublicKey, DSA_OID}; +use core::cmp::min; +use digest::{ + block_buffer::Eager, + consts::U256, + core_api::{BlockSizeUser, BufferKindUser, CoreProxy, FixedOutputCore}, + typenum::{IsLess, Le, NonZero}, + Digest, FixedOutput, HashMarker, OutputSizeUser, +}; +use num_bigint::BigUint; +use num_traits::One; +use pkcs8::{ + der::{asn1::UIntRef, AnyRef, Decode, Encode}, + AlgorithmIdentifier, DecodePrivateKey, EncodePrivateKey, PrivateKeyInfo, SecretDocument, +}; +use rand::{CryptoRng, RngCore}; +use signature::{DigestSigner, RandomizedDigestSigner}; +use zeroize::Zeroizing; + +/// DSA private key +/// +/// The [`(try_)sign_digest_with_rng`](::signature::RandomizedDigestSigner) API uses regular non-deterministic signatures, +/// while the [`(try_)sign_digest`](::signature::DigestSigner) API uses deterministic signatures as described in RFC 6979 +#[derive(Clone, PartialEq)] +#[must_use] +pub struct PrivateKey { + /// Public key + public_key: PublicKey, + + /// Private component x + x: Zeroizing, +} + +opaque_debug::implement!(PrivateKey); + +impl PrivateKey { + /// Construct a new private key from the public key and private component + /// + /// These values are not getting verified for validity + pub fn from_components(public_key: PublicKey, x: BigUint) -> Self { + Self { + public_key, + x: Zeroizing::new(x), + } + } + + /// Generate a new DSA keypair + #[inline] + pub fn generate( + rng: &mut R, + components: Components, + ) -> PrivateKey { + crate::generate::keypair(rng, components) + } + + /// DSA public key + pub const fn public_key(&self) -> &PublicKey { + &self.public_key + } + + /// DSA private component + /// + /// If you decide to clone this value, please consider using [`Zeroize::zeroize`](::zeroize::Zeroize::zeroize()) to zero out the memory after you're done using the clone + #[must_use] + pub fn x(&self) -> &BigUint { + &self.x + } + + /// Check whether the private key is valid + #[must_use] + pub fn is_valid(&self) -> bool { + if !self.public_key().is_valid() { + return false; + } + + *self.x() >= BigUint::one() && self.x() < self.public_key().components().q() + } + + /// Sign some pre-hashed data + fn sign_prehashed(&self, (k, inv_k): (BigUint, BigUint), hash: &[u8]) -> Option { + // Refuse to sign with an invalid key + if !self.is_valid() { + return None; + } + + let components = self.public_key().components(); + let (p, q, g) = (components.p(), components.q(), components.g()); + let x = self.x(); + + let r = g.modpow(&k, p) % q; + + let n = (q.bits() / 8) as usize; + let block_size = hash.len(); // Hash function output size + + let z_len = min(n, block_size); + let z = BigUint::from_bytes_be(&hash[..z_len]); + + let s = (inv_k * (z + x * &r)) % q; + + Some(Signature::from_components(r, s)) + } +} + +impl DigestSigner for PrivateKey +where + D: Digest + CoreProxy + FixedOutput, + D::Core: BlockSizeUser + + BufferKindUser + + Clone + + Default + + FixedOutputCore + + HashMarker + + OutputSizeUser, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, +{ + fn try_sign_digest(&self, digest: D) -> Result { + let hash = digest.finalize_fixed(); + let ks = crate::generate::secret_number_rfc6979::(self, &hash); + + self.sign_prehashed(ks, &hash) + .ok_or_else(signature::Error::new) + } +} + +impl RandomizedDigestSigner for PrivateKey +where + D: Digest, +{ + fn try_sign_digest_with_rng( + &self, + mut rng: impl CryptoRng + RngCore, + digest: D, + ) -> Result { + let ks = crate::generate::secret_number(&mut rng, self.public_key().components()) + .ok_or_else(signature::Error::new)?; + let hash = digest.finalize(); + + self.sign_prehashed(ks, &hash) + .ok_or_else(signature::Error::new) + } +} + +impl EncodePrivateKey for PrivateKey { + fn to_pkcs8_der(&self) -> pkcs8::Result { + let parameters = self.public_key().components().to_vec()?; + let parameters = AnyRef::from_der(¶meters)?; + let algorithm = AlgorithmIdentifier { + oid: DSA_OID, + parameters: Some(parameters), + }; + + let x_bytes = self.x.to_bytes_be(); + let x = UIntRef::new(&x_bytes)?; + let private_key = x.to_vec()?; + + let private_key_info = PrivateKeyInfo::new(algorithm, &private_key); + private_key_info.try_into() + } +} + +impl<'a> TryFrom> for PrivateKey { + type Error = pkcs8::Error; + + fn try_from(value: PrivateKeyInfo<'a>) -> Result { + value.algorithm.assert_algorithm_oid(DSA_OID)?; + + let parameters = value.algorithm.parameters_any()?; + let components: Components = parameters.decode_into()?; + + if !components.is_valid() { + return Err(pkcs8::Error::KeyMalformed); + } + + let x = UIntRef::from_der(value.private_key)?; + let x = BigUint::from_bytes_be(x.as_bytes()); + + let y = if let Some(y_bytes) = value.public_key { + let y = UIntRef::from_der(y_bytes)?; + BigUint::from_bytes_be(y.as_bytes()) + } else { + crate::generate::public_component(&components, &x) + }; + + let public_key = PublicKey::from_components(components, y); + let private_key = PrivateKey::from_components(public_key, x); + + if !private_key.is_valid() { + return Err(pkcs8::Error::KeyMalformed); + } + + Ok(private_key) + } +} + +impl DecodePrivateKey for PrivateKey {} diff --git a/dsa/src/public_key.rs b/dsa/src/public_key.rs new file mode 100644 index 00000000..537bc72a --- /dev/null +++ b/dsa/src/public_key.rs @@ -0,0 +1,145 @@ +//! +//! Module containing the definition of the public key container +//! + +use crate::{sig::Signature, two, Components, DSA_OID}; +use core::cmp::min; +use digest::Digest; +use num_bigint::{BigUint, ModInverse}; +use num_traits::One; +use pkcs8::{ + der::{asn1::UIntRef, AnyRef, Decode, Encode}, + spki, AlgorithmIdentifier, DecodePublicKey, EncodePublicKey, SubjectPublicKeyInfo, +}; +use signature::DigestVerifier; + +/// DSA public key +#[derive(Clone, PartialEq, PartialOrd)] +#[must_use] +pub struct PublicKey { + /// common components + components: Components, + + /// Public component y + y: BigUint, +} + +opaque_debug::implement!(PublicKey); + +impl PublicKey { + /// Construct a new public key from the common components and the public component + /// + /// These values are not getting verified for validity + pub const fn from_components(components: Components, y: BigUint) -> Self { + Self { components, y } + } + + /// DSA common components + pub const fn components(&self) -> &Components { + &self.components + } + + /// DSA public component + #[must_use] + pub const fn y(&self) -> &BigUint { + &self.y + } + + /// Check whether the public key is valid + #[must_use] + pub fn is_valid(&self) -> bool { + let components = self.components(); + if !components.is_valid() { + return false; + } + + *self.y() >= two() && self.y().modpow(components.q(), components.p()) == BigUint::one() + } + + /// Verify some prehashed data + #[must_use] + fn verify_prehashed(&self, hash: &[u8], signature: &Signature) -> Option { + // Refuse to verify with an invalid key + if !self.is_valid() { + return None; + } + + let components = self.components(); + let (p, q, g) = (components.p(), components.q(), components.g()); + let (r, s) = (signature.r(), signature.s()); + let y = self.y(); + + let w = s.mod_inverse(q)?.to_biguint().unwrap(); + + let n = (q.bits() / 8) as usize; + let block_size = hash.len(); // Hash function output size + + let z_len = min(n, block_size); + let z = BigUint::from_bytes_be(&hash[..z_len]); + + let u1 = (&z * &w) % q; + let u2 = (r * &w) % q; + let v = (g.modpow(&u1, p) * y.modpow(&u2, p) % p) % q; + + Some(v == *r) + } +} + +impl DigestVerifier for PublicKey +where + D: Digest, +{ + fn verify_digest(&self, digest: D, signature: &Signature) -> Result<(), signature::Error> { + let hash = digest.finalize(); + + let is_valid = self + .verify_prehashed(&hash, signature) + .ok_or_else(signature::Error::new)?; + + if !is_valid { + return Err(signature::Error::new()); + } + + Ok(()) + } +} + +impl EncodePublicKey for PublicKey { + fn to_public_key_der(&self) -> spki::Result { + let parameters = self.components.to_vec()?; + let parameters = AnyRef::from_der(¶meters)?; + let algorithm = AlgorithmIdentifier { + oid: DSA_OID, + parameters: Some(parameters), + }; + + let y_bytes = self.y.to_bytes_be(); + let y = UIntRef::new(&y_bytes)?; + let public_key = y.to_vec()?; + + let public_key_info = SubjectPublicKeyInfo { + algorithm, + subject_public_key: &public_key, + }; + + public_key_info.try_into() + } +} + +impl<'a> TryFrom> for PublicKey { + type Error = spki::Error; + + fn try_from(value: SubjectPublicKeyInfo<'a>) -> Result { + value.algorithm.assert_algorithm_oid(DSA_OID)?; + + let parameters = value.algorithm.parameters_any()?; + let components = parameters.decode_into()?; + + let y = UIntRef::from_der(value.subject_public_key)?; + let y = BigUint::from_bytes_be(y.as_bytes()); + + Ok(Self::from_components(components, y)) + } +} + +impl DecodePublicKey for PublicKey {} diff --git a/dsa/src/sig.rs b/dsa/src/sig.rs new file mode 100644 index 00000000..0c291de8 --- /dev/null +++ b/dsa/src/sig.rs @@ -0,0 +1,102 @@ +//! +//! Module containing the definition of the Signature container +//! + +use alloc::vec::Vec; +use num_bigint::BigUint; +use pkcs8::der::{self, asn1::UIntRef, Decode, Encode, Reader, Sequence}; + +/// Container of the DSA signature +#[derive(Clone)] +#[must_use] +pub struct Signature { + /// Internally cached DER representation of the signature + der_repr: Vec, + + /// Signature part r + r: BigUint, + + /// Signature part s + s: BigUint, +} + +opaque_debug::implement!(Signature); + +impl Signature { + /// Create a new Signature container from its components + pub fn from_components(r: BigUint, s: BigUint) -> Self { + let mut signature = Self { + der_repr: Vec::with_capacity(0), + r, + s, + }; + signature.der_repr = signature.to_vec().unwrap(); + + signature + } + + /// Signature part r + #[must_use] + pub fn r(&self) -> &BigUint { + &self.r + } + + /// Signature part s + #[must_use] + pub fn s(&self) -> &BigUint { + &self.s + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + &self.der_repr + } +} + +impl<'a> Decode<'a> for Signature { + fn decode>(reader: &mut R) -> der::Result { + reader.sequence(|sequence| { + let r = sequence.decode::>()?; + let s = sequence.decode::>()?; + + let r = BigUint::from_bytes_be(r.as_bytes()); + let s = BigUint::from_bytes_be(s.as_bytes()); + + Ok(Self::from_components(r, s)) + }) + } +} + +impl PartialEq for Signature { + fn eq(&self, other: &Self) -> bool { + self.r().eq(other.r()) && self.s().eq(other.s()) + } +} + +impl PartialOrd for Signature { + fn partial_cmp(&self, other: &Self) -> Option { + (self.r(), self.s()).partial_cmp(&(other.r(), other.s())) + } +} + +impl<'a> Sequence<'a> for Signature { + fn fields(&self, encoder: F) -> der::Result + where + F: FnOnce(&[&dyn der::Encode]) -> der::Result, + { + let r_bytes = self.r.to_bytes_be(); + let s_bytes = self.s.to_bytes_be(); + + let r = UIntRef::new(&r_bytes)?; + let s = UIntRef::new(&s_bytes)?; + + encoder(&[&r, &s]) + } +} + +impl signature::Signature for Signature { + fn from_bytes(bytes: &[u8]) -> Result { + Signature::from_der(bytes).map_err(|_| signature::Error::new()) + } +} diff --git a/dsa/tests/components.rs b/dsa/tests/components.rs new file mode 100644 index 00000000..e3bb04aa --- /dev/null +++ b/dsa/tests/components.rs @@ -0,0 +1,25 @@ +use dsa::Components; +use pkcs8::{ + der::{Decode, Encode}, + Document, +}; + +const OPENSSL_PEM_COMPONENTS: &str = include_str!("pems/params.pem"); + +#[test] +fn decode_encode_openssl_components() { + let (_, document) = + Document::from_pem(OPENSSL_PEM_COMPONENTS).expect("Failed to parse components PEM"); + let raw_components = document.as_bytes(); + + let components = + Components::from_der(raw_components).expect("Failed to parse DER into component structure"); + + assert!(components.is_valid()); + + let reencoded_components = components + .to_vec() + .expect("Failed to encode components to DER"); + + assert_eq!(raw_components, reencoded_components); +} diff --git a/dsa/tests/deterministic.rs b/dsa/tests/deterministic.rs new file mode 100644 index 00000000..1b21d4d4 --- /dev/null +++ b/dsa/tests/deterministic.rs @@ -0,0 +1,344 @@ +use digest::{ + block_buffer::Eager, + consts::U256, + core_api::{BlockSizeUser, BufferKindUser, CoreProxy, FixedOutputCore}, + typenum::{IsLess, Le, NonZero}, + Digest, FixedOutput, HashMarker, OutputSizeUser, +}; +use dsa::{Components, PrivateKey, PublicKey, Signature}; +use num_bigint::BigUint; +use num_traits::Num; +use sha1::Sha1; +use sha2::{Sha224, Sha256, Sha384, Sha512}; +use signature::DigestSigner; + +const MESSAGE: &[u8] = b"sample"; +const MESSAGE_2: &[u8] = b"test"; + +fn dsa_1024_private_key() -> PrivateKey { + let p = BigUint::from_str_radix( + "86F5CA03DCFEB225063FF830A0C769B9DD9D6153AD91D7CE27F787C43278B447\ + E6533B86B18BED6E8A48B784A14C252C5BE0DBF60B86D6385BD2F12FB763ED88\ + 73ABFD3F5BA2E0A8C0A59082EAC056935E529DAF7C610467899C77ADEDFC846C\ + 881870B7B19B2B58F9BE0521A17002E3BDD6B86685EE90B3D9A1B02B782B1779", + 16, + ) + .unwrap(); + let q = BigUint::from_str_radix("996F967F6C8E388D9E28D01E205FBA957A5698B1", 16).unwrap(); + let g = BigUint::from_str_radix( + "07B0F92546150B62514BB771E2A0C0CE387F03BDA6C56B505209FF25FD3C133D\ + 89BBCD97E904E09114D9A7DEFDEADFC9078EA544D2E401AEECC40BB9FBBF78FD\ + 87995A10A1C27CB7789B594BA7EFB5C4326A9FE59A070E136DB77175464ADCA4\ + 17BE5DCE2F40D10A46A3A3943F26AB7FD9C0398FF8C76EE0A56826A8A88F1DBD", + 16, + ) + .unwrap(); + + let x = BigUint::from_str_radix("411602CB19A6CCC34494D79D98EF1E7ED5AF25F7", 16).unwrap(); + let y = BigUint::from_str_radix( + "5DF5E01DED31D0297E274E1691C192FE5868FEF9E19A84776454B100CF16F653\ + 92195A38B90523E2542EE61871C0440CB87C322FC4B4D2EC5E1E7EC766E1BE8D\ + 4CE935437DC11C3C8FD426338933EBFE739CB3465F4D3668C5E473508253B1E6\ + 82F65CBDC4FAE93C2EA212390E54905A86E2223170B44EAA7DA5DD9FFCFB7F3B", + 16, + ) + .unwrap(); + + let components = Components::from_components(p, q, g); + let public_key = PublicKey::from_components(components, y); + + PrivateKey::from_components(public_key, x) +} + +fn dsa_2048_private_key() -> PrivateKey { + let p = BigUint::from_str_radix( + "9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642F0B5C48\ + C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757264E5A1A44F\ + FE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F9716BFE6117C6B5\ + B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091EB51743BF33050C38DE2\ + 35567E1B34C3D6A5C0CEAA1A0F368213C3D19843D0B4B09DCB9FC72D39C8DE41\ + F1BF14D4BB4563CA28371621CAD3324B6A2D392145BEBFAC748805236F5CA2FE\ + 92B871CD8F9C36D3292B5509CA8CAA77A2ADFC7BFD77DDA6F71125A7456FEA15\ + 3E433256A2261C6A06ED3693797E7995FAD5AABBCFBE3EDA2741E375404AE25B", + 16, + ) + .unwrap(); + let q = BigUint::from_str_radix( + "F2C3119374CE76C9356990B465374A17F23F9ED35089BD969F61C6DDE9998C1F", + 16, + ) + .unwrap(); + let g = BigUint::from_str_radix( + "5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E24809670716C613\ + D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D1AA58C4328A06C4\ + 6A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A338661D10461C0D135472\ + 085057F3494309FFA73C611F78B32ADBB5740C361C9F35BE90997DB2014E2EF5\ + AA61782F52ABEB8BD6432C4DD097BC5423B285DAFB60DC364E8161F4A2A35ACA\ + 3A10B1C4D203CC76A470A33AFDCBDD92959859ABD8B56E1725252D78EAC66E71\ + BA9AE3F1DD2487199874393CD4D832186800654760E1E34C09E4D155179F9EC0\ + DC4473F996BDCE6EED1CABED8B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", + 16, + ) + .unwrap(); + + let x = BigUint::from_str_radix( + "69C7548C21D0DFEA6B9A51C9EAD4E27C33D3B3F180316E5BCAB92C933F0E4DBC", + 16, + ) + .unwrap(); + let y = BigUint::from_str_radix( + "667098C654426C78D7F8201EAC6C203EF030D43605032C2F1FA937E5237DBD94\ + 9F34A0A2564FE126DC8B715C5141802CE0979C8246463C40E6B6BDAA2513FA61\ + 1728716C2E4FD53BC95B89E69949D96512E873B9C8F8DFD499CC312882561ADE\ + CB31F658E934C0C197F2C4D96B05CBAD67381E7B768891E4DA3843D24D94CDFB\ + 5126E9B8BF21E8358EE0E0A30EF13FD6A664C0DCE3731F7FB49A4845A4FD8254\ + 687972A2D382599C9BAC4E0ED7998193078913032558134976410B89D2C171D1\ + 23AC35FD977219597AA7D15C1A9A428E59194F75C721EBCBCFAE44696A499AFA\ + 74E04299F132026601638CB87AB79190D4A0986315DA8EEC6561C938996BEADF", + 16, + ) + .unwrap(); + + let components = Components::from_components(p, q, g); + let public_key = PublicKey::from_components(components, y); + + PrivateKey::from_components(public_key, x) +} + +/// Generate a signature given the unhashed message and a private key +fn generate_signature(private_key: PrivateKey, data: &[u8]) -> Signature +where + D: Digest + CoreProxy + FixedOutput, + D::Core: BlockSizeUser + + BufferKindUser + + Clone + + Default + + FixedOutputCore + + HashMarker + + OutputSizeUser, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, +{ + private_key.sign_digest(::new().chain_update(data)) +} + +/// Generate a signature using the 1024-bit DSA key +fn generate_1024_signature(data: &[u8]) -> Signature +where + D: Digest + CoreProxy + FixedOutput, + D::Core: BlockSizeUser + + BufferKindUser + + Clone + + Default + + FixedOutputCore + + HashMarker + + OutputSizeUser, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, +{ + generate_signature::(dsa_1024_private_key(), data) +} + +/// Generate a signature using the 2048-bit DSA key +fn generate_2048_signature(data: &[u8]) -> Signature +where + D: Digest + CoreProxy + FixedOutput, + D::Core: BlockSizeUser + + BufferKindUser + + Clone + + Default + + FixedOutputCore + + HashMarker + + OutputSizeUser, + ::BlockSize: IsLess, + Le<::BlockSize, U256>: NonZero, +{ + generate_signature::(dsa_2048_private_key(), data) +} + +/// Create a signature container from the two components in their textual hexadecimal form +fn from_str_signature(r: &str, s: &str) -> Signature { + Signature::from_components( + BigUint::from_str_radix(r, 16).unwrap(), + BigUint::from_str_radix(s, 16).unwrap(), + ) +} + +/// Return the RFC 6979 test cases +/// +/// # Returns +/// +/// Vector of tuples. +/// First element is the message appended to the panic upon signature mismatch, the second one is the expected signature and the third element is a function +/// that generates an RFC-6979 signature using the `dsa` crate +fn cases() -> Vec<(Signature, Box Signature>)> { + vec![ + // sha1, 1024, "sample" + ( + from_str_signature( + "2E1A0C2562B2912CAAF89186FB0F42001585DA55", + "29EFB6B0AFF2D7A68EB70CA313022253B9A88DF5", + ), + Box::new(|| generate_1024_signature::(MESSAGE)), + ), + // sha1, 1024, "test" + ( + from_str_signature( + "42AB2052FD43E123F0607F115052A67DCD9C5C77", + "183916B0230D45B9931491D4C6B0BD2FB4AAF088", + ), + Box::new(|| generate_1024_signature::(MESSAGE_2)), + ), + // sha224, 1024, "sample" + ( + from_str_signature( + "4BC3B686AEA70145856814A6F1BB53346F02101E", + "410697B92295D994D21EDD2F4ADA85566F6F94C1", + ), + Box::new(|| generate_1024_signature::(MESSAGE)), + ), + // sha224, 1024, "test" + ( + from_str_signature( + "6868E9964E36C1689F6037F91F28D5F2C30610F2", + "49CEC3ACDC83018C5BD2674ECAAD35B8CD22940F", + ), + Box::new(|| generate_1024_signature::(MESSAGE_2)), + ), + // sha256, 1024, "sample" + ( + from_str_signature( + "81F2F5850BE5BC123C43F71A3033E9384611C545", + "4CDD914B65EB6C66A8AAAD27299BEE6B035F5E89", + ), + Box::new(|| generate_1024_signature::(MESSAGE)), + ), + // sha256, 1024, "test" + ( + from_str_signature( + "22518C127299B0F6FDC9872B282B9E70D0790812", + "6837EC18F150D55DE95B5E29BE7AF5D01E4FE160", + ), + Box::new(|| generate_1024_signature::(MESSAGE_2)), + ), + // sha384, 1024, "sample" + ( + from_str_signature( + "07F2108557EE0E3921BC1774F1CA9B410B4CE65A", + "54DF70456C86FAC10FAB47C1949AB83F2C6F7595", + ), + Box::new(|| generate_1024_signature::(MESSAGE)), + ), + // sha384, 1024, "test" + ( + from_str_signature( + "854CF929B58D73C3CBFDC421E8D5430CD6DB5E66", + "91D0E0F53E22F898D158380676A871A157CDA622", + ), + Box::new(|| generate_1024_signature::(MESSAGE_2)), + ), + // sha512, 1024, "sample" + ( + from_str_signature( + "16C3491F9B8C3FBBDD5E7A7B667057F0D8EE8E1B", + "02C36A127A7B89EDBB72E4FFBC71DABC7D4FC69C", + ), + Box::new(|| generate_1024_signature::(MESSAGE)), + ), + // sha512, 1024, "test" + ( + from_str_signature( + "8EA47E475BA8AC6F2D821DA3BD212D11A3DEB9A0", + "7C670C7AD72B6C050C109E1790008097125433E8", + ), + Box::new(|| generate_1024_signature::(MESSAGE_2)), + ), + // sha1, 2048, "sample" + ( + from_str_signature( + "3A1B2DBD7489D6ED7E608FD036C83AF396E290DBD602408E8677DAABD6E7445A", + "D26FCBA19FA3E3058FFC02CA1596CDBB6E0D20CB37B06054F7E36DED0CDBBCCF", + ), + Box::new(|| generate_2048_signature::(MESSAGE)), + ), + // sha1, 2048, "test" + ( + from_str_signature( + "C18270A93CFC6063F57A4DFA86024F700D980E4CF4E2CB65A504397273D98EA0", + "414F22E5F31A8B6D33295C7539C1C1BA3A6160D7D68D50AC0D3A5BEAC2884FAA", + ), + Box::new(|| generate_2048_signature::(MESSAGE_2)), + ), + // sha224, 2048, "sample" + ( + from_str_signature( + "DC9F4DEADA8D8FF588E98FED0AB690FFCE858DC8C79376450EB6B76C24537E2C", + "A65A9C3BC7BABE286B195D5DA68616DA8D47FA0097F36DD19F517327DC848CEC", + ), + Box::new(|| generate_2048_signature::(MESSAGE)), + ), + // sha224, 2048, "test" + ( + from_str_signature( + "272ABA31572F6CC55E30BF616B7A265312018DD325BE031BE0CC82AA17870EA3", + "E9CC286A52CCE201586722D36D1E917EB96A4EBDB47932F9576AC645B3A60806", + ), + Box::new(|| generate_2048_signature::(MESSAGE_2)), + ), + // sha256, 2048, "sample" + ( + from_str_signature( + "EACE8BDBBE353C432A795D9EC556C6D021F7A03F42C36E9BC87E4AC7932CC809", + "7081E175455F9247B812B74583E9E94F9EA79BD640DC962533B0680793A38D53", + ), + Box::new(|| generate_2048_signature::(MESSAGE)), + ), + // sha256, 2048, "test" + ( + from_str_signature( + "8190012A1969F9957D56FCCAAD223186F423398D58EF5B3CEFD5A4146A4476F0", + "7452A53F7075D417B4B013B278D1BB8BBD21863F5E7B1CEE679CF2188E1AB19E", + ), + Box::new(|| generate_2048_signature::(MESSAGE_2)), + ), + // sha384, 2048, "sample" + ( + from_str_signature( + "B2DA945E91858834FD9BF616EBAC151EDBC4B45D27D0DD4A7F6A22739F45C00B", + "19048B63D9FD6BCA1D9BAE3664E1BCB97F7276C306130969F63F38FA8319021B", + ), + Box::new(|| generate_2048_signature::(MESSAGE)), + ), + // sha384, 2048, "test" + ( + from_str_signature( + "239E66DDBE8F8C230A3D071D601B6FFBDFB5901F94D444C6AF56F732BEB954BE", + "6BD737513D5E72FE85D1C750E0F73921FE299B945AAD1C802F15C26A43D34961", + ), + Box::new(|| generate_2048_signature::(MESSAGE_2)), + ), + // sha512, 2048, "sample" + ( + from_str_signature( + "2016ED092DC5FB669B8EFB3D1F31A91EECB199879BE0CF78F02BA062CB4C942E", + "D0C76F84B5F091E141572A639A4FB8C230807EEA7D55C8A154A224400AFF2351", + ), + Box::new(|| generate_2048_signature::(MESSAGE)), + ), + // sha512, 2048, "test" + ( + from_str_signature( + "89EC4BB1400ECCFF8E7D9AA515CD1DE7803F2DAFF09693EE7FD1353E90A68307", + "C9F0BDABCC0D880BB137A994CC7F3980CE91CC10FAF529FC46565B15CEA854E1", + ), + Box::new(|| generate_2048_signature::(MESSAGE_2)), + ), + ] +} + +#[test] +fn rfc6979_signatures() { + for (idx, (expected, gen_fn)) in cases().into_iter().enumerate() { + assert_eq!(expected, gen_fn(), "{}th test case", idx); + } +} diff --git a/dsa/tests/pems/params.pem b/dsa/tests/pems/params.pem new file mode 100644 index 00000000..7f24db1d --- /dev/null +++ b/dsa/tests/pems/params.pem @@ -0,0 +1,14 @@ +-----BEGIN DSA PARAMETERS----- +MIICLAKCAQEAkGWPlAeMc0XCaHO80sng7cLuefKO4FS5N3Bp95ZV7M14z0lOZz5X +69OfpacMM9uWPplYS/yY1XhpybBDbGVE+nU6PlTjrCZxIaSeyMg1paa7iyVQvSJ9 +yJfcl+6ufSMtNB7fwJklodaJsSgst2rKC0QtBNkCIxl0/q3GI3uF4GVxUQ0Y9H8K +O8Fj2Vn3ju4/pTk32dR12RK1vhvfUR9LCpz5WTdnWdKa2lcCmppUBTHG2ZEWUHju +P2XcI295mjE71TR1W+XwpRznLhHF0v+da1tzLP9q8xiGFyYqI0iOGAWPKJPjfTlq +/kqo/AzwiB3dRU2R7XbGvLKUhMPZTiNBmQIhAJzihmrV1ZA2bA9/416eDLdQGrUB +u0qd3ZEvBpwzTjxPAoIBAGOua3Cqupe0wqmjb4PhnMNvoudl3g0TCuFwxX+ClsaN +uTio1VPQ6/EmOaS+69ItkAdjcHWCMmuEoa9kpzEroBOSQbu7lek1CS0X89bt0fOR +TSI54rJR8iBYSvePC9IcnOWTyyp0vrxKb+OGLvUQ2cy0wsRGXt9/DVx1k8+OLlBk +Enf2z4IS2Oqsh34ZNqRktiIkY0NEAhHE5yVdVJbTXV7w5BYeiJqnXV6dEjYFukqh +vFKcXD1n2iC9xP6oBbgphYULIcPfCZVnJTpPiVz+YqDIh6iVC58QYZebNSccMHY2 +DxY8zZUb0kHSyaDtYm3CY2Nn8WFFQoqQqNfb+PB9dLs= +-----END DSA PARAMETERS----- diff --git a/dsa/tests/pems/private.pem b/dsa/tests/pems/private.pem new file mode 100644 index 00000000..02d6fa99 --- /dev/null +++ b/dsa/tests/pems/private.pem @@ -0,0 +1,15 @@ +-----BEGIN PRIVATE KEY----- +MIICZAIBADCCAjkGByqGSM44BAEwggIsAoIBAQCQZY+UB4xzRcJoc7zSyeDtwu55 +8o7gVLk3cGn3llXszXjPSU5nPlfr05+lpwwz25Y+mVhL/JjVeGnJsENsZUT6dTo+ +VOOsJnEhpJ7IyDWlpruLJVC9In3Il9yX7q59Iy00Ht/AmSWh1omxKCy3asoLRC0E +2QIjGXT+rcYje4XgZXFRDRj0fwo7wWPZWfeO7j+lOTfZ1HXZErW+G99RH0sKnPlZ +N2dZ0praVwKamlQFMcbZkRZQeO4/Zdwjb3maMTvVNHVb5fClHOcuEcXS/51rW3Ms +/2rzGIYXJiojSI4YBY8ok+N9OWr+Sqj8DPCIHd1FTZHtdsa8spSEw9lOI0GZAiEA +nOKGatXVkDZsD3/jXp4Mt1AatQG7Sp3dkS8GnDNOPE8CggEAY65rcKq6l7TCqaNv +g+Gcw2+i52XeDRMK4XDFf4KWxo25OKjVU9Dr8SY5pL7r0i2QB2NwdYIya4Shr2Sn +MSugE5JBu7uV6TUJLRfz1u3R85FNIjnislHyIFhK948L0hyc5ZPLKnS+vEpv44Yu +9RDZzLTCxEZe338NXHWTz44uUGQSd/bPghLY6qyHfhk2pGS2IiRjQ0QCEcTnJV1U +ltNdXvDkFh6ImqddXp0SNgW6SqG8UpxcPWfaIL3E/qgFuCmFhQshw98JlWclOk+J +XP5ioMiHqJULnxBhl5s1JxwwdjYPFjzNlRvSQdLJoO1ibcJjY2fxYUVCipCo19v4 +8H10uwQiAiBcmS3IlLPz13zgcj5s6pMApi3F2TshHKiCMp7D866qjA== +-----END PRIVATE KEY----- diff --git a/dsa/tests/pems/public.pem b/dsa/tests/pems/public.pem new file mode 100644 index 00000000..1b01f1f5 --- /dev/null +++ b/dsa/tests/pems/public.pem @@ -0,0 +1,20 @@ +-----BEGIN PUBLIC KEY----- +MIIDRjCCAjkGByqGSM44BAEwggIsAoIBAQCQZY+UB4xzRcJoc7zSyeDtwu558o7g +VLk3cGn3llXszXjPSU5nPlfr05+lpwwz25Y+mVhL/JjVeGnJsENsZUT6dTo+VOOs +JnEhpJ7IyDWlpruLJVC9In3Il9yX7q59Iy00Ht/AmSWh1omxKCy3asoLRC0E2QIj +GXT+rcYje4XgZXFRDRj0fwo7wWPZWfeO7j+lOTfZ1HXZErW+G99RH0sKnPlZN2dZ +0praVwKamlQFMcbZkRZQeO4/Zdwjb3maMTvVNHVb5fClHOcuEcXS/51rW3Ms/2rz +GIYXJiojSI4YBY8ok+N9OWr+Sqj8DPCIHd1FTZHtdsa8spSEw9lOI0GZAiEAnOKG +atXVkDZsD3/jXp4Mt1AatQG7Sp3dkS8GnDNOPE8CggEAY65rcKq6l7TCqaNvg+Gc +w2+i52XeDRMK4XDFf4KWxo25OKjVU9Dr8SY5pL7r0i2QB2NwdYIya4Shr2SnMSug +E5JBu7uV6TUJLRfz1u3R85FNIjnislHyIFhK948L0hyc5ZPLKnS+vEpv44Yu9RDZ +zLTCxEZe338NXHWTz44uUGQSd/bPghLY6qyHfhk2pGS2IiRjQ0QCEcTnJV1UltNd +XvDkFh6ImqddXp0SNgW6SqG8UpxcPWfaIL3E/qgFuCmFhQshw98JlWclOk+JXP5i +oMiHqJULnxBhl5s1JxwwdjYPFjzNlRvSQdLJoO1ibcJjY2fxYUVCipCo19v48H10 +uwOCAQUAAoIBACMQg2b+H3R2sEsTWDaE9w9qOzWiIpmX+ZbErFqkzmNO3C3IFJUU +ZoJoc/SHYV3YF/LOTJEAbvD6IJoRGgPFHzLG+0OcaBdGcSanHF8wFBu169wTeCuO +vcok3AObiwakjUd3auufHUCct6KsLNM2ssG3pwOS+bYz4YfaCO/Tu11XUDQ5Gyr/ +WUJaJh2b7o1KLHGuh/dhU6t6nEofOIgwwqVSfL6Pm9v0c1a43SaQnk3pdSNg0o5i +2IxZvMIO7kN4PyvmVQnz/E2qRY6SzSEM0QY+KtPz/T0rp9DvzDiQ5gC+UYudWPVD +o4kkbTEJ8K++5MtETUjXovJp4VoRALL82CY= +-----END PUBLIC KEY----- diff --git a/dsa/tests/private_key.rs b/dsa/tests/private_key.rs new file mode 100644 index 00000000..837c2440 --- /dev/null +++ b/dsa/tests/private_key.rs @@ -0,0 +1,72 @@ +// We abused the deprecated attribute for unsecure key sizes +// But we want to use those small key sizes for fast tests +#![allow(deprecated)] + +use digest::Digest; +use dsa::{consts::DSA_1024_160, Components, PrivateKey}; +use num_bigint::BigUint; +use num_traits::Zero; +use pkcs8::{DecodePrivateKey, EncodePrivateKey, LineEnding}; +use sha1::Sha1; +use signature::{DigestVerifier, RandomizedDigestSigner}; + +const OPENSSL_PEM_PRIVATE_KEY: &str = include_str!("pems/private.pem"); + +fn generate_keypair() -> PrivateKey { + let mut rng = rand::thread_rng(); + let components = Components::generate(&mut rng, DSA_1024_160); + PrivateKey::generate(&mut rng, components) +} + +#[test] +fn decode_encode_openssl_private_key() { + let private_key = PrivateKey::from_pkcs8_pem(OPENSSL_PEM_PRIVATE_KEY) + .expect("Failed to decode PEM encoded OpenSSL key"); + assert!(private_key.is_valid()); + + let reencoded_private_key = private_key + .to_pkcs8_pem(LineEnding::LF) + .expect("Failed to encode private key into PEM representation"); + + assert_eq!(*reencoded_private_key, OPENSSL_PEM_PRIVATE_KEY); +} + +#[test] +fn encode_decode_private_key() { + let private_key = generate_keypair(); + let encoded_private_key = private_key.to_pkcs8_pem(LineEnding::LF).unwrap(); + let decoded_private_key = PrivateKey::from_pkcs8_pem(&encoded_private_key).unwrap(); + + assert_eq!(private_key, decoded_private_key); +} + +#[test] +fn sign_and_verify() { + const DATA: &[u8] = b"SIGN AND VERIFY THOSE BYTES"; + + let private_key = generate_keypair(); + let public_key = private_key.public_key(); + + let signature = + private_key.sign_digest_with_rng(rand::thread_rng(), Sha1::new().chain_update(DATA)); + + assert!(public_key + .verify_digest(Sha1::new().chain_update(DATA), &signature) + .is_ok()); +} + +#[test] +fn verify_validity() { + let private_key = generate_keypair(); + let components = private_key.public_key().components(); + + assert!( + BigUint::zero() < *private_key.x() && private_key.x() < components.q(), + "Requirement 0 PublicKey { + let mut rng = rand::thread_rng(); + let components = Components::generate(&mut rng, DSA_1024_160); + let private_key = PrivateKey::generate(&mut rng, components); + + private_key.public_key().clone() +} + +#[test] +fn decode_encode_openssl_public_key() { + let public_key = PublicKey::from_public_key_pem(OPENSSL_PEM_PUBLIC_KEY) + .expect("Failed to decode PEM encoded OpenSSL public key"); + assert!(public_key.is_valid()); + + let reencoded_public_key = public_key + .to_public_key_pem(LineEnding::LF) + .expect("Failed to encode public key into PEM representation"); + + assert_eq!(reencoded_public_key, OPENSSL_PEM_PUBLIC_KEY); +} + +#[test] +fn encode_decode_public_key() { + let public_key = generate_public_key(); + let encoded_public_key = public_key.to_public_key_pem(LineEnding::LF).unwrap(); + let decoded_public_key = PublicKey::from_public_key_pem(&encoded_public_key).unwrap(); + + assert_eq!(public_key, decoded_public_key); +} + +#[test] +fn validate_public_key() { + let public_key = generate_public_key(); + let p = public_key.components().p(); + let q = public_key.components().q(); + + // Taken from the parameter validation from bouncy castle + assert_eq!(public_key.y().modpow(q, p), BigUint::one()); +} diff --git a/dsa/tests/signature.rs b/dsa/tests/signature.rs new file mode 100644 index 00000000..8d4634f4 --- /dev/null +++ b/dsa/tests/signature.rs @@ -0,0 +1,89 @@ +#![allow(deprecated)] + +use digest::Digest; +use dsa::{consts::DSA_1024_160, Components, PrivateKey, Signature}; +use pkcs8::der::{Decode, Encode}; +use rand::{CryptoRng, RngCore, SeedableRng}; +use rand_chacha::ChaCha8Rng; +use sha2::Sha256; +use signature::{DigestVerifier, RandomizedDigestSigner}; + +/// Seed used for the ChaCha8 RNG +const SEED: u64 = 0x2103_1949; + +/// Message to be signed/verified +const MESSAGE: &[u8] = b"test"; + +/// Message signed by this crate using the keys generated by this CSPRNG +/// +/// This signature was generated using the keys generated by this CSPRNG (the per-message `k` component was also generated using the CSPRNG) +const MESSAGE_SIGNATURE_CRATE_ASN1: &[u8] = &[ + 0x30, 0x2C, 0x02, 0x14, 0x45, 0x1D, 0xE5, 0x76, 0x21, 0xD8, 0xFD, 0x76, 0xC1, 0x6F, 0x45, 0x4E, + 0xDE, 0x5F, 0x09, 0x79, 0x76, 0x52, 0xF3, 0xA5, 0x02, 0x14, 0x53, 0x60, 0xE6, 0xB7, 0xF0, 0xCF, + 0xAE, 0x49, 0xB1, 0x58, 0x5C, 0xCF, 0x5F, 0x3F, 0x94, 0x49, 0x21, 0xA0, 0xBF, 0xD2, +]; + +/// Message signed by OpenSSL using the keys generated by this CSPRNG +/// +/// This signature was generated using the SHA-256 digest +const MESSAGE_SIGNATURE_OPENSSL_ASN1: &[u8] = &[ + 0x30, 0x2C, 0x02, 0x14, 0x6D, 0xB3, 0x8E, 0xAF, 0x97, 0x13, 0x7E, 0x07, 0xFF, 0x24, 0xB8, 0x66, + 0x97, 0x18, 0xE1, 0x6F, 0xD7, 0x9A, 0x28, 0x2D, 0x02, 0x14, 0x47, 0x8C, 0x0B, 0x96, 0x51, 0x08, + 0x08, 0xC8, 0x34, 0x9D, 0x0D, 0x41, 0xC7, 0x73, 0x0F, 0xB5, 0x9C, 0xBB, 0x00, 0x34, +]; + +/// Get the seeded CSPRNG +fn seeded_csprng() -> impl CryptoRng + RngCore { + ChaCha8Rng::seed_from_u64(SEED) +} + +/// Generate a DSA keypair using a seeded CSPRNG +fn generate_deterministic_keypair() -> PrivateKey { + let mut rng = seeded_csprng(); + let components = Components::generate(&mut rng, DSA_1024_160); + PrivateKey::generate(&mut rng, components) +} + +#[test] +fn decode_encode_signature() { + let signature_openssl = + Signature::from_der(MESSAGE_SIGNATURE_OPENSSL_ASN1).expect("Failed to decode signature"); + let encoded_signature_openssl = signature_openssl + .to_vec() + .expect("Failed to encode signature"); + + assert_eq!(MESSAGE_SIGNATURE_OPENSSL_ASN1, encoded_signature_openssl); + + let signature_crate = + Signature::from_der(MESSAGE_SIGNATURE_CRATE_ASN1).expect("Failed to decode signature"); + let encoded_signature_crate = signature_crate + .to_vec() + .expect("Failed to encode signature"); + + assert_eq!(MESSAGE_SIGNATURE_CRATE_ASN1, encoded_signature_crate); +} + +#[test] +fn sign_message() { + let private_key = generate_deterministic_keypair(); + let generated_signature = + private_key.sign_digest_with_rng(seeded_csprng(), Sha256::new().chain_update(MESSAGE)); + + let expected_signature = + Signature::from_der(MESSAGE_SIGNATURE_CRATE_ASN1).expect("Failed to decode signature"); + + assert_eq!(generated_signature, expected_signature); +} + +#[test] +fn verify_signature() { + let private_key = generate_deterministic_keypair(); + let public_key = private_key.public_key(); + + let signature = Signature::from_der(MESSAGE_SIGNATURE_OPENSSL_ASN1) + .expect("Failed to parse ASN.1 representation of the test signature"); + + assert!(public_key + .verify_digest(Sha256::new().chain_update(MESSAGE), &signature) + .is_ok()); +}