From 341371e5d6f9112cb2dc81070e0ca4524b399f20 Mon Sep 17 00:00:00 2001 From: Will Song Date: Wed, 31 Jan 2024 11:01:47 -0500 Subject: [PATCH 1/5] add lms to RustCrypto/signatures --- .github/workflows/lms.yml | 52 ++++ Cargo.toml | 1 + lms/.github/workflows/lms-rust.yml | 52 ++++ lms/.gitignore | 2 + lms/CHANGELOG.md | 8 + lms/Cargo.toml | 33 +++ lms/LICENSE-APACHE | 201 +++++++++++++ lms/LICENSE-MIT | 25 ++ lms/README.md | 70 +++++ lms/lms/constants.rs | 13 + lms/lms/error.rs | 19 ++ lms/lms/lib.rs | 14 + lms/lms/lms/keypair.rs | 13 + lms/lms/lms/mod.rs | 46 +++ lms/lms/lms/modes.rs | 118 ++++++++ lms/lms/lms/private.rs | 354 +++++++++++++++++++++++ lms/lms/lms/public.rs | 301 ++++++++++++++++++++ lms/lms/lms/signature.rs | 317 +++++++++++++++++++++ lms/lms/ots/keypair.rs | 13 + lms/lms/ots/mod.rs | 203 +++++++++++++ lms/lms/ots/modes.rs | 219 ++++++++++++++ lms/lms/ots/private.rs | 149 ++++++++++ lms/lms/ots/public.rs | 150 ++++++++++ lms/lms/ots/signature.rs | 439 +++++++++++++++++++++++++++++ lms/lms/ots/util.rs | 56 ++++ lms/lms/types.rs | 13 + 26 files changed, 2881 insertions(+) create mode 100644 .github/workflows/lms.yml create mode 100644 lms/.github/workflows/lms-rust.yml create mode 100644 lms/.gitignore create mode 100644 lms/CHANGELOG.md create mode 100644 lms/Cargo.toml create mode 100644 lms/LICENSE-APACHE create mode 100644 lms/LICENSE-MIT create mode 100644 lms/README.md create mode 100644 lms/lms/constants.rs create mode 100644 lms/lms/error.rs create mode 100644 lms/lms/lib.rs create mode 100644 lms/lms/lms/keypair.rs create mode 100644 lms/lms/lms/mod.rs create mode 100644 lms/lms/lms/modes.rs create mode 100644 lms/lms/lms/private.rs create mode 100644 lms/lms/lms/public.rs create mode 100644 lms/lms/lms/signature.rs create mode 100644 lms/lms/ots/keypair.rs create mode 100644 lms/lms/ots/mod.rs create mode 100644 lms/lms/ots/modes.rs create mode 100644 lms/lms/ots/private.rs create mode 100644 lms/lms/ots/public.rs create mode 100644 lms/lms/ots/signature.rs create mode 100644 lms/lms/ots/util.rs create mode 100644 lms/lms/types.rs diff --git a/.github/workflows/lms.yml b/.github/workflows/lms.yml new file mode 100644 index 00000000..4914ade5 --- /dev/null +++ b/.github/workflows/lms.yml @@ -0,0 +1,52 @@ +name: lms-rust +on: + pull_request: + paths: + - ".github/workflows/lms.yml" + - "lms/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: . + +env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Dwarnings" + +jobs: + clippy: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.73.0 # MSRV + - stable + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + components: clippy + - run: cargo clippy --no-default-features + - run: cargo clippy + - run: cargo clippy --all-features + + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.73.0 # MSRV + - stable + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - run: cargo check --all-features + - run: cargo test --no-default-features + - run: cargo test + - run: cargo test --all-features diff --git a/Cargo.toml b/Cargo.toml index f881d13d..326f23fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "ecdsa", "ed448", "ed25519", + "lms", "rfc6979" ] diff --git a/lms/.github/workflows/lms-rust.yml b/lms/.github/workflows/lms-rust.yml new file mode 100644 index 00000000..9297ac02 --- /dev/null +++ b/lms/.github/workflows/lms-rust.yml @@ -0,0 +1,52 @@ +name: lms-rust +on: + pull_request: + paths: + - ".github/workflows/lms-rust.yml" + - "lms/**" + - "Cargo.*" + push: + branches: main + +defaults: + run: + working-directory: . + +env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Dwarnings" + +jobs: + clippy: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.73.0 # MSRV for RustCrypto + - stable + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + components: clippy + - run: cargo clippy --no-default-features + - run: cargo clippy + - run: cargo clippy --all-features + + test: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.73.0 # MSRV for RustCrypto + - stable + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - run: cargo check --all-features + - run: cargo test --no-default-features + - run: cargo test + - run: cargo test --all-features diff --git a/lms/.gitignore b/lms/.gitignore new file mode 100644 index 00000000..1e7caa9e --- /dev/null +++ b/lms/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target/ diff --git a/lms/CHANGELOG.md b/lms/CHANGELOG.md new file mode 100644 index 00000000..41d27065 --- /dev/null +++ b/lms/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## 0.1.0 (2024-01-30) +- Initial release \ No newline at end of file diff --git a/lms/Cargo.toml b/lms/Cargo.toml new file mode 100644 index 00000000..d817869f --- /dev/null +++ b/lms/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "lms-signature" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +name = "lms" +path = "lms/lib.rs" +edition = "2021" + +[dependencies] +anyhow = "1.0.75" +digest = "0.10.7" +generic-array = {version = "0.14.4", features = ["zeroize"]} +rand = "0.8.5" +sha2 = "0.10.8" +static_assertions = "1.1.0" +rand_core = "0.6.4" +zeroize = "1.7.0" + +[dependencies.typenum] +version = "1.17.0" +features = ["const-generics"] + +[dependencies.signature] +version = "2.3.0-pre.0" +features = ["digest", "std", "rand_core"] + +[dev-dependencies] +hex = "0.4.3" +hex-literal = "0.4.1" diff --git a/lms/LICENSE-APACHE b/lms/LICENSE-APACHE new file mode 100644 index 00000000..a99806cf --- /dev/null +++ b/lms/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 2024 Trail of Bits + +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. \ No newline at end of file diff --git a/lms/LICENSE-MIT b/lms/LICENSE-MIT new file mode 100644 index 00000000..06d55201 --- /dev/null +++ b/lms/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2024 Trail of Bits + +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. \ No newline at end of file diff --git a/lms/README.md b/lms/README.md new file mode 100644 index 00000000..721ecfb1 --- /dev/null +++ b/lms/README.md @@ -0,0 +1,70 @@ +# Leighton-Micali Hash-Based Signatures + +This repository contains implementations of [Leighton-Micali Hash-Based +Signatures (RFC 8554)](https://datatracker.ietf.org/doc/html/rfc8554). + +## Security Notice + +LMS signatures are stateful: Users must take care to never sign more than one +message with the same internal LM-OTS private key. To avoid catastrophe, state +must be maintained across multiple invocations of the signing algorithm. + +When using our LMS implementations, the internal counter (`q`) will be +incremented before each signature is returned. + +If the LMS private key is persisted to storage, you **MUST** update the +persistent storage after each signature is generated and before it is released +to the rest of the application. Failure to adhere to this requirement is a +security vulnerability in your application. + +For a stateless hash-based signature algorithm, see +[SPHINCS+](https://sphincs.org). + +NOTE: this project has not been externally audited, but the entire codebase +was internally reviewed by cryptographers at Trail of Bits. + +## Installation + +```terminal +cargo install +``` + +## Usage + +Our implementation uses strongly typed private and public key types. + +```rust +let mut rng = thread_rng(); +let mut seckey = lms::lms::PrivateKey::new::>(&mut rng); +let pubkey = seckey.public(); // of type lms::lms::PublicKey +let sig = seckey.try_sign_with_rng(&mut rng, "example".as_bytes()).unwrap(); +let sig_valid = pubkey.verify("example".as_bytes(), &sig).is_ok(); +``` + +We can generate LMOTS signatures in the same way using `lms::ots::PrivateKey` +instead. + +### Key Management + +We do not require much from the user in terms of key management. Any internal +state changing operation uses mutable reference to update the internal state. +When persisting private keys to long term storage, users must be very careful +that **the same private key is never read from disk twice**. This would create +two private keys in the same state and thus when they are both used to sign a +message, the LMOTS private keys will have been reused, which is considered **not +good**. + +## 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. \ No newline at end of file diff --git a/lms/lms/constants.rs b/lms/lms/constants.rs new file mode 100644 index 00000000..6dc8d051 --- /dev/null +++ b/lms/lms/constants.rs @@ -0,0 +1,13 @@ +//! Constants as defined in RFC 8554 + +/// The length of the identifier `I` +pub const ID_LEN: usize = 16; + +/// `D_PBLC` +pub const D_PBLC: [u8; 2] = [0x80, 0x80]; +/// `D_MESG` +pub const D_MESG: [u8; 2] = [0x81, 0x81]; +/// `D_LEAF` +pub const D_LEAF: [u8; 2] = [0x82, 0x82]; +/// `D_INTR` +pub const D_INTR: [u8; 2] = [0x83, 0x83]; diff --git a/lms/lms/error.rs b/lms/lms/error.rs new file mode 100644 index 00000000..08e1cb93 --- /dev/null +++ b/lms/lms/error.rs @@ -0,0 +1,19 @@ +//! Error types + +// TODO: review errors and make sure they are appropriate +// I expect it does not make sense to use a single error type for both +// LMS and OTS parsing, as we are currently doing. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +/// The error returned by `TryFrom<&[u8]>` impls +pub enum LmsDeserializeError { + /// Length of the slice was `< 4` and no algorithm can be parsed + NoAlgorithm, + /// The parsed algorithm does not match the requested deserialization + WrongAlgorithm, + /// The slice did not contain enough data + TooShort, + /// The slice contained too much data + TooLong, + /// The parsed `q` value was too large + InvalidQ, +} diff --git a/lms/lms/lib.rs b/lms/lms/lib.rs new file mode 100644 index 00000000..56a6f2fe --- /dev/null +++ b/lms/lms/lib.rs @@ -0,0 +1,14 @@ +//! LMS in Rust +//! +//! This is a strongly typed implementation of Leighton-Micali signatures. You +//! can find the private key, public key, and signature struct documentations in +//! their respective crates. See [lms] for anything LMS related and [ots] for +//! anything LM-OTS related. + +pub mod error; +pub mod lms; +pub mod ots; + +// TODO: do we need to expose these? +pub(crate) mod constants; +pub(crate) mod types; diff --git a/lms/lms/lms/keypair.rs b/lms/lms/lms/keypair.rs new file mode 100644 index 00000000..505e7a5f --- /dev/null +++ b/lms/lms/lms/keypair.rs @@ -0,0 +1,13 @@ +use crate::lms::modes::LmsMode; +use crate::lms::private::PrivateKey; +use crate::lms::public::PublicKey; +use signature::Keypair; + +// implements the Keypair trait for PrivateKey +impl Keypair for PrivateKey { + type VerifyingKey = PublicKey; + + fn verifying_key(&self) -> Self::VerifyingKey { + self.public() + } +} diff --git a/lms/lms/lms/mod.rs b/lms/lms/lms/mod.rs new file mode 100644 index 00000000..dc3832f0 --- /dev/null +++ b/lms/lms/lms/mod.rs @@ -0,0 +1,46 @@ +//! Everything related to LMS (and not LM-OTS) + +mod keypair; +pub(crate) mod modes; +mod private; +mod public; +pub mod signature; + +pub use modes::{ + LmsMode, LmsSha256M32H10, LmsSha256M32H15, LmsSha256M32H20, LmsSha256M32H25, LmsSha256M32H5, +}; +pub use private::PrivateKey; +pub use public::PublicKey; +pub use signature::Signature; + +#[cfg(test)] +mod tests { + use ::signature::{RandomizedSignerMut, Verifier}; + + use super::*; + + use crate::{lms::PrivateKey, ots::LmsOtsSha256N32W4}; + + fn test_sign_and_verify() { + let mut rng = rand::thread_rng(); + + // Generate a fresh keypair + let mut sk = PrivateKey::::new(&mut rng); + let pk = sk.public(); + + let msg = "this is a test message".as_bytes(); + + // Sign the message + let sig = sk.try_sign_with_rng(&mut rng, msg); + let sig = sig.unwrap(); + + // Verify the signature + assert!(pk.verify(msg, &sig).is_ok()); + } + + // TODO: macro-generate these exhaustively + #[test] + fn test_sign_and_verify_lms_sha256_m32_h5_lmsots_sha256_n32_w4() { + test_sign_and_verify::>(); + } +} diff --git a/lms/lms/lms/modes.rs b/lms/lms/lms/modes.rs new file mode 100644 index 00000000..d4dcc128 --- /dev/null +++ b/lms/lms/lms/modes.rs @@ -0,0 +1,118 @@ +//! LMS modes +use crate::ots::modes::LmsOtsMode; +use crate::types::Typecode; +use digest::Digest; +use digest::Output; +use generic_array::ArrayLength; +use std::ops::Add; +use std::{ + marker::PhantomData, + ops::{Shl, Sub}, +}; +use typenum::{bit::B1, Add1, Shleft, Sub1, U1, U10, U15, U20, U25, U5}; + +/// The basic trait that must be implemented for any valid LMS mode +pub trait LmsMode: Typecode + Clone { + /// The underlying hash function + type Hasher: Digest; + /// The underlying LM-OTS mode + type OtsMode: LmsOtsMode; + /// Length of the internal Merkle tree, computed as `2^(h+1)-1` + type TreeLen: ArrayLength>; + /// `h` as a type + type HLen: ArrayLength>; + /// The length of the hash function output as a type + const M: usize; + /// `h` as a [usize] + const H: usize; + /// The number of leaves as a [u32], computed as `2^h` + const LEAVES: u32; // precomputed + /// `TreeLen` as a [u32], `2^(h+1)-1` + const TREE_NODES: u32; // precomputed +} + +#[derive(Debug)] +pub struct LmsModeInternal< + OtsMode: LmsOtsMode, + Hasher: Digest, + HLen: ArrayLength>, + const M: usize, + const H: usize, + const TC: u32, +> { + _ots_mode: PhantomData, + _hasher: PhantomData, + _h_len: PhantomData, +} + +impl< + OtsMode: LmsOtsMode, + Hasher: Digest, + TreeLen: ArrayLength>, + const M: usize, + const H: usize, + const TC: u32, + > Clone for LmsModeInternal +{ + fn clone(&self) -> Self { + *self + } +} + +impl< + OtsMode: LmsOtsMode, + Hasher: Digest, + TreeLen: ArrayLength>, + const M: usize, + const H: usize, + const TC: u32, + > Copy for LmsModeInternal +{ +} + +impl< + OtsMode: LmsOtsMode, + Hasher: Digest, + HLen: ArrayLength>, + const M: usize, + const H: usize, + const TC: u32, + > LmsMode for LmsModeInternal +where + HLen: Add, + U1: Shl<>::Output>, + Shleft>::Output>: Sub, + Sub1>::Output>>: ArrayLength>, +{ + type OtsMode = OtsMode; + type Hasher = Hasher; + type TreeLen = Sub1>>; + type HLen = HLen; + const M: usize = M; + const H: usize = H; + const LEAVES: u32 = 1 << H; // precomputed as 2 to the H power + const TREE_NODES: u32 = (1 << (H + 1)) - 1; // 2^(H+1)-1 +} + +impl< + Hasher: Digest, + OtsMode: LmsOtsMode, + TreeLen: ArrayLength>, + const M: usize, + const H: usize, + const TC: u32, + > Typecode for LmsModeInternal +{ + const TYPECODE: u32 = TC; +} + +/// LMS_SHA256_M32_H5 +pub type LmsSha256M32H5 = LmsModeInternal; +/// LMS_SHA256_M32_H10 +pub type LmsSha256M32H10 = LmsModeInternal; +/// LMS_SHA256_M32_H15 +pub type LmsSha256M32H15 = LmsModeInternal; +/// LMS_SHA256_M32_H20 +pub type LmsSha256M32H20 = LmsModeInternal; +/// LMS_SHA256_M32_H25 +pub type LmsSha256M32H25 = LmsModeInternal; diff --git a/lms/lms/lms/private.rs b/lms/lms/lms/private.rs new file mode 100644 index 00000000..9756f29b --- /dev/null +++ b/lms/lms/lms/private.rs @@ -0,0 +1,354 @@ +use crate::constants::{D_INTR, D_LEAF, ID_LEN}; +use crate::error::LmsDeserializeError; +use crate::lms::{LmsMode, PublicKey, Signature}; +use crate::ots::PrivateKey as OtsPrivateKey; +use crate::types::{Identifier, Typecode}; + +use anyhow::anyhow; +use digest::{Digest, Output, OutputSizeUser}; +use generic_array::{ArrayLength, GenericArray}; +use rand::{CryptoRng, Rng}; +use signature::{Error, RandomizedSignerMut}; + +use std::cmp::Ordering; +use std::ops::Add; +use typenum::{Sum, U28}; + +/// Opaque struct representing a LMS public key +/// +/// Note: there is no requirement to map specific LMS algorithms to specific +/// LM-OTS algorithms so it must be parametrized. With the algorithms provided +/// by this crate, this is done via +/// [LmsSha256M32H10](crate::lms::LmsSha256M32H10)<[LmsOtsSha256N32W4](crate::ots::LmsOtsSha256N32W4)>. +pub struct PrivateKey { + id: Identifier, + seed: Output, // Re-generate the leaf privkeys as-needed from a seed + auth_tree: GenericArray, Mode::TreeLen>, // TODO: Decide whether/when to precompute + q: u32, +} + +impl PrivateKey { + /// Creates a new private key with a random identifier using + /// algorithm 5 from + pub fn new(mut rng: impl Rng + CryptoRng) -> Self { + let mut id = Identifier::default(); + rng.fill_bytes(id.as_mut()); + + let mut seed = Output::::default(); + rng.fill_bytes(seed.as_mut()); + Self::new_from_seed(id, seed) + } + + // Returns a new LMS private key generated pseudorandomly from an identifier + // and secret seed. The seed must be equal to the hash output length of the + // LMS mode ([Mode::M]) + // + // TODO: Return error rather than panic? Or just make the input a + // GenericArray? This is the algorithm from Appendix A of + // + pub fn new_from_seed(id: Identifier, seed: impl AsRef<[u8]>) -> Self { + //let seed = seed.as_ref(); + let seed = GenericArray::clone_from_slice(seed.as_ref()); + let mut sk = Self { + id, + seed, + auth_tree: GenericArray::default(), + q: 0, // we set q = 0 when generating keys; it will change + }; + sk.gen_pk_tree(); // TODO: Use lazy generation / MTT + sk + } + + /// Generates a Merkle tree of OTS public key hashes, using the indexing scheme of RFC 8554 offset by 1 + /// auth_tree[i] == T[i+1] + /// (i.e. the root is at index 0, the internal nodes are at indices 1..MODE_LEAVES-1, and the leaves are at indices MODE_LEAVES-1..2*MODE_LEAVES-1) + fn gen_pk_tree(&mut self) { + for i in 0..Mode::LEAVES { + let mut r: u32 = i + Mode::LEAVES; + let ots_priv = OtsPrivateKey::::new_from_seed(i, self.id, &self.seed); + Mode::Hasher::new() + .chain_update(self.id) + .chain_update(r.to_be_bytes()) + .chain_update(D_LEAF) + .chain_update(ots_priv.public().k) + .finalize_into(&mut self.auth_tree[(r - 1) as usize]); + let mut j: u32 = i; + while (j % 2) == 1 { + r = (r - 1) >> 1; + j = (j - 1) >> 1; + Mode::Hasher::new() + .chain_update(self.id) + .chain_update(r.to_be_bytes()) + .chain_update(D_INTR) + .chain_update(self.auth_tree[(2 * r - 1) as usize].clone()) + .chain_update(self.auth_tree[(2 * r) as usize].clone()) + .finalize_into(&mut self.auth_tree[(r - 1) as usize]); + } + } + } + + /// this implements algorithm 1 from + pub fn public(&self) -> PublicKey { + PublicKey::::new(self.id, self.auth_tree[0].clone()) + } + + /// Returns the 16-byte identifier of the key pair + pub fn id(&self) -> &Identifier { + &self.id + } + + /// Returns the current value of the signing index q + pub fn q(&self) -> u32 { + self.q + } +} + +// this implements the algorithm from Appendix D in +impl RandomizedSignerMut> for PrivateKey { + fn try_sign_with_rng( + &mut self, + rng: &mut impl rand_core::CryptoRngCore, + msg: &[u8], + ) -> Result, Error> { + if self.q >= Mode::LEAVES { + return Err(Error::from_source(anyhow!( + "private key has been exhausted" + ))); + } + + let mut ots_priv_key = + OtsPrivateKey::::new_from_seed(self.q, self.id, &self.seed); + let ots_sig = ots_priv_key.try_sign_with_rng(rng, msg)?; + + let r = (1 << Mode::H) + self.q; + + let auth_path = (0..Mode::H).map(|i| self.auth_tree[(((r >> i) ^ 1) - 1) as usize].clone()); + + // increment q + self.q += 1; + + Ok(Signature:: { + q: self.q - 1, + lmots_sig: ots_sig, + path: auth_path.collect(), + }) + } +} + +/// Converts a [PrivateKey] into its byte representation +impl From> + for GenericArray::OutputSize, U28>> +where + ::OutputSize: Add, + Sum<::OutputSize, U28>: ArrayLength, +{ + fn from(pk: PrivateKey) -> Self { + // Return u32(type) || u32(otstype) || u32(q) || id || seed + GenericArray::from_exact_iter( + std::iter::empty() + .chain(Mode::TYPECODE.to_be_bytes()) + .chain(Mode::OtsMode::TYPECODE.to_be_bytes()) + .chain(pk.q.to_be_bytes()) + .chain(pk.id) + .chain(pk.seed), + ) + .unwrap() + } +} + +/// Tries to parse a [PrivateKey] from an exact slice +impl<'a, Mode: LmsMode> TryFrom<&'a [u8]> for PrivateKey { + type Error = LmsDeserializeError; + + fn try_from(pk: &'a [u8]) -> Result { + if pk.len() < 4 { + return Err(LmsDeserializeError::NoAlgorithm); + } + + let (alg, pk) = pk.split_at(4); + let expected = Mode::M + ID_LEN + 8; + + // will never panic because alg is a 4 byte slice + if u32::from_be_bytes(alg.try_into().unwrap()) != Mode::TYPECODE { + return Err(LmsDeserializeError::WrongAlgorithm); + } + + match pk.len().cmp(&expected) { + Ordering::Less => Err(LmsDeserializeError::TooShort), + Ordering::Greater => Err(LmsDeserializeError::TooLong), + Ordering::Equal => { + // pk is now guaranteed to be of the form otstype || q || id || seed + let (otstype, qk) = pk.split_at(ID_LEN); + let (q, idseed) = qk.split_at(4); + let (id, seed) = idseed.split_at(ID_LEN); + + // check the OTS type + if u32::from_be_bytes(otstype.try_into().unwrap()) != Mode::OtsMode::TYPECODE { + return Err(LmsDeserializeError::WrongAlgorithm); + } + + let mut key = Self { + q: u32::from_be_bytes(q.try_into().expect("ok")), + id: id.try_into().expect("ok"), + seed: GenericArray::clone_from_slice(seed), + auth_tree: GenericArray::default(), + }; + key.gen_pk_tree(); + Ok(key) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::PrivateKey; + use crate::lms::modes::{LmsSha256M32H10, LmsSha256M32H5}; + use crate::ots::modes::{LmsOtsSha256N32W4, LmsOtsSha256N32W8}; + use hex_literal::hex; + use signature::{RandomizedSignerMut, SignatureEncoding}; + + #[test] + fn test_pk_tree_kat1() { + let seed = hex!("558b8966c48ae9cb898b423c83443aae014a72f1b1ab5cc85cf1d892903b5439"); + let id = hex!("d08fabd4a2091ff0a8cb4ed834e74534"); + let expected_k = hex!("32a58885cd9ba0431235466bff9651c6c92124404d45fa53cf161c28f1ad5a8e"); + + let lms_priv = PrivateKey::>::new_from_seed(id, seed); + let lms_pub = lms_priv.public(); + assert_eq!(lms_pub.k(), expected_k); + assert_eq!(lms_pub.id(), &id); + } + + #[test] + fn test_pk_tree_kat2() { + let seed = hex!("a1c4696e2608035a886100d05cd99945eb3370731884a8235e2fb3d4d71f2547"); + let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); + let expected_k = hex!("a1cd035833e0e90059603f26e07ad2aad152338e7a5e5984bcd5f7bb4eba40b7"); + + let lms_priv = PrivateKey::>::new_from_seed(id, seed); + let lms_pub = lms_priv.public(); + assert_eq!(lms_pub.k(), expected_k); + assert_eq!(lms_pub.id(), &id); + } + + #[test] + fn test_kat_2() { + let expected_signature = [ + 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x0e, 0xb1, 0xed, 0x54, 0xa2, 0x46, + 0x0d, 0x51, 0x23, 0x88, 0xca, 0xd5, 0x33, 0x13, 0x8d, 0x24, 0x05, 0x34, 0xe9, 0x7b, + 0x1e, 0x82, 0xd3, 0x3b, 0xd9, 0x27, 0xd2, 0x01, 0xdf, 0xc2, 0x4e, 0xbb, 0x11, 0xb3, + 0x64, 0x90, 0x23, 0x69, 0x6f, 0x85, 0x15, 0x0b, 0x18, 0x9e, 0x50, 0xc0, 0x0e, 0x98, + 0x85, 0x0a, 0xc3, 0x43, 0xa7, 0x7b, 0x36, 0x38, 0x31, 0x9c, 0x34, 0x7d, 0x73, 0x10, + 0x26, 0x9d, 0x3b, 0x77, 0x14, 0xfa, 0x40, 0x6b, 0x8c, 0x35, 0xb0, 0x21, 0xd5, 0x4d, + 0x4f, 0xda, 0xda, 0x7b, 0x9c, 0xe5, 0xd4, 0xba, 0x5b, 0x06, 0x71, 0x9e, 0x72, 0xaa, + 0xf5, 0x8c, 0x5a, 0xae, 0x7a, 0xca, 0x05, 0x7a, 0xa0, 0xe2, 0xe7, 0x4e, 0x7d, 0xcf, + 0xd1, 0x7a, 0x08, 0x23, 0x42, 0x9d, 0xb6, 0x29, 0x65, 0xb7, 0xd5, 0x63, 0xc5, 0x7b, + 0x4c, 0xec, 0x94, 0x2c, 0xc8, 0x65, 0xe2, 0x9c, 0x1d, 0xad, 0x83, 0xca, 0xc8, 0xb4, + 0xd6, 0x1a, 0xac, 0xc4, 0x57, 0xf3, 0x36, 0xe6, 0xa1, 0x0b, 0x66, 0x32, 0x3f, 0x58, + 0x87, 0xbf, 0x35, 0x23, 0xdf, 0xca, 0xde, 0xe1, 0x58, 0x50, 0x3b, 0xfa, 0xa8, 0x9d, + 0xc6, 0xbf, 0x59, 0xda, 0xa8, 0x2a, 0xfd, 0x2b, 0x5e, 0xbb, 0x2a, 0x9c, 0xa6, 0x57, + 0x2a, 0x60, 0x67, 0xce, 0xe7, 0xc3, 0x27, 0xe9, 0x03, 0x9b, 0x3b, 0x6e, 0xa6, 0xa1, + 0xed, 0xc7, 0xfd, 0xc3, 0xdf, 0x92, 0x7a, 0xad, 0xe1, 0x0c, 0x1c, 0x9f, 0x2d, 0x5f, + 0xf4, 0x46, 0x45, 0x0d, 0x2a, 0x39, 0x98, 0xd0, 0xf9, 0xf6, 0x20, 0x2b, 0x5e, 0x07, + 0xc3, 0xf9, 0x7d, 0x24, 0x58, 0xc6, 0x9d, 0x3c, 0x81, 0x90, 0x64, 0x39, 0x78, 0xd7, + 0xa7, 0xf4, 0xd6, 0x4e, 0x97, 0xe3, 0xf1, 0xc4, 0xa0, 0x8a, 0x7c, 0x5b, 0xc0, 0x3f, + 0xd5, 0x56, 0x82, 0xc0, 0x17, 0xe2, 0x90, 0x7e, 0xab, 0x07, 0xe5, 0xbb, 0x2f, 0x19, + 0x01, 0x43, 0x47, 0x5a, 0x60, 0x43, 0xd5, 0xe6, 0xd5, 0x26, 0x34, 0x71, 0xf4, 0xee, + 0xcf, 0x6e, 0x25, 0x75, 0xfb, 0xc6, 0xff, 0x37, 0xed, 0xfa, 0x24, 0x9d, 0x6c, 0xda, + 0x1a, 0x09, 0xf7, 0x97, 0xfd, 0x5a, 0x3c, 0xd5, 0x3a, 0x06, 0x67, 0x00, 0xf4, 0x58, + 0x63, 0xf0, 0x4b, 0x6c, 0x8a, 0x58, 0xcf, 0xd3, 0x41, 0x24, 0x1e, 0x00, 0x2d, 0x0d, + 0x2c, 0x02, 0x17, 0x47, 0x2b, 0xf1, 0x8b, 0x63, 0x6a, 0xe5, 0x47, 0xc1, 0x77, 0x13, + 0x68, 0xd9, 0xf3, 0x17, 0x83, 0x5c, 0x9b, 0x0e, 0xf4, 0x30, 0xb3, 0xdf, 0x40, 0x34, + 0xf6, 0xaf, 0x00, 0xd0, 0xda, 0x44, 0xf4, 0xaf, 0x78, 0x00, 0xbc, 0x7a, 0x5c, 0xf8, + 0xa5, 0xab, 0xdb, 0x12, 0xdc, 0x71, 0x8b, 0x55, 0x9b, 0x74, 0xca, 0xb9, 0x09, 0x0e, + 0x33, 0xcc, 0x58, 0xa9, 0x55, 0x30, 0x09, 0x81, 0xc4, 0x20, 0xc4, 0xda, 0x8f, 0xfd, + 0x67, 0xdf, 0x54, 0x08, 0x90, 0xa0, 0x62, 0xfe, 0x40, 0xdb, 0xa8, 0xb2, 0xc1, 0xc5, + 0x48, 0xce, 0xd2, 0x24, 0x73, 0x21, 0x9c, 0x53, 0x49, 0x11, 0xd4, 0x8c, 0xca, 0xab, + 0xfb, 0x71, 0xbc, 0x71, 0x86, 0x2f, 0x4a, 0x24, 0xeb, 0xd3, 0x76, 0xd2, 0x88, 0xfd, + 0x4e, 0x6f, 0xb0, 0x6e, 0xd8, 0x70, 0x57, 0x87, 0xc5, 0xfe, 0xdc, 0x81, 0x3c, 0xd2, + 0x69, 0x7e, 0x5b, 0x1a, 0xac, 0x1c, 0xed, 0x45, 0x76, 0x7b, 0x14, 0xce, 0x88, 0x40, + 0x9e, 0xae, 0xbb, 0x60, 0x1a, 0x93, 0x55, 0x9a, 0xae, 0x89, 0x3e, 0x14, 0x3d, 0x1c, + 0x39, 0x5b, 0xc3, 0x26, 0xda, 0x82, 0x1d, 0x79, 0xa9, 0xed, 0x41, 0xdc, 0xfb, 0xe5, + 0x49, 0x14, 0x7f, 0x71, 0xc0, 0x92, 0xf4, 0xf3, 0xac, 0x52, 0x2b, 0x5c, 0xc5, 0x72, + 0x90, 0x70, 0x66, 0x50, 0x48, 0x7b, 0xae, 0x9b, 0xb5, 0x67, 0x1e, 0xcc, 0x9c, 0xcc, + 0x2c, 0xe5, 0x1e, 0xad, 0x87, 0xac, 0x01, 0x98, 0x52, 0x68, 0x52, 0x12, 0x22, 0xfb, + 0x90, 0x57, 0xdf, 0x7e, 0xd4, 0x18, 0x10, 0xb5, 0xef, 0x0d, 0x4f, 0x7c, 0xc6, 0x73, + 0x68, 0xc9, 0x0f, 0x57, 0x3b, 0x1a, 0xc2, 0xce, 0x95, 0x6c, 0x36, 0x5e, 0xd3, 0x8e, + 0x89, 0x3c, 0xe7, 0xb2, 0xfa, 0xe1, 0x5d, 0x36, 0x85, 0xa3, 0xdf, 0x2f, 0xa3, 0xd4, + 0xcc, 0x09, 0x8f, 0xa5, 0x7d, 0xd6, 0x0d, 0x2c, 0x97, 0x54, 0xa8, 0xad, 0xe9, 0x80, + 0xad, 0x0f, 0x93, 0xf6, 0x78, 0x70, 0x75, 0xc3, 0xf6, 0x80, 0xa2, 0xba, 0x19, 0x36, + 0xa8, 0xc6, 0x1d, 0x1a, 0xf5, 0x2a, 0xb7, 0xe2, 0x1f, 0x41, 0x6b, 0xe0, 0x9d, 0x2a, + 0x8d, 0x64, 0xc3, 0xd3, 0xd8, 0x58, 0x29, 0x68, 0xc2, 0x83, 0x99, 0x02, 0x22, 0x9f, + 0x85, 0xae, 0xe2, 0x97, 0xe7, 0x17, 0xc0, 0x94, 0xc8, 0xdf, 0x4a, 0x23, 0xbb, 0x5d, + 0xb6, 0x58, 0xdd, 0x37, 0x7b, 0xf0, 0xf4, 0xff, 0x3f, 0xfd, 0x8f, 0xba, 0x5e, 0x38, + 0x3a, 0x48, 0x57, 0x48, 0x02, 0xed, 0x54, 0x5b, 0xbe, 0x7a, 0x6b, 0x47, 0x53, 0x53, + 0x33, 0x53, 0xd7, 0x37, 0x06, 0x06, 0x76, 0x40, 0x13, 0x5a, 0x7c, 0xe5, 0x17, 0x27, + 0x9c, 0xd6, 0x83, 0x03, 0x97, 0x47, 0xd2, 0x18, 0x64, 0x7c, 0x86, 0xe0, 0x97, 0xb0, + 0xda, 0xa2, 0x87, 0x2d, 0x54, 0xb8, 0xf3, 0xe5, 0x08, 0x59, 0x87, 0x62, 0x95, 0x47, + 0xb8, 0x30, 0xd8, 0x11, 0x81, 0x61, 0xb6, 0x50, 0x79, 0xfe, 0x7b, 0xc5, 0x9a, 0x99, + 0xe9, 0xc3, 0xc7, 0x38, 0x0e, 0x3e, 0x70, 0xb7, 0x13, 0x8f, 0xe5, 0xd9, 0xbe, 0x25, + 0x51, 0x50, 0x2b, 0x69, 0x8d, 0x09, 0xae, 0x19, 0x39, 0x72, 0xf2, 0x7d, 0x40, 0xf3, + 0x8d, 0xea, 0x26, 0x4a, 0x01, 0x26, 0xe6, 0x37, 0xd7, 0x4a, 0xe4, 0xc9, 0x2a, 0x62, + 0x49, 0xfa, 0x10, 0x34, 0x36, 0xd3, 0xeb, 0x0d, 0x40, 0x29, 0xac, 0x71, 0x2b, 0xfc, + 0x7a, 0x5e, 0xac, 0xbd, 0xd7, 0x51, 0x8d, 0x6d, 0x4f, 0xe9, 0x03, 0xa5, 0xae, 0x65, + 0x52, 0x7c, 0xd6, 0x5b, 0xb0, 0xd4, 0xe9, 0x92, 0x5c, 0xa2, 0x4f, 0xd7, 0x21, 0x4d, + 0xc6, 0x17, 0xc1, 0x50, 0x54, 0x4e, 0x42, 0x3f, 0x45, 0x0c, 0x99, 0xce, 0x51, 0xac, + 0x80, 0x05, 0xd3, 0x3a, 0xcd, 0x74, 0xf1, 0xbe, 0xd3, 0xb1, 0x7b, 0x72, 0x66, 0xa4, + 0xa3, 0xbb, 0x86, 0xda, 0x7e, 0xba, 0x80, 0xb1, 0x01, 0xe1, 0x5c, 0xb7, 0x9d, 0xe9, + 0xa2, 0x07, 0x85, 0x2c, 0xf9, 0x12, 0x49, 0xef, 0x48, 0x06, 0x19, 0xff, 0x2a, 0xf8, + 0xca, 0xbc, 0xa8, 0x31, 0x25, 0xd1, 0xfa, 0xa9, 0x4c, 0xbb, 0x0a, 0x03, 0xa9, 0x06, + 0xf6, 0x83, 0xb3, 0xf4, 0x7a, 0x97, 0xc8, 0x71, 0xfd, 0x51, 0x3e, 0x51, 0x0a, 0x7a, + 0x25, 0xf2, 0x83, 0xb1, 0x96, 0x07, 0x57, 0x78, 0x49, 0x61, 0x52, 0xa9, 0x1c, 0x2b, + 0xf9, 0xda, 0x76, 0xeb, 0xe0, 0x89, 0xf4, 0x65, 0x48, 0x77, 0xf2, 0xd5, 0x86, 0xae, + 0x71, 0x49, 0xc4, 0x06, 0xe6, 0x63, 0xea, 0xde, 0xb2, 0xb5, 0xc7, 0xe8, 0x24, 0x29, + 0xb9, 0xe8, 0xcb, 0x48, 0x34, 0xc8, 0x34, 0x64, 0xf0, 0x79, 0x99, 0x53, 0x32, 0xe4, + 0xb3, 0xc8, 0xf5, 0xa7, 0x2b, 0xb4, 0xb8, 0xc6, 0xf7, 0x4b, 0x0d, 0x45, 0xdc, 0x6c, + 0x1f, 0x79, 0x95, 0x2c, 0x0b, 0x74, 0x20, 0xdf, 0x52, 0x5e, 0x37, 0xc1, 0x53, 0x77, + 0xb5, 0xf0, 0x98, 0x43, 0x19, 0xc3, 0x99, 0x39, 0x21, 0xe5, 0xcc, 0xd9, 0x7e, 0x09, + 0x75, 0x92, 0x06, 0x45, 0x30, 0xd3, 0x3d, 0xe3, 0xaf, 0xad, 0x57, 0x33, 0xcb, 0xe7, + 0x70, 0x3c, 0x52, 0x96, 0x26, 0x3f, 0x77, 0x34, 0x2e, 0xfb, 0xf5, 0xa0, 0x47, 0x55, + 0xb0, 0xb3, 0xc9, 0x97, 0xc4, 0x32, 0x84, 0x63, 0xe8, 0x4c, 0xaa, 0x2d, 0xe3, 0xff, + 0xdc, 0xd2, 0x97, 0xba, 0xaa, 0xac, 0xd7, 0xae, 0x64, 0x6e, 0x44, 0xb5, 0xc0, 0xf1, + 0x60, 0x44, 0xdf, 0x38, 0xfa, 0xbd, 0x29, 0x6a, 0x47, 0xb3, 0xa8, 0x38, 0xa9, 0x13, + 0x98, 0x2f, 0xb2, 0xe3, 0x70, 0xc0, 0x78, 0xed, 0xb0, 0x42, 0xc8, 0x4d, 0xb3, 0x4c, + 0xe3, 0x6b, 0x46, 0xcc, 0xb7, 0x64, 0x60, 0xa6, 0x90, 0xcc, 0x86, 0xc3, 0x02, 0x45, + 0x7d, 0xd1, 0xcd, 0xe1, 0x97, 0xec, 0x80, 0x75, 0xe8, 0x2b, 0x39, 0x3d, 0x54, 0x20, + 0x75, 0x13, 0x4e, 0x2a, 0x17, 0xee, 0x70, 0xa5, 0xe1, 0x87, 0x07, 0x5d, 0x03, 0xae, + 0x3c, 0x85, 0x3c, 0xff, 0x60, 0x72, 0x9b, 0xa4, 0x00, 0x00, 0x00, 0x05, 0x4d, 0xe1, + 0xf6, 0x96, 0x5b, 0xda, 0xbc, 0x67, 0x6c, 0x5a, 0x4d, 0xc7, 0xc3, 0x5f, 0x97, 0xf8, + 0x2c, 0xb0, 0xe3, 0x1c, 0x68, 0xd0, 0x4f, 0x1d, 0xad, 0x96, 0x31, 0x4f, 0xf0, 0x9e, + 0x6b, 0x3d, 0xe9, 0x6a, 0xee, 0xe3, 0x00, 0xd1, 0xf6, 0x8b, 0xf1, 0xbc, 0xa9, 0xfc, + 0x58, 0xe4, 0x03, 0x23, 0x36, 0xcd, 0x81, 0x9a, 0xaf, 0x57, 0x87, 0x44, 0xe5, 0x0d, + 0x13, 0x57, 0xa0, 0xe4, 0x28, 0x67, 0x04, 0xd3, 0x41, 0xaa, 0x0a, 0x33, 0x7b, 0x19, + 0xfe, 0x4b, 0xc4, 0x3c, 0x2e, 0x79, 0x96, 0x4d, 0x4f, 0x35, 0x10, 0x89, 0xf2, 0xe0, + 0xe4, 0x1c, 0x7c, 0x43, 0xae, 0x0d, 0x49, 0xe7, 0xf4, 0x04, 0xb0, 0xf7, 0x5b, 0xe8, + 0x0e, 0xa3, 0xaf, 0x09, 0x8c, 0x97, 0x52, 0x42, 0x0a, 0x8a, 0xc0, 0xea, 0x2b, 0xbb, + 0x1f, 0x4e, 0xeb, 0xa0, 0x52, 0x38, 0xae, 0xf0, 0xd8, 0xce, 0x63, 0xf0, 0xc6, 0xe5, + 0xe4, 0x04, 0x1d, 0x95, 0x39, 0x8a, 0x6f, 0x7f, 0x3e, 0x0e, 0xe9, 0x7c, 0xc1, 0x59, + 0x18, 0x49, 0xd4, 0xed, 0x23, 0x63, 0x38, 0xb1, 0x47, 0xab, 0xde, 0x9f, 0x51, 0xef, + 0x9f, 0xd4, 0xe1, 0xc1, + ]; + + let seed = hex!("a1c4696e2608035a886100d05cd99945eb3370731884a8235e2fb3d4d71f2547"); + let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); + let _expected_k = hex!("a1cd035833e0e90059603f26e07ad2aad152338e7a5e5984bcd5f7bb4eba40b7"); + + let mut lms_priv = PrivateKey::>::new_from_seed(id, seed); + lms_priv.q = 4; + let _lms_pub = lms_priv.public(); + + let msg = "The enumeration in the Constitution, of certain rights, shall not be construed to deny or disparage others retained by the people.\n".as_bytes(); + + use crate::ots::tests::ConstantRng; + let c = hex!("0eb1ed54a2460d512388cad533138d240534e97b1e82d33bd927d201dfc24ebb"); + + let mut rng = ConstantRng(&c); + let sig = lms_priv + .try_sign_with_rng(&mut rng, msg) + .unwrap() + .to_bytes(); + assert_eq!(sig.len(), expected_signature.len()); + assert_eq!(sig, expected_signature) + } +} diff --git a/lms/lms/lms/public.rs b/lms/lms/lms/public.rs new file mode 100644 index 00000000..526469f1 --- /dev/null +++ b/lms/lms/lms/public.rs @@ -0,0 +1,301 @@ +use std::cmp::Ordering; +use std::ops::Add; + +use crate::constants::{D_LEAF, ID_LEN}; + +use crate::error::LmsDeserializeError; +use crate::lms::Signature; +use crate::types::Typecode; +use crate::{constants::D_INTR, lms::LmsMode}; +use digest::{Digest, OutputSizeUser}; +use generic_array::{ArrayLength, GenericArray}; +use signature::{Error, Verifier}; +use typenum::{Sum, U24}; + +//use crate::signature::Signature as Signature; +use crate::types::Identifier; + +use digest::Output; + +#[derive(Debug)] +/// Opaque struct representing a LMS public key +pub struct PublicKey { + pub(crate) id: Identifier, + pub(crate) k: Output, +} + +impl Clone for PublicKey { + fn clone(&self) -> Self { + Self { + id: self.id, + k: self.k.clone(), + } + } +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + self.id == other.id && self.k == other.k + } +} + +impl PublicKey { + pub fn new(id: Identifier, k: Output) -> Self { + Self { id, k } + } + + /// Returns the 16-byte identifier of the public key + pub fn id(&self) -> &Identifier { + &self.id + } + + /// Returns the N-byte public key as a byte slice + pub fn k(&self) -> &[u8] { + &self.k + } +} + +impl Verifier> for PublicKey { + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), Error> { + // Compute the LMS Public Key Candidate Tc from the signature, + // message, identifier, pubtype, and ots_typecode, using + // Algorithm 6a. + let key_candidate = signature + .lmots_sig + .recover_pubkey(self.id, signature.q, msg); + + let mut node_num = signature.q + Mode::LEAVES; + let mut tmp = Mode::Hasher::new() + .chain_update(self.id) + .chain_update(node_num.to_be_bytes()) + .chain_update(D_LEAF) + .chain_update(key_candidate.k) + .finalize(); + + for i in 0..Mode::H { + // Tc = H(I || u32str(node_num/2) || u16str(D_INTR) || path[i] || tmp) + let mut hasher = Mode::Hasher::new() + .chain_update(self.id) + .chain_update((node_num / 2).to_be_bytes()) + .chain_update(D_INTR); + if node_num % 2 == 1 { + hasher.update(&signature.path[i]); + hasher.update(&tmp); + } else { + // Tc = H(I || u32str(node_num/2) || u16str(D_INTR) || tmp || path[i]) + hasher.update(&tmp); + hasher.update(&signature.path[i]); + } + hasher.finalize_into(&mut tmp); + node_num /= 2; + } + if self.k == tmp { + Ok(()) + } else { + Err(Error::new()) + } + } +} + +/// Converts a [PublicKey] into its byte representation +impl From> + for GenericArray::OutputSize, U24>> +where + ::OutputSize: Add, + Sum<::OutputSize, U24>: ArrayLength, +{ + fn from(pk: PublicKey) -> Self { + // Return u32(type) || u32(otstype) || id || k + GenericArray::from_exact_iter( + std::iter::empty() + .chain(Mode::TYPECODE.to_be_bytes()) + .chain(Mode::OtsMode::TYPECODE.to_be_bytes()) + .chain(pk.id) + .chain(pk.k), + ) + .unwrap() + } +} + +/// Tries to parse a [PublicKey] from an exact slice +impl<'a, Mode: LmsMode> TryFrom<&'a [u8]> for PublicKey { + type Error = LmsDeserializeError; + + fn try_from(pk: &'a [u8]) -> Result { + let expected_len = Mode::M + ID_LEN + 8; + + match pk.len().cmp(&expected_len) { + Ordering::Less => return Err(LmsDeserializeError::TooShort), + Ordering::Greater => return Err(LmsDeserializeError::TooLong), + Ordering::Equal => (), + }; + + let (alg, pk) = pk.split_at(4); + + // will never panic because we already checked the length + if u32::from_be_bytes(alg.try_into().unwrap()) != Mode::TYPECODE { + return Err(LmsDeserializeError::WrongAlgorithm); + } + + // pk is now guaranteed to be of the form u32(otstype) || ID || K + let (otstype, id_k) = pk.split_at(4); + + // Check that otstype is correct + if u32::from_be_bytes(otstype.try_into().unwrap()) != Mode::OtsMode::TYPECODE { + return Err(LmsDeserializeError::WrongAlgorithm); + } + + let (id, k) = id_k.split_at(ID_LEN); + + Ok(Self { + id: id.try_into().unwrap(), + k: GenericArray::clone_from_slice(k), + }) + } +} + +#[cfg(test)] +mod tests { + use std::ops::Add; + + use crate::{ + lms::modes::*, + lms::PrivateKey, + lms::PublicKey, + ots::{LmsOtsSha256N32W4, LmsOtsSha256N32W8}, + }; + use digest::OutputSizeUser; + use generic_array::{ArrayLength, GenericArray}; + use hex_literal::hex; + use typenum::{Sum, U24}; + + const KAT1: [u8; 56] = hex!( + " + 00000005 + 00000004 + 61a5d57d37f5e46bfb7520806b07a1b8 + 50650e3b31fe4a773ea29a07f09cf2ea + 30e579f0df58ef8e298da0434cb2b878" + ); + + #[test] + fn test_pubkey_deserialize_kat1() { + let pk = PublicKey::>::try_from(&KAT1[..]).unwrap(); + let expected = PublicKey::> { + id: hex!("61a5d57d37f5e46bfb7520806b07a1b8"), + k: hex!("50650e3b31fe4a773ea29a07f09cf2ea30e579f0df58ef8e298da0434cb2b878").into(), + }; + assert_eq!(pk, expected); + } + + #[test] + fn test_pubkey_deserialize_kat1_wrong_lms_mode() { + let pk = PublicKey::>::try_from(&KAT1[..]); + assert_eq!(pk, Err(crate::error::LmsDeserializeError::WrongAlgorithm)); + } + + #[test] + fn test_pubkey_deserialize_kat1_wrong_otsmode() { + let pk = PublicKey::>::try_from(&KAT1[..]); + assert_eq!(pk, Err(crate::error::LmsDeserializeError::WrongAlgorithm)); + } + + #[test] + fn test_pubkey_deserialize_kat1_too_short() { + let pk_bytes = &KAT1[..(KAT1.len() - 4)]; + let pk = PublicKey::>::try_from(pk_bytes); + assert_eq!(pk, Err(crate::error::LmsDeserializeError::TooShort)); + } + + #[test] + fn test_pubkey_deserialize_kat1_too_long() { + let mut pk_bytes = vec![42; 4]; + pk_bytes.extend_from_slice(&KAT1[..]); + + let pk = PublicKey::>::try_from(&pk_bytes[..]); + assert_eq!(pk, Err(crate::error::LmsDeserializeError::TooLong)); + } + + #[test] + fn test_kat1_round_trip() { + let pk_bytes = hex!( + " + 00000005 + 00000004 + 61a5d57d37f5e46bfb7520806b07a1b8 + 50650e3b31fe4a773ea29a07f09cf2ea + 30e579f0df58ef8e298da0434cb2b878" + ); + let pk = PublicKey::>::try_from(&pk_bytes[..]).unwrap(); + let pk_serialized: GenericArray = pk.clone().into(); + let bytes = pk_serialized.as_slice(); + assert_eq!(bytes, &pk_bytes[..]); + } + + #[test] + fn test_kat2() { + // Tests that the serialized public key from RFC seed matches the expected value + let seed = hex!("a1c4696e2608035a886100d05cd99945eb3370731884a8235e2fb3d4d71f2547"); + let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); + let expected_pubkey = hex!( + " + 00000005 + 00000004 + 215f83b7ccb9acbcd08db97b0d04dc2b + a1cd035833e0e90059603f26e07ad2aa + d152338e7a5e5984bcd5f7bb4eba40b7 + " + ); + let lms_priv = PrivateKey::>::new_from_seed(id, seed); + let lms_pub = lms_priv.public(); + let lms_pub_serialized: GenericArray = lms_pub.into(); + let bytes = lms_pub_serialized.as_slice(); + assert_eq!(bytes, &expected_pubkey[..]); + } + + fn test_serialize_deserialize_random() + where + PublicKey: std::fmt::Debug, + ::OutputSize: Add, + Sum<::OutputSize, U24>: ArrayLength, + { + let rng = rand::thread_rng(); + let lms_priv = PrivateKey::::new(rng); + let lms_pub = lms_priv.public(); + let lms_pub_serialized: GenericArray< + u8, + Sum<::OutputSize, U24>, + > = lms_pub.clone().into(); + let bytes = lms_pub_serialized.as_slice(); + let lms_pub_deserialized = PublicKey::::try_from(bytes).unwrap(); + assert_eq!(lms_pub, lms_pub_deserialized); + } + + #[test] + fn test_serialize_deserialize_random_lms_sha256_m32_h5_lms_ots_sha256_n32_w8() { + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_lms_sha256_m32_h10_lms_ots_sha256_n32_w8() { + test_serialize_deserialize_random::>(); + } + + // These tests use too much memory and overflow the stack + /* + #[test] + fn test_serialize_deserialize_random_lms_sha256_m32_h15_lms_ots_sha256_n32_w8(){ + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_lms_sha256_m32_h20_lms_ots_sha256_n32_w8(){ + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_lms_sha256_m32_h25_lms_ots_sha256_n32_w8(){ + test_serialize_deserialize_random::>(); + } + */ +} diff --git a/lms/lms/lms/signature.rs b/lms/lms/lms/signature.rs new file mode 100644 index 00000000..ef1ba5dd --- /dev/null +++ b/lms/lms/lms/signature.rs @@ -0,0 +1,317 @@ +//! Contains the [Signature] type + +use crate::error::LmsDeserializeError; +use crate::lms::LmsMode; +use crate::ots::modes::LmsOtsMode; +use crate::ots::Signature as OtsSignature; +use generic_array::{ArrayLength, GenericArray}; +use signature::SignatureEncoding; + +use std::{ + cmp::Ordering, + ops::{Add, Mul}, +}; + +use typenum::{Prod, Sum, U1, U4}; + +/// Opaque struct representing a LMS signature +pub struct Signature { + pub(crate) q: u32, // TODO: do these really need to be public? + pub(crate) lmots_sig: OtsSignature, + pub(crate) path: GenericArray, Mode::HLen>, +} + +// manual implementation is required to not require bounds on Mode +impl Clone for Signature { + fn clone(&self) -> Self { + Self { + q: self.q, + lmots_sig: self.lmots_sig.clone(), + path: self.path.clone(), + } + } +} + +// manual implementation is required to not require bounds on Mode +impl PartialEq for Signature { + fn eq(&self, other: &Self) -> bool { + self.q == other.q && self.lmots_sig == other.lmots_sig && self.path == other.path + } +} + +impl SignatureEncoding for Signature +where + ::PLen: Add, + ::NLen: Mul::PLen, U1>>, + Prod<::NLen, Sum<::PLen, U1>>: + Add, + Sum< + Prod<::NLen, Sum<::PLen, U1>>, + U4, + >: ArrayLength, +{ + type Repr = Vec; // TODO: GenericArray +} + +impl From> for Vec +where + ::PLen: Add, + ::NLen: Mul::PLen, U1>>, + Prod<::NLen, Sum<::PLen, U1>>: + Add, + Sum< + Prod<::NLen, Sum<::PLen, U1>>, + U4, + >: ArrayLength, +{ + fn from(val: Signature) -> Self { + let mut sig = Vec::new(); + sig.extend_from_slice(&val.q.to_be_bytes()); + let lms_sig: GenericArray = val.lmots_sig.into(); + sig.extend_from_slice(&lms_sig); + sig.extend_from_slice(&Mode::TYPECODE.to_be_bytes()); + for node in val.path { + sig.extend_from_slice(&node); + } + sig + } +} + +/// Tries to parse a [Signature] from an exact slice +impl TryFrom<&[u8]> for Signature { + type Error = LmsDeserializeError; + + fn try_from(sig: &[u8]) -> Result { + // Follows the validations in algorithm 6a of RFC 8554 + + // Fully check signature length up-front. Removes need for checks as we go. + match sig + .len() + .cmp(&(8 + Mode::OtsMode::SIG_LEN + Mode::M * Mode::H)) + { + Ordering::Less => return Err(LmsDeserializeError::TooShort), + Ordering::Greater => return Err(LmsDeserializeError::TooLong), + Ordering::Equal => (), + }; + + // 6a.2.a: Get q + let (q_bytes, sig) = sig.split_at(4); + let q = u32::from_be_bytes(q_bytes.try_into().unwrap()); + + // 6a.2.i: If q >= 2^H, return INVALID. + if q >= 1 << Mode::H { + return Err(LmsDeserializeError::InvalidQ); + } + + // We already checked that this won't panic + let (lmots_sig, sig) = sig.split_at(Mode::OtsMode::SIG_LEN); + + // Checks that the OTS mode is correct happen inside `try_from` + let ots_signature = OtsSignature::::try_from(lmots_sig)?; + + // 6a.2.f: Get LMS typecode (sigtype) + let (sigtype, path) = sig.split_at(4); + + // 6a.2.g: If the OTS typecode is not equal to the typecode of the + // expected LM-OTS Mode, return INVALID. + if u32::from_be_bytes(sigtype.try_into().unwrap()) != Mode::TYPECODE { + return Err(LmsDeserializeError::WrongAlgorithm); + } + + // Path length is already validated by initial length check + let path = path + .chunks_exact(Mode::M) + .map(GenericArray::clone_from_slice) + .collect(); + + Ok(Self { + q, + lmots_sig: ots_signature, + path, + }) + } +} + +#[cfg(test)] +mod tests { + use std::ops::{Add, Mul}; + + use crate::lms::modes::*; + use crate::lms::{PrivateKey, PublicKey, Signature}; + use crate::ots::modes::*; + use generic_array::ArrayLength; + use hex_literal::hex; + use rand::thread_rng; + use signature::{RandomizedSignerMut, Verifier}; + use typenum::{Prod, Sum, U1, U4}; + + #[test] + fn test_deserialize_kat1() { + let pk_bytes = hex!("0000000500000004d2f14ff6346af964569f7d6cb880a1b66c5004917da6eafe4d9ef6c6407b3db0e5485b122d9ebe15cda93cfec582d7ab"); + let sig_bytes = hex!( + " + 0000000a + 00000004 + 0703c491e7558b35011ece3592eaa5da + 4d918786771233e8353bc4f62323185c + 95cae05b899e35dffd71705470620998 + 8ebfdf6e37960bb5c38d7657e8bffeef + 9bc042da4b4525650485c66d0ce19b31 + 7587c6ba4bffcc428e25d08931e72dfb + 6a120c5612344258b85efdb7db1db9e1 + 865a73caf96557eb39ed3e3f426933ac + 9eeddb03a1d2374af7bf771855774562 + 37f9de2d60113c23f846df26fa942008 + a698994c0827d90e86d43e0df7f4bfcd + b09b86a373b98288b7094ad81a0185ac + 100e4f2c5fc38c003c1ab6fea479eb2f + 5ebe48f584d7159b8ada03586e65ad9c + 969f6aecbfe44cf356888a7b15a3ff07 + 4f771760b26f9c04884ee1faa329fbf4 + e61af23aee7fa5d4d9a5dfcf43c4c26c + e8aea2ce8a2990d7ba7b57108b47dabf + beadb2b25b3cacc1ac0cef346cbb90fb + 044beee4fac2603a442bdf7e507243b7 + 319c9944b1586e899d431c7f91bcccc8 + 690dbf59b28386b2315f3d36ef2eaa3c + f30b2b51f48b71b003dfb08249484201 + 043f65f5a3ef6bbd61ddfee81aca9ce6 + 0081262a00000480dcbc9a3da6fbef5c + 1c0a55e48a0e729f9184fcb1407c3152 + 9db268f6fe50032a363c9801306837fa + fabdf957fd97eafc80dbd165e435d0e2 + dfd836a28b354023924b6fb7e48bc0b3 + ed95eea64c2d402f4d734c8dc26f3ac5 + 91825daef01eae3c38e3328d00a77dc6 + 57034f287ccb0f0e1c9a7cbdc828f627 + 205e4737b84b58376551d44c12c3c215 + c812a0970789c83de51d6ad787271963 + 327f0a5fbb6b5907dec02c9a90934af5 + a1c63b72c82653605d1dcce51596b3c2 + b45696689f2eb382007497557692caac + 4d57b5de9f5569bc2ad0137fd47fb47e + 664fcb6db4971f5b3e07aceda9ac130e + 9f38182de994cff192ec0e82fd6d4cb7 + f3fe00812589b7a7ce51544045643301 + 6b84a59bec6619a1c6c0b37dd1450ed4 + f2d8b584410ceda8025f5d2d8dd0d217 + 6fc1cf2cc06fa8c82bed4d944e71339e + ce780fd025bd41ec34ebff9d4270a322 + 4e019fcb444474d482fd2dbe75efb203 + 89cc10cd600abb54c47ede93e08c114e + db04117d714dc1d525e11bed8756192f + 929d15462b939ff3f52f2252da2ed64d + 8fae88818b1efa2c7b08c8794fb1b214 + aa233db3162833141ea4383f1a6f120b + e1db82ce3630b3429114463157a64e91 + 234d475e2f79cbf05e4db6a9407d72c6 + bff7d1198b5c4d6aad2831db61274993 + 715a0182c7dc8089e32c8531deed4f74 + 31c07c02195eba2ef91efb5613c37af7 + ae0c066babc69369700e1dd26eddc0d2 + 16c781d56e4ce47e3303fa73007ff7b9 + 49ef23be2aa4dbf25206fe45c20dd888 + 395b2526391a724996a44156beac8082 + 12858792bf8e74cba49dee5e8812e019 + da87454bff9e847ed83db07af3137430 + 82f880a278f682c2bd0ad6887cb59f65 + 2e155987d61bbf6a88d36ee93b6072e6 + 656d9ccbaae3d655852e38deb3a2dcf8 + 058dc9fb6f2ab3d3b3539eb77b248a66 + 1091d05eb6e2f297774fe6053598457c + c61908318de4b826f0fc86d4bb117d33 + e865aa805009cc2918d9c2f840c4da43 + a703ad9f5b5806163d7161696b5a0adc + 00000005 + d5c0d1bebb06048ed6fe2ef2c6cef305 + b3ed633941ebc8b3bec9738754cddd60 + e1920ada52f43d055b5031cee6192520 + d6a5115514851ce7fd448d4a39fae2ab + 2335b525f484e9b40d6a4a969394843b + dcf6d14c48e8015e08ab92662c05c6e9 + f90b65a7a6201689999f32bfd368e5e3 + ec9cb70ac7b8399003f175c40885081a + 09ab3034911fe125631051df0408b394 + 6b0bde790911e8978ba07dd56c73e7ee + " + ); + let msg = hex!( + " + 54686520706f77657273206e6f742064 + 656c65676174656420746f2074686520 + 556e6974656420537461746573206279 + 2074686520436f6e737469747574696f + 6e2c206e6f722070726f686962697465 + 6420627920697420746f207468652053 + 74617465732c20617265207265736572 + 76656420746f20746865205374617465 + 7320726573706563746976656c792c20 + 6f7220746f207468652070656f706c65 + 2e0a" + ); + let pk = PublicKey::>::try_from(&pk_bytes[..]).unwrap(); + let sig = Signature::>::try_from(&sig_bytes[..]).unwrap(); + assert!(pk.verify(&msg[..], &sig).is_ok()); + } + + fn test_serialize_deserialize_random() + where + ::PLen: Add, + ::NLen: Mul::PLen, U1>>, + Prod<::NLen, Sum<::PLen, U1>>: + Add, + Sum< + Prod<::NLen, Sum<::PLen, U1>>, + U4, + >: ArrayLength, + { + let mut rng = thread_rng(); + let mut sk = PrivateKey::::new(&mut rng); + let pk = sk.public(); + let msg = b"Hello, world!"; + let sig = sk.sign_with_rng(&mut rng, msg); + let sig_bytes: Vec<_> = sig.clone().into(); + let sig2 = Signature::::try_from(&sig_bytes[..]).unwrap(); + assert!(pk.verify(msg, &sig2).is_ok()); + } + + #[test] + fn test_serialize_deserialize_random_h5_w1() { + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_h5_w2() { + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_h5_w4() { + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_h5_w8() { + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_h10_w1() { + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_h10_w2() { + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_h10_w4() { + test_serialize_deserialize_random::>(); + } + + #[test] + fn test_serialize_deserialize_random_h10_w8() { + test_serialize_deserialize_random::>(); + } +} diff --git a/lms/lms/ots/keypair.rs b/lms/lms/ots/keypair.rs new file mode 100644 index 00000000..a66928a4 --- /dev/null +++ b/lms/lms/ots/keypair.rs @@ -0,0 +1,13 @@ +use crate::ots::modes::LmsOtsMode; +use crate::ots::private::PrivateKey; +use crate::ots::public::PublicKey; +use signature::Keypair; + +// implements the Keypair trait for PrivateKey +impl Keypair for PrivateKey { + type VerifyingKey = PublicKey; + + fn verifying_key(&self) -> Self::VerifyingKey { + self.public() + } +} diff --git a/lms/lms/ots/mod.rs b/lms/lms/ots/mod.rs new file mode 100644 index 00000000..b60674ea --- /dev/null +++ b/lms/lms/ots/mod.rs @@ -0,0 +1,203 @@ +//! Everything related to LM-OTS + +mod keypair; +pub(crate) mod modes; +mod private; +mod public; +pub mod signature; +mod util; + +pub use modes::{ + LmsOtsMode, LmsOtsSha256N32W1, LmsOtsSha256N32W2, LmsOtsSha256N32W4, LmsOtsSha256N32W8, +}; +pub use private::PrivateKey; +pub use public::PublicKey; +pub use signature::Signature; + +#[cfg(test)] +pub mod tests { + use crate::constants::ID_LEN; + use crate::ots::modes::{ + LmsOtsMode, LmsOtsSha256N32W1, LmsOtsSha256N32W2, LmsOtsSha256N32W4, LmsOtsSha256N32W8, + }; + use crate::ots::private::PrivateKey; + use digest::Digest; + use digest::OutputSizeUser; + use generic_array::{ArrayLength, GenericArray}; + use hex_literal::hex; + use rand::thread_rng; + use rand_core::{CryptoRng, RngCore}; + use signature::RandomizedSignerMut; + use signature::Verifier; + use std::matches; + use std::ops::Add; + use typenum::{Sum, U2}; + + // tests that a signature signed with a private key verifies under + // its public key + fn test_sign() + where + ::OutputSize: Add, + Sum<::OutputSize, U2>: ArrayLength, + { + let mut rng = thread_rng(); + let mut sk = PrivateKey::::new(0, [0xcc; ID_LEN], &mut rng); + let pk = sk.public(); + let msg = "this is a test message".as_bytes(); + + assert!(sk.is_valid()); + let sig = sk.try_sign_with_rng(&mut rng, msg); + assert!(!sk.is_valid()); + + assert!(sig.is_ok()); + + let sig = sig.unwrap(); + let result = pk.verify(msg, &sig); + + assert!(matches!(result, Ok(()))); + } + + // tests that a signature signed with a private key does not verify under + // a different public key + fn test_sign_fail_verify() + where + ::OutputSize: Add, + Sum<::OutputSize, U2>: ArrayLength, + { + let mut rng = thread_rng(); + let mut sk = PrivateKey::::new(0, [0xcc; ID_LEN], &mut rng); + let mut pk = sk.public(); + let msg = "this is a test message".as_bytes(); + + assert!(sk.is_valid()); + let sig = sk.try_sign_with_rng(&mut rng, msg); + assert!(!sk.is_valid()); + + assert!(sig.is_ok()); + + let sig = sig.unwrap(); + // modify q to get the wrong public key + pk.q = 1; + let result = pk.verify(msg, &sig); + + assert!(result.is_err()); + } + + #[test] + fn test_signverify_sha256_n32_w1() { + test_sign::(); + } + + #[test] + fn test_signverify_sha256_n32_w2() { + test_sign::(); + } + + #[test] + fn test_signverify_sha256_n32_w4() { + test_sign::(); + } + + #[test] + fn test_signverify_sha256_n32_w8() { + test_sign::(); + } + + #[test] + fn test_sign_fail_verify_sha256_n32_w1() { + test_sign_fail_verify::(); + } + + #[test] + fn test_sign_fail_verify_sha256_n32_w2() { + test_sign_fail_verify::(); + } + + #[test] + fn test_sign_fail_verify_sha256_n32_w4() { + test_sign_fail_verify::(); + } + + #[test] + fn test_sign_fail_verify_sha256_n32_w8() { + test_sign_fail_verify::(); + } + + /// Constant RNG for testing purposes only. + pub struct ConstantRng<'a>(pub &'a [u8]); + + impl<'a> RngCore for ConstantRng<'a> { + fn next_u32(&mut self) -> u32 { + let (head, tail) = self.0.split_at(4); + self.0 = tail; + u32::from_be_bytes(head.try_into().unwrap()) + } + + fn next_u64(&mut self) -> u64 { + let (head, tail) = self.0.split_at(8); + self.0 = tail; + u64::from_be_bytes(head.try_into().unwrap()) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + let (hd, tl) = self.0.split_at(dest.len()); + dest.copy_from_slice(hd); + self.0 = tl; + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + if dest.len() > self.0.len() { + return Err(rand_core::Error::new("not enough bytes")); + } + let (hd, tl) = self.0.split_at(dest.len()); + dest.copy_from_slice(hd); + self.0 = tl; + Ok(()) + } + } + + /// WARNING: This is not a secure cryptographic RNG. It is only used for testing. + impl CryptoRng for ConstantRng<'_> {} + + #[test] + /// Test Case 2, Appendix F. LMS level 2. https://datatracker.ietf.org/doc/html/rfc8554#appendix-F + fn test_sign_kat1() { + let seed = hex!("a1c4696e2608035a886100d05cd99945eb3370731884a8235e2fb3d4d71f2547"); + let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); + let q = 4; + let y0 = hex!("11b3649023696f85150b189e50c00e98850ac343a77b3638319c347d7310269d"); + let mut sk = PrivateKey::::new_from_seed(q, id, seed); + let _ = sk.public(); + + let c = hex!("0eb1ed54a2460d512388cad533138d240534e97b1e82d33bd927d201dfc24ebb"); + let mut rng = ConstantRng(&c); + let msg = "The enumeration in the Constitution, of certain rights, shall not be construed to deny or disparage others retained by the people.\n".as_bytes(); + let sig = sk.try_sign_with_rng(&mut rng, msg).unwrap(); + + assert_eq!(&sig.c, GenericArray::from_slice(&c)); + assert_eq!(&sig.y[0], GenericArray::from_slice(&y0)); + } + + #[test] + // Tests that the public key generated from a given seed matches the expected value. + fn test_keygen_kat() { + let seed = hex!("a1c4696e2608035a886100d05cd99945eb3370731884a8235e2fb3d4d71f2547"); + let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); + let q = 5; + // Test Case 2, Appendix F. final signature. path[0] + // https://datatracker.ietf.org/doc/html/rfc8554#appendix-F + + let k = hex!("4de1f6965bdabc676c5a4dc7c35f97f82cb0e31c68d04f1dad96314ff09e6b3d"); + + let sk = PrivateKey::::new_from_seed(q, id, seed); + let pk = sk.public(); + // H(I||u32str(r)||u16str(D_LEAF)||OTS_PUB_HASH[r-2^h]) + let x = ::Hasher::new() + .chain_update(pk.id) + .chain_update((pk.q + (1 << 5)).to_be_bytes()) + .chain_update(crate::constants::D_LEAF) + .chain_update(pk.k) + .finalize(); + assert_eq!(&x[..], &k[..]); + } +} diff --git a/lms/lms/ots/modes.rs b/lms/lms/ots/modes.rs new file mode 100644 index 00000000..427d41c1 --- /dev/null +++ b/lms/lms/ots/modes.rs @@ -0,0 +1,219 @@ +use crate::ots::util::coefs; +use crate::types::Typecode; +use digest::{Digest, Output}; +use generic_array::{ArrayLength, GenericArray}; +use sha2::Sha256; +use static_assertions::const_assert_eq; +use std::marker::PhantomData; +use typenum::consts::{U133, U265, U34, U67}; +use typenum::Unsigned; + +/// The basic trait that must be implemented by any OTS mode. +pub trait LmsOtsMode: Typecode { + /// The underlying hash function + type Hasher: Digest; + /// The length of the hash function output as a type + type NLen: ArrayLength; + /// The value of P as a type + type PLen: ArrayLength> + ArrayLength; + /// The length of the hash function output as a [usize] + const N: usize; + /// The Winternitz window, which should be a value that divides 8 + const W: usize; + /// The number of `W` bit fields required to contain the hash of the message + const U: usize; // internal value calculated as https://datatracker.ietf.org/doc/html/rfc8554#appendix-B + /// The number of `W` bit fields required to contain the checksum + const V: usize; // see above + /// Computed as `U` + `V` + const P: usize; + /// The left shift required to get the checksum bits + const LS: usize; + /// The total length of the signature + const SIG_LEN: usize; + + /// Expands a message into its Winternitz coefficients and checksum + fn expand(message: &Output) -> GenericArray { + // Returns an array containing Coefs(message, w, U) || Coefs(checksum, w, V) + // where Coefs(M, w, L) is an array containing coef(M, i, w) for each i in 0..L + // See RFC 8554 section 3.1.3. + + // Expand the message into its coefficients + // Immediately allocates full expanded length, but only the first U coefficients are used + // in this step + let mut arr: GenericArray::PLen> = GenericArray::default(); + for (i, c) in coefs(message, Self::W).enumerate().take(Self::U) { + arr[i] = c; + } + + // Compute the checksum as described in RFC 8554 section 4.4 + // The checksum is the sum of all "negated" chunks + // This means that if every chunk of message `a` is <= each corresponding chunk of message ``b``, + // then the checksum of `a` is greater than the checksum of `b` + + // The negation is done by subtracting the chunk value from 2^W - 1 + let cksum = (&arr) + .into_iter() + .take(Self::U) + .map(|&x| ((1u16 << Self::W) - 1 - (x as u16))) + .sum::() + << Self::LS; + + // The checksum itself in then expanded into its coefficients and appended to the message coefficients + let cksum_bytes = cksum.to_be_bytes(); + let cksum_chunks = coefs(&cksum_bytes, Self::W).take(Self::V); + + for (i, c) in cksum_chunks.enumerate() { + arr[Self::U + i] = c; + } + arr + } +} + +#[derive(Debug)] +pub struct LmsOtsModeInternal< + Hasher: Digest, + const W: usize, + PP: ArrayLength> + ArrayLength, + const TC: u32, +> { + _phantomdata: PhantomData<(Hasher, PP)>, +} + +impl< + Hasher: Digest, + const W: usize, + PP: ArrayLength> + ArrayLength, + const TC: u32, + > Typecode for LmsOtsModeInternal +{ + const TYPECODE: u32 = TC; +} + +/// because trait associated consts cannot be used as generic values, we work around this by passing in an additional +/// type representing the array length P used for private keys, which gets checked via some static asserts +/// +/// NLen and N are calculated using the associated OutputSize of the given Digest, as specified by +/// https://datatracker.ietf.org/doc/html/rfc8554#section-4.1 +impl< + Hasher: Digest, + const W: usize, + PP: ArrayLength> + ArrayLength, + const TC: u32, + > LmsOtsMode for LmsOtsModeInternal +{ + type Hasher = Hasher; + type NLen = Hasher::OutputSize; + type PLen = PP; + const N: usize = Hasher::OutputSize::USIZE; + const W: usize = W; + const U: usize = (8 * Self::N + W - 1) / W; + const V: usize = ((((1 << W) - 1) * Self::U).ilog2() as usize / W) + 1; + const P: usize = Self::U + Self::V; + const LS: usize = 16 - Self::V * W; + const SIG_LEN: usize = 4 + Self::N * (Self::P + 1); +} + +/// `LMOTS_SHA256_N32_W1` +pub type LmsOtsSha256N32W1 = LmsOtsModeInternal; +/// `LMOTS_SHA256_N32_W2` +pub type LmsOtsSha256N32W2 = LmsOtsModeInternal; +/// `LMOTS_SHA256_N32_W4` +pub type LmsOtsSha256N32W4 = LmsOtsModeInternal; +/// `LMOTS_SHA256_N32_W8` +pub type LmsOtsSha256N32W8 = LmsOtsModeInternal; + +// make sure that the auto generated N, P, LS, SIG_LEN values are correct +const_assert_eq!( + ::NLen::USIZE, + LmsOtsSha256N32W1::N +); +const_assert_eq!( + ::PLen::USIZE, + LmsOtsSha256N32W1::P +); +const_assert_eq!(LmsOtsSha256N32W1::N, 32); +const_assert_eq!(LmsOtsSha256N32W1::P, 265); +const_assert_eq!(LmsOtsSha256N32W1::LS, 7); +const_assert_eq!(LmsOtsSha256N32W1::SIG_LEN, 8516); + +const_assert_eq!( + ::NLen::USIZE, + LmsOtsSha256N32W2::N +); +const_assert_eq!( + ::PLen::USIZE, + LmsOtsSha256N32W2::P +); +const_assert_eq!(LmsOtsSha256N32W2::N, 32); +const_assert_eq!(LmsOtsSha256N32W2::P, 133); +const_assert_eq!(LmsOtsSha256N32W2::LS, 6); +const_assert_eq!(LmsOtsSha256N32W2::SIG_LEN, 4292); + +const_assert_eq!( + ::NLen::USIZE, + LmsOtsSha256N32W4::N +); +const_assert_eq!( + ::PLen::USIZE, + LmsOtsSha256N32W4::P +); +const_assert_eq!(LmsOtsSha256N32W4::N, 32); +const_assert_eq!(LmsOtsSha256N32W4::P, 67); +const_assert_eq!(LmsOtsSha256N32W4::LS, 4); +const_assert_eq!(LmsOtsSha256N32W4::SIG_LEN, 2180); + +const_assert_eq!( + ::NLen::USIZE, + LmsOtsSha256N32W8::N +); +const_assert_eq!( + ::PLen::USIZE, + LmsOtsSha256N32W8::P +); +const_assert_eq!(LmsOtsSha256N32W8::N, 32); +const_assert_eq!(LmsOtsSha256N32W8::P, 34); +const_assert_eq!(LmsOtsSha256N32W8::LS, 0); +const_assert_eq!(LmsOtsSha256N32W8::SIG_LEN, 1124); + +#[cfg(test)] +mod test { + use generic_array::GenericArray; + + use super::LmsOtsMode; + #[test] + fn test_checksum_zero_w1() { + let arr = [0u8; super::LmsOtsSha256N32W1::N]; + let cksm = super::LmsOtsSha256N32W1::expand(GenericArray::from_slice(&arr)); + assert_eq!( + &cksm[super::LmsOtsSha256N32W1::U..], + &[1, 0, 0, 0, 0, 0, 0, 0, 0] + ); + } + + #[test] + fn test_checksum_ones_w1() { + let arr = [255u8; super::LmsOtsSha256N32W1::N]; + let cksm = super::LmsOtsSha256N32W1::expand(GenericArray::from_slice(&arr)); + assert_eq!( + &cksm[super::LmsOtsSha256N32W1::U..], + &[0, 0, 0, 0, 0, 0, 0, 0, 0] + ); + } + + #[test] + fn test_checksum_ten_w4() { + let arr = [0xaa; super::LmsOtsSha256N32W4::N]; + let cksm = super::LmsOtsSha256N32W4::expand(GenericArray::from_slice(&arr)); + assert_eq!(&cksm[super::LmsOtsSha256N32W4::U..], &[0x01, 0x04, 0x00]); + } + + #[test] + fn test_expand_zero_w8() { + let arr = [0u8; super::LmsOtsSha256N32W8::N]; + let expanded = super::LmsOtsSha256N32W8::expand(GenericArray::from_slice(&arr)); + let mut expected = [0u8; super::LmsOtsSha256N32W8::P]; + expected[super::LmsOtsSha256N32W8::U] = 0x1f; + expected[super::LmsOtsSha256N32W8::U + 1] = 0xe0; + assert_eq!(&expanded.as_slice(), &expected); + } +} diff --git a/lms/lms/ots/private.rs b/lms/lms/ots/private.rs new file mode 100644 index 00000000..2cfaea04 --- /dev/null +++ b/lms/lms/ots/private.rs @@ -0,0 +1,149 @@ +use crate::constants::{D_MESG, D_PBLC}; +use crate::ots::modes::LmsOtsMode; +use crate::ots::public::PublicKey; +use crate::ots::signature::Signature; + +use crate::types::Identifier; +use anyhow::anyhow; +use digest::{Digest, Output}; +use generic_array::sequence::GenericSequence; +use generic_array::GenericArray; +use rand_core::CryptoRngCore; +use signature::{Error, RandomizedSignerMut}; +use zeroize::Zeroize; +//use std::mem::MaybeUninit; + +#[derive(Debug)] +/// Opaque struct representing an LM-OTS private key. Does not implement +/// [Clone] because OTS keys are supposed to be one time use. +pub struct PrivateKey { + q: u32, + id: Identifier, + x: GenericArray, Mode::PLen>, + valid: bool, +} + +impl PrivateKey { + /// Generate a private key, expanded pseudorandomly from a seed generated by `rng`. + /// Uses the algorithm from appendix A + // a key part of this code working is the DerefMut impl for GenericArray which we abuse in a similar manner to + // generic_array::ArrayBuilder's internal implementation + /// If LM-OTS is being used directly, q MUST be set to the all-zero value + /// + pub fn new(q: u32, id: Identifier, rng: &mut impl CryptoRngCore) -> Self { + let mut seed: GenericArray = GenericArray::default(); + rng.fill_bytes(&mut seed); + Self::new_from_seed(q, id, seed) + } + + /// Returns a private key generated pseudorandomly from a seed + /// according to Appendix A of + pub fn new_from_seed(q: u32, id: Identifier, seed: impl AsRef<[u8]>) -> Self { + let seed = seed.as_ref(); + let x = GenericArray::generate(|i| { + Mode::Hasher::new() + .chain_update(id) + .chain_update(q.to_be_bytes()) + .chain_update((i as u16).to_be_bytes()) + .chain_update([0xff]) + .chain_update(seed) + .finalize() + }); + + Self { + q, + id, + x, + valid: true, + } + } + + /// this implements algorithm 1 from + pub fn public(&self) -> PublicKey { + let mut hasher = Mode::Hasher::new() + .chain_update(self.id) + .chain_update(self.q.to_be_bytes()) + .chain_update(D_PBLC); + + let mut tmp = Output::::default(); + for i in 0..Mode::P { + //let mut tmp = self.x[i].clone(); + tmp.clone_from(&self.x[i]); + for j in 0..((1u32 << Mode::W) - 1) { + Mode::Hasher::new() + .chain_update(self.id) + .chain_update(self.q.to_be_bytes()) + .chain_update((i as u16).to_be_bytes()) + .chain_update((j as u8).to_be_bytes()) + .chain_update(&tmp) + .finalize_into(&mut tmp); + } + hasher.update(&tmp); + } + + PublicKey { + id: self.id, + q: self.q, + k: hasher.finalize(), + } + } + + /// `true` if the private key can be used for signing operations + /// + /// `false` if it has already been used + pub fn is_valid(&self) -> bool { + self.valid + } +} + +impl RandomizedSignerMut> for PrivateKey { + fn try_sign_with_rng( + &mut self, + rng: &mut impl CryptoRngCore, + msg: &[u8], + ) -> Result, Error> { + if !self.valid { + return Err(Error::from_source(anyhow!("private key no longer valid"))); + } + + // Generate the message randomizer C + let mut c = >::default(); + rng.fill_bytes(&mut c); + + // Q is the randomized message hash + let q = Mode::Hasher::new() + .chain_update(self.id) + .chain_update(self.q.to_be_bytes()) + .chain_update(D_MESG) + .chain_update(&c) + .chain_update(msg) + .finalize(); + + // Y is the signature. We iterate over the message hash and checksum expanded into Winternitz coefficients + let y = Mode::expand(&q).into_iter().enumerate().map(|(i, a)| { + let a = a as u32; + let mut tmp = self.x[i].clone(); + for j in 0..a { + Mode::Hasher::new() + .chain_update(self.id) + .chain_update(self.q.to_be_bytes()) + .chain_update((i as u16).to_be_bytes()) + .chain_update((j as u8).to_be_bytes()) + .chain_update(&tmp) + .finalize_into(&mut tmp); + } + tmp + }); + let y = GenericArray::from_iter(y); + + let sig = Signature { c, y }; + + // zero out fields so we can't use the private key a second time + self.q.zeroize(); + self.id.zeroize(); + self.x.zeroize(); + self.valid = false; + + Ok(sig) + } +} diff --git a/lms/lms/ots/public.rs b/lms/lms/ots/public.rs new file mode 100644 index 00000000..6c79cd5c --- /dev/null +++ b/lms/lms/ots/public.rs @@ -0,0 +1,150 @@ +//! Contains the [PublicKey] type + +use crate::constants::ID_LEN; +use crate::error::LmsDeserializeError; +use crate::ots::modes::LmsOtsMode; +use crate::ots::signature::Signature; + +use crate::types::Identifier; +use digest::{Output, OutputSizeUser}; + +use generic_array::{ArrayLength, GenericArray}; +use signature::{Error, Verifier}; +use std::cmp::Ordering; +use std::ops::Add; +use typenum::{Sum, U2, U24}; + +#[derive(Debug)] +/// Opaque struct representing a LM-OTS public key +pub struct PublicKey { + pub(crate) q: u32, + pub(crate) id: Identifier, + pub(crate) k: Output, +} + +// manual Clone impl because Mode is not Clone +impl Clone for PublicKey { + fn clone(&self) -> Self { + Self { + q: self.q, + id: self.id, + k: self.k.clone(), + } + } +} + +// manual PartialEq impl because Mode is not PartialEq +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + self.q == other.q && self.id == other.id && self.k == other.k + } +} + +impl Verifier> for PublicKey +where + // required to concat Q and cksm(Q) + ::OutputSize: Add, + Sum<::OutputSize, U2>: ArrayLength, +{ + // this implements algorithm 4a of https://datatracker.ietf.org/doc/html/rfc8554#section-4.6 + fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), Error> { + // If the public key is not at least four bytes long, return INVALID. + // We are calling this method on a valid public key so there's no worry here. + let kc = signature.recover_pubkey(self.id, self.q, msg); + // 4. If Kc is equal to K, return VALID; otherwise, return INVALID. + if self.k == kc.k { + Ok(()) + } else { + Err(Error::new()) + } + } +} + +/// Converts a [PublicKey] into its byte representation +impl From> + for GenericArray::OutputSize, U24>> +where + ::OutputSize: Add, + Sum<::OutputSize, U24>: ArrayLength, +{ + fn from(pk: PublicKey) -> Self { + // Return u32str(type) || I || u32str(q) || K + GenericArray::from_exact_iter( + std::iter::empty() + .chain(Mode::TYPECODE.to_be_bytes()) + .chain(pk.id) + .chain(pk.q.to_be_bytes()) + .chain(pk.k), + ) + .expect("ok") + } +} + +/// Tries to parse a [PublicKey] from an exact slice +impl<'a, Mode: LmsOtsMode> TryFrom<&'a [u8]> for PublicKey { + type Error = LmsDeserializeError; + + fn try_from(pk: &'a [u8]) -> Result { + if pk.len() < 4 { + return Err(LmsDeserializeError::NoAlgorithm); + } + + let (alg, pk) = pk.split_at(4); + let expected = Mode::N + ID_LEN + 4; + + // will never panic because alg is a 4 byte slice + if u32::from_be_bytes(alg.try_into().unwrap()) != Mode::TYPECODE { + return Err(LmsDeserializeError::WrongAlgorithm); + } + + match pk.len().cmp(&expected) { + Ordering::Less => Err(LmsDeserializeError::TooShort), + Ordering::Greater => Err(LmsDeserializeError::TooLong), + Ordering::Equal => { + // pk is now guaranteed to be of the form I || q || K + let (i, qk) = pk.split_at(ID_LEN); + let (q, k) = qk.split_at(4); + + Ok(Self { + q: u32::from_be_bytes(q.try_into().expect("ok")), + id: i.try_into().expect("ok"), + k: GenericArray::clone_from_slice(k), + }) + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::constants::ID_LEN; + use crate::error::LmsDeserializeError; + use crate::ots::modes::{LmsOtsSha256N32W4, LmsOtsSha256N32W8}; + use crate::ots::private::PrivateKey; + use crate::ots::public::PublicKey; + use generic_array::GenericArray; + use rand::thread_rng; + + #[test] + fn test_serde() { + let pk = + PrivateKey::::new(0, [0xbb; ID_LEN], &mut thread_rng()).public(); + let pk_serialized: GenericArray = pk.clone().into(); + let bytes = pk_serialized.as_slice(); + let pk_deserialized = PublicKey::::try_from(bytes); + + assert!(pk_deserialized.is_ok()); + let pk_deserialized = pk_deserialized.unwrap(); + assert_eq!(pk, pk_deserialized); + + let pk_wrongalgo = PublicKey::::try_from(bytes); + let pk_short = PublicKey::::try_from(&bytes[0..(bytes.len() - 1)]); + let mut long_bytes = pk_serialized.into_iter().collect::>(); + long_bytes.push(0); + let pk_long = PublicKey::::try_from(long_bytes.as_slice()); + + assert_eq!(pk_wrongalgo, Err(LmsDeserializeError::WrongAlgorithm)); + assert_eq!(pk_short, Err(LmsDeserializeError::TooShort)); + assert_eq!(pk_long, Err(LmsDeserializeError::TooLong)); + } +} diff --git a/lms/lms/ots/signature.rs b/lms/lms/ots/signature.rs new file mode 100644 index 00000000..cac479d9 --- /dev/null +++ b/lms/lms/ots/signature.rs @@ -0,0 +1,439 @@ +//! Contains the [Signature] type + +use crate::constants::{D_MESG, D_PBLC}; +use crate::error::LmsDeserializeError; +use crate::ots::modes::LmsOtsMode; +use crate::types::Identifier; +use digest::Digest; +use generic_array::{ArrayLength, GenericArray}; +use signature::SignatureEncoding; +use std::cmp::Ordering; +use std::ops::{Add, Mul}; +use typenum::{Prod, Sum, U1, U4}; + +use super::PublicKey; + +#[derive(Debug, Eq)] +/// Opaque struct representing a LM-OTS signature +pub struct Signature { + pub(crate) c: digest::Output, + pub(crate) y: GenericArray, Mode::PLen>, +} + +// manual implementation is required to not require bounds on Mode +impl Clone for Signature { + fn clone(&self) -> Self { + Self { + c: self.c.clone(), + y: self.y.clone(), + } + } +} + +// manual implementation is required to not require bounds on Mode +impl PartialEq for Signature { + fn eq(&self, other: &Self) -> bool { + self.c == other.c && self.y == other.y + } +} + +/// Useful type alias to get the [GenericArray] representation +pub type Output = GenericArray< + u8, + Sum::NLen, Sum<::PLen, U1>>, U4>, +>; + +/// Converts a [Signature] into its byte representation +impl From> for Output +where + // required for Output + Mode::PLen: Add, + Mode::NLen: Mul>, + Prod>: Add, + Sum>, U4>: ArrayLength, +{ + fn from(sig: Signature) -> Self { + GenericArray::from_exact_iter( + std::iter::empty() + .chain(Mode::TYPECODE.to_be_bytes()) + .chain(sig.c.clone()) + .chain(sig.y.iter().flatten().cloned()), + ) + .expect("ok") + } +} + +/// Tries to parse a [Signature] from an exact slice +impl<'a, Mode: LmsOtsMode> TryFrom<&'a [u8]> for Signature { + type Error = LmsDeserializeError; + + fn try_from(sig: &'a [u8]) -> Result { + if sig.len() < 4 { + return Err(LmsDeserializeError::NoAlgorithm); + } + + let (alg, sig) = sig.split_at(4); + + // will never panic because alg is a 4 byte slice + if u32::from_be_bytes(alg.try_into().unwrap()) != Mode::TYPECODE { + return Err(LmsDeserializeError::WrongAlgorithm); + } + + let expected = Mode::N * (Mode::P + 1); + + match sig.len().cmp(&expected) { + Ordering::Less => Err(LmsDeserializeError::TooShort), + Ordering::Greater => Err(LmsDeserializeError::TooLong), + Ordering::Equal => { + // sig is now guaranteed to be of the form C || y[0] || ... || y[p - 1] + let (c, y) = sig.split_at(Mode::N); + + let c = GenericArray::clone_from_slice(c); + + // ys is an iterator over GenericArray of length p + let ys = y.chunks_exact(Mode::N).map(GenericArray::clone_from_slice); + debug_assert!(ys.len() == Mode::P); + let y = GenericArray::from_iter(ys); + + Ok(Self { c, y }) + } + } + } +} + +impl Signature { + /// Returns a public key candidate for this signature as defined by + /// algorithm 4b of the LMS RFC. The signature will always be valid for + /// the returned public key candidate. + pub fn recover_pubkey(&self, id: Identifier, q: u32, msg: &[u8]) -> PublicKey { + // algorithm 4b + + // Q = H(I || u32str(q) || u16str(D_MESG) || C || message) + let msg_hash = Mode::Hasher::new() + .chain_update(id) + .chain_update(q.to_be_bytes()) + .chain_update(D_MESG) + .chain_update(&self.c) + .chain_update(msg) + .finalize(); + + // first part of + // Kc = H(I || u32str(q) || u16str(D_PBLC) || z[0] || z[1] || ... || z[p-1]) + let mut hasher = Mode::Hasher::new() + .chain_update(id) + .chain_update(q.to_be_bytes()) + .chain_update(D_PBLC); + + let mut tmp = GenericArray::default(); + for (i, a) in Mode::expand(&msg_hash).into_iter().enumerate() { + tmp.clone_from(&self.y[i]); + + // for ( j = a; j < 2^w - 1; j = j + 1 ) + for j in a..((1usize << Mode::W) - 1) as u8 { + // tmp = H(I || u32str(q) || u16str(i) || u8str(j) || tmp) + Mode::Hasher::new() + .chain_update(id) + .chain_update(q.to_be_bytes()) + .chain_update((i as u16).to_be_bytes()) + .chain_update(j.to_be_bytes()) + .chain_update(&tmp) + .finalize_into(&mut tmp); + } + // z[i] = tmp + // except we directly absorb into the hash function to finish off the computation of + // Kc = H(I || u32str(q) || u16str(D_PBLC) || z[0] || z[1] || ... || z[p-1]) + hasher.update(&tmp); + } + PublicKey { + id, + q, + k: hasher.finalize(), + } + } +} + +impl SignatureEncoding for Signature +where + // required for Output + Mode::PLen: Add, + Mode::NLen: Mul>, + Prod>: Add, + Sum>, U4>: ArrayLength, +{ + type Repr = Output; +} + +#[cfg(test)] +mod tests { + use crate::ots::modes::{LmsOtsSha256N32W4, LmsOtsSha256N32W8}; + use crate::ots::signature::{LmsDeserializeError, Signature}; + + #[test] + fn test_deserialize_sha256_n32_w8() { + let sig_str = "00000004\ + 0703c491e7558b35011ece3592eaa5da4d918786771233e8353bc4f62323185c\ + 95cae05b899e35dffd717054706209988ebfdf6e37960bb5c38d7657e8bffeef\ + 9bc042da4b4525650485c66d0ce19b317587c6ba4bffcc428e25d08931e72dfb\ + 6a120c5612344258b85efdb7db1db9e1865a73caf96557eb39ed3e3f426933ac\ + 9eeddb03a1d2374af7bf77185577456237f9de2d60113c23f846df26fa942008\ + a698994c0827d90e86d43e0df7f4bfcdb09b86a373b98288b7094ad81a0185ac\ + 100e4f2c5fc38c003c1ab6fea479eb2f5ebe48f584d7159b8ada03586e65ad9c\ + 969f6aecbfe44cf356888a7b15a3ff074f771760b26f9c04884ee1faa329fbf4\ + e61af23aee7fa5d4d9a5dfcf43c4c26ce8aea2ce8a2990d7ba7b57108b47dabf\ + beadb2b25b3cacc1ac0cef346cbb90fb044beee4fac2603a442bdf7e507243b7\ + 319c9944b1586e899d431c7f91bcccc8690dbf59b28386b2315f3d36ef2eaa3c\ + f30b2b51f48b71b003dfb08249484201043f65f5a3ef6bbd61ddfee81aca9ce6\ + 0081262a00000480dcbc9a3da6fbef5c1c0a55e48a0e729f9184fcb1407c3152\ + 9db268f6fe50032a363c9801306837fafabdf957fd97eafc80dbd165e435d0e2\ + dfd836a28b354023924b6fb7e48bc0b3ed95eea64c2d402f4d734c8dc26f3ac5\ + 91825daef01eae3c38e3328d00a77dc657034f287ccb0f0e1c9a7cbdc828f627\ + 205e4737b84b58376551d44c12c3c215c812a0970789c83de51d6ad787271963\ + 327f0a5fbb6b5907dec02c9a90934af5a1c63b72c82653605d1dcce51596b3c2\ + b45696689f2eb382007497557692caac4d57b5de9f5569bc2ad0137fd47fb47e\ + 664fcb6db4971f5b3e07aceda9ac130e9f38182de994cff192ec0e82fd6d4cb7\ + f3fe00812589b7a7ce515440456433016b84a59bec6619a1c6c0b37dd1450ed4\ + f2d8b584410ceda8025f5d2d8dd0d2176fc1cf2cc06fa8c82bed4d944e71339e\ + ce780fd025bd41ec34ebff9d4270a3224e019fcb444474d482fd2dbe75efb203\ + 89cc10cd600abb54c47ede93e08c114edb04117d714dc1d525e11bed8756192f\ + 929d15462b939ff3f52f2252da2ed64d8fae88818b1efa2c7b08c8794fb1b214\ + aa233db3162833141ea4383f1a6f120be1db82ce3630b3429114463157a64e91\ + 234d475e2f79cbf05e4db6a9407d72c6bff7d1198b5c4d6aad2831db61274993\ + 715a0182c7dc8089e32c8531deed4f7431c07c02195eba2ef91efb5613c37af7\ + ae0c066babc69369700e1dd26eddc0d216c781d56e4ce47e3303fa73007ff7b9\ + 49ef23be2aa4dbf25206fe45c20dd888395b2526391a724996a44156beac8082\ + 12858792bf8e74cba49dee5e8812e019da87454bff9e847ed83db07af3137430\ + 82f880a278f682c2bd0ad6887cb59f652e155987d61bbf6a88d36ee93b6072e6\ + 656d9ccbaae3d655852e38deb3a2dcf8058dc9fb6f2ab3d3b3539eb77b248a66\ + 1091d05eb6e2f297774fe6053598457cc61908318de4b826f0fc86d4bb117d33\ + e865aa805009cc2918d9c2f840c4da43a703ad9f5b5806163d7161696b5a0adc"; + let sig_bytes = hex::decode(sig_str).unwrap(); + let mut long_bytes = sig_bytes.clone(); + long_bytes.push(0); + + let sig = Signature::::try_from(&sig_bytes[..]); + let sig_wrongalgo = Signature::::try_from(&sig_bytes[..]); + let sig_short = Signature::::try_from(&sig_bytes[..sig_bytes.len() - 1]); + let sig_long = Signature::::try_from(&long_bytes[..]); + + assert!(sig.is_ok()); + assert_eq!(sig_wrongalgo, Err(LmsDeserializeError::WrongAlgorithm)); + assert_eq!(sig_short, Err(LmsDeserializeError::TooShort)); + assert_eq!(sig_long, Err(LmsDeserializeError::TooLong)); + + let ec: [u8; 32] = [ + 0x07, 0x03, 0xc4, 0x91, 0xe7, 0x55, 0x8b, 0x35, 0x01, 0x1e, 0xce, 0x35, 0x92, 0xea, + 0xa5, 0xda, 0x4d, 0x91, 0x87, 0x86, 0x77, 0x12, 0x33, 0xe8, 0x35, 0x3b, 0xc4, 0xf6, + 0x23, 0x23, 0x18, 0x5c, + ]; + let ey: [[u8; 32]; 34] = [ + [ + 0x95, 0xca, 0xe0, 0x5b, 0x89, 0x9e, 0x35, 0xdf, 0xfd, 0x71, 0x70, 0x54, 0x70, 0x62, + 0x09, 0x98, 0x8e, 0xbf, 0xdf, 0x6e, 0x37, 0x96, 0x0b, 0xb5, 0xc3, 0x8d, 0x76, 0x57, + 0xe8, 0xbf, 0xfe, 0xef, + ], + [ + 0x9b, 0xc0, 0x42, 0xda, 0x4b, 0x45, 0x25, 0x65, 0x04, 0x85, 0xc6, 0x6d, 0x0c, 0xe1, + 0x9b, 0x31, 0x75, 0x87, 0xc6, 0xba, 0x4b, 0xff, 0xcc, 0x42, 0x8e, 0x25, 0xd0, 0x89, + 0x31, 0xe7, 0x2d, 0xfb, + ], + [ + 0x6a, 0x12, 0x0c, 0x56, 0x12, 0x34, 0x42, 0x58, 0xb8, 0x5e, 0xfd, 0xb7, 0xdb, 0x1d, + 0xb9, 0xe1, 0x86, 0x5a, 0x73, 0xca, 0xf9, 0x65, 0x57, 0xeb, 0x39, 0xed, 0x3e, 0x3f, + 0x42, 0x69, 0x33, 0xac, + ], + [ + 0x9e, 0xed, 0xdb, 0x03, 0xa1, 0xd2, 0x37, 0x4a, 0xf7, 0xbf, 0x77, 0x18, 0x55, 0x77, + 0x45, 0x62, 0x37, 0xf9, 0xde, 0x2d, 0x60, 0x11, 0x3c, 0x23, 0xf8, 0x46, 0xdf, 0x26, + 0xfa, 0x94, 0x20, 0x08, + ], + [ + 0xa6, 0x98, 0x99, 0x4c, 0x08, 0x27, 0xd9, 0x0e, 0x86, 0xd4, 0x3e, 0x0d, 0xf7, 0xf4, + 0xbf, 0xcd, 0xb0, 0x9b, 0x86, 0xa3, 0x73, 0xb9, 0x82, 0x88, 0xb7, 0x09, 0x4a, 0xd8, + 0x1a, 0x01, 0x85, 0xac, + ], + [ + 0x10, 0x0e, 0x4f, 0x2c, 0x5f, 0xc3, 0x8c, 0x00, 0x3c, 0x1a, 0xb6, 0xfe, 0xa4, 0x79, + 0xeb, 0x2f, 0x5e, 0xbe, 0x48, 0xf5, 0x84, 0xd7, 0x15, 0x9b, 0x8a, 0xda, 0x03, 0x58, + 0x6e, 0x65, 0xad, 0x9c, + ], + [ + 0x96, 0x9f, 0x6a, 0xec, 0xbf, 0xe4, 0x4c, 0xf3, 0x56, 0x88, 0x8a, 0x7b, 0x15, 0xa3, + 0xff, 0x07, 0x4f, 0x77, 0x17, 0x60, 0xb2, 0x6f, 0x9c, 0x04, 0x88, 0x4e, 0xe1, 0xfa, + 0xa3, 0x29, 0xfb, 0xf4, + ], + [ + 0xe6, 0x1a, 0xf2, 0x3a, 0xee, 0x7f, 0xa5, 0xd4, 0xd9, 0xa5, 0xdf, 0xcf, 0x43, 0xc4, + 0xc2, 0x6c, 0xe8, 0xae, 0xa2, 0xce, 0x8a, 0x29, 0x90, 0xd7, 0xba, 0x7b, 0x57, 0x10, + 0x8b, 0x47, 0xda, 0xbf, + ], + [ + 0xbe, 0xad, 0xb2, 0xb2, 0x5b, 0x3c, 0xac, 0xc1, 0xac, 0x0c, 0xef, 0x34, 0x6c, 0xbb, + 0x90, 0xfb, 0x04, 0x4b, 0xee, 0xe4, 0xfa, 0xc2, 0x60, 0x3a, 0x44, 0x2b, 0xdf, 0x7e, + 0x50, 0x72, 0x43, 0xb7, + ], + [ + 0x31, 0x9c, 0x99, 0x44, 0xb1, 0x58, 0x6e, 0x89, 0x9d, 0x43, 0x1c, 0x7f, 0x91, 0xbc, + 0xcc, 0xc8, 0x69, 0x0d, 0xbf, 0x59, 0xb2, 0x83, 0x86, 0xb2, 0x31, 0x5f, 0x3d, 0x36, + 0xef, 0x2e, 0xaa, 0x3c, + ], + [ + 0xf3, 0x0b, 0x2b, 0x51, 0xf4, 0x8b, 0x71, 0xb0, 0x03, 0xdf, 0xb0, 0x82, 0x49, 0x48, + 0x42, 0x01, 0x04, 0x3f, 0x65, 0xf5, 0xa3, 0xef, 0x6b, 0xbd, 0x61, 0xdd, 0xfe, 0xe8, + 0x1a, 0xca, 0x9c, 0xe6, + ], + [ + 0x00, 0x81, 0x26, 0x2a, 0x00, 0x00, 0x04, 0x80, 0xdc, 0xbc, 0x9a, 0x3d, 0xa6, 0xfb, + 0xef, 0x5c, 0x1c, 0x0a, 0x55, 0xe4, 0x8a, 0x0e, 0x72, 0x9f, 0x91, 0x84, 0xfc, 0xb1, + 0x40, 0x7c, 0x31, 0x52, + ], + [ + 0x9d, 0xb2, 0x68, 0xf6, 0xfe, 0x50, 0x03, 0x2a, 0x36, 0x3c, 0x98, 0x01, 0x30, 0x68, + 0x37, 0xfa, 0xfa, 0xbd, 0xf9, 0x57, 0xfd, 0x97, 0xea, 0xfc, 0x80, 0xdb, 0xd1, 0x65, + 0xe4, 0x35, 0xd0, 0xe2, + ], + [ + 0xdf, 0xd8, 0x36, 0xa2, 0x8b, 0x35, 0x40, 0x23, 0x92, 0x4b, 0x6f, 0xb7, 0xe4, 0x8b, + 0xc0, 0xb3, 0xed, 0x95, 0xee, 0xa6, 0x4c, 0x2d, 0x40, 0x2f, 0x4d, 0x73, 0x4c, 0x8d, + 0xc2, 0x6f, 0x3a, 0xc5, + ], + [ + 0x91, 0x82, 0x5d, 0xae, 0xf0, 0x1e, 0xae, 0x3c, 0x38, 0xe3, 0x32, 0x8d, 0x00, 0xa7, + 0x7d, 0xc6, 0x57, 0x03, 0x4f, 0x28, 0x7c, 0xcb, 0x0f, 0x0e, 0x1c, 0x9a, 0x7c, 0xbd, + 0xc8, 0x28, 0xf6, 0x27, + ], + [ + 0x20, 0x5e, 0x47, 0x37, 0xb8, 0x4b, 0x58, 0x37, 0x65, 0x51, 0xd4, 0x4c, 0x12, 0xc3, + 0xc2, 0x15, 0xc8, 0x12, 0xa0, 0x97, 0x07, 0x89, 0xc8, 0x3d, 0xe5, 0x1d, 0x6a, 0xd7, + 0x87, 0x27, 0x19, 0x63, + ], + [ + 0x32, 0x7f, 0x0a, 0x5f, 0xbb, 0x6b, 0x59, 0x07, 0xde, 0xc0, 0x2c, 0x9a, 0x90, 0x93, + 0x4a, 0xf5, 0xa1, 0xc6, 0x3b, 0x72, 0xc8, 0x26, 0x53, 0x60, 0x5d, 0x1d, 0xcc, 0xe5, + 0x15, 0x96, 0xb3, 0xc2, + ], + [ + 0xb4, 0x56, 0x96, 0x68, 0x9f, 0x2e, 0xb3, 0x82, 0x00, 0x74, 0x97, 0x55, 0x76, 0x92, + 0xca, 0xac, 0x4d, 0x57, 0xb5, 0xde, 0x9f, 0x55, 0x69, 0xbc, 0x2a, 0xd0, 0x13, 0x7f, + 0xd4, 0x7f, 0xb4, 0x7e, + ], + [ + 0x66, 0x4f, 0xcb, 0x6d, 0xb4, 0x97, 0x1f, 0x5b, 0x3e, 0x07, 0xac, 0xed, 0xa9, 0xac, + 0x13, 0x0e, 0x9f, 0x38, 0x18, 0x2d, 0xe9, 0x94, 0xcf, 0xf1, 0x92, 0xec, 0x0e, 0x82, + 0xfd, 0x6d, 0x4c, 0xb7, + ], + [ + 0xf3, 0xfe, 0x00, 0x81, 0x25, 0x89, 0xb7, 0xa7, 0xce, 0x51, 0x54, 0x40, 0x45, 0x64, + 0x33, 0x01, 0x6b, 0x84, 0xa5, 0x9b, 0xec, 0x66, 0x19, 0xa1, 0xc6, 0xc0, 0xb3, 0x7d, + 0xd1, 0x45, 0x0e, 0xd4, + ], + [ + 0xf2, 0xd8, 0xb5, 0x84, 0x41, 0x0c, 0xed, 0xa8, 0x02, 0x5f, 0x5d, 0x2d, 0x8d, 0xd0, + 0xd2, 0x17, 0x6f, 0xc1, 0xcf, 0x2c, 0xc0, 0x6f, 0xa8, 0xc8, 0x2b, 0xed, 0x4d, 0x94, + 0x4e, 0x71, 0x33, 0x9e, + ], + [ + 0xce, 0x78, 0x0f, 0xd0, 0x25, 0xbd, 0x41, 0xec, 0x34, 0xeb, 0xff, 0x9d, 0x42, 0x70, + 0xa3, 0x22, 0x4e, 0x01, 0x9f, 0xcb, 0x44, 0x44, 0x74, 0xd4, 0x82, 0xfd, 0x2d, 0xbe, + 0x75, 0xef, 0xb2, 0x03, + ], + [ + 0x89, 0xcc, 0x10, 0xcd, 0x60, 0x0a, 0xbb, 0x54, 0xc4, 0x7e, 0xde, 0x93, 0xe0, 0x8c, + 0x11, 0x4e, 0xdb, 0x04, 0x11, 0x7d, 0x71, 0x4d, 0xc1, 0xd5, 0x25, 0xe1, 0x1b, 0xed, + 0x87, 0x56, 0x19, 0x2f, + ], + [ + 0x92, 0x9d, 0x15, 0x46, 0x2b, 0x93, 0x9f, 0xf3, 0xf5, 0x2f, 0x22, 0x52, 0xda, 0x2e, + 0xd6, 0x4d, 0x8f, 0xae, 0x88, 0x81, 0x8b, 0x1e, 0xfa, 0x2c, 0x7b, 0x08, 0xc8, 0x79, + 0x4f, 0xb1, 0xb2, 0x14, + ], + [ + 0xaa, 0x23, 0x3d, 0xb3, 0x16, 0x28, 0x33, 0x14, 0x1e, 0xa4, 0x38, 0x3f, 0x1a, 0x6f, + 0x12, 0x0b, 0xe1, 0xdb, 0x82, 0xce, 0x36, 0x30, 0xb3, 0x42, 0x91, 0x14, 0x46, 0x31, + 0x57, 0xa6, 0x4e, 0x91, + ], + [ + 0x23, 0x4d, 0x47, 0x5e, 0x2f, 0x79, 0xcb, 0xf0, 0x5e, 0x4d, 0xb6, 0xa9, 0x40, 0x7d, + 0x72, 0xc6, 0xbf, 0xf7, 0xd1, 0x19, 0x8b, 0x5c, 0x4d, 0x6a, 0xad, 0x28, 0x31, 0xdb, + 0x61, 0x27, 0x49, 0x93, + ], + [ + 0x71, 0x5a, 0x01, 0x82, 0xc7, 0xdc, 0x80, 0x89, 0xe3, 0x2c, 0x85, 0x31, 0xde, 0xed, + 0x4f, 0x74, 0x31, 0xc0, 0x7c, 0x02, 0x19, 0x5e, 0xba, 0x2e, 0xf9, 0x1e, 0xfb, 0x56, + 0x13, 0xc3, 0x7a, 0xf7, + ], + [ + 0xae, 0x0c, 0x06, 0x6b, 0xab, 0xc6, 0x93, 0x69, 0x70, 0x0e, 0x1d, 0xd2, 0x6e, 0xdd, + 0xc0, 0xd2, 0x16, 0xc7, 0x81, 0xd5, 0x6e, 0x4c, 0xe4, 0x7e, 0x33, 0x03, 0xfa, 0x73, + 0x00, 0x7f, 0xf7, 0xb9, + ], + [ + 0x49, 0xef, 0x23, 0xbe, 0x2a, 0xa4, 0xdb, 0xf2, 0x52, 0x06, 0xfe, 0x45, 0xc2, 0x0d, + 0xd8, 0x88, 0x39, 0x5b, 0x25, 0x26, 0x39, 0x1a, 0x72, 0x49, 0x96, 0xa4, 0x41, 0x56, + 0xbe, 0xac, 0x80, 0x82, + ], + [ + 0x12, 0x85, 0x87, 0x92, 0xbf, 0x8e, 0x74, 0xcb, 0xa4, 0x9d, 0xee, 0x5e, 0x88, 0x12, + 0xe0, 0x19, 0xda, 0x87, 0x45, 0x4b, 0xff, 0x9e, 0x84, 0x7e, 0xd8, 0x3d, 0xb0, 0x7a, + 0xf3, 0x13, 0x74, 0x30, + ], + [ + 0x82, 0xf8, 0x80, 0xa2, 0x78, 0xf6, 0x82, 0xc2, 0xbd, 0x0a, 0xd6, 0x88, 0x7c, 0xb5, + 0x9f, 0x65, 0x2e, 0x15, 0x59, 0x87, 0xd6, 0x1b, 0xbf, 0x6a, 0x88, 0xd3, 0x6e, 0xe9, + 0x3b, 0x60, 0x72, 0xe6, + ], + [ + 0x65, 0x6d, 0x9c, 0xcb, 0xaa, 0xe3, 0xd6, 0x55, 0x85, 0x2e, 0x38, 0xde, 0xb3, 0xa2, + 0xdc, 0xf8, 0x05, 0x8d, 0xc9, 0xfb, 0x6f, 0x2a, 0xb3, 0xd3, 0xb3, 0x53, 0x9e, 0xb7, + 0x7b, 0x24, 0x8a, 0x66, + ], + [ + 0x10, 0x91, 0xd0, 0x5e, 0xb6, 0xe2, 0xf2, 0x97, 0x77, 0x4f, 0xe6, 0x05, 0x35, 0x98, + 0x45, 0x7c, 0xc6, 0x19, 0x08, 0x31, 0x8d, 0xe4, 0xb8, 0x26, 0xf0, 0xfc, 0x86, 0xd4, + 0xbb, 0x11, 0x7d, 0x33, + ], + [ + 0xe8, 0x65, 0xaa, 0x80, 0x50, 0x09, 0xcc, 0x29, 0x18, 0xd9, 0xc2, 0xf8, 0x40, 0xc4, + 0xda, 0x43, 0xa7, 0x03, 0xad, 0x9f, 0x5b, 0x58, 0x06, 0x16, 0x3d, 0x71, 0x61, 0x69, + 0x6b, 0x5a, 0x0a, 0xdc, + ], + ]; + + // test value decoding + let sig = sig.unwrap(); + assert_eq!(sig.c.as_slice(), &ec); + assert_eq!(sig.y[0].as_slice(), &ey[0]); + assert_eq!(sig.y[1].as_slice(), &ey[1]); + assert_eq!(sig.y[2].as_slice(), &ey[2]); + assert_eq!(sig.y[3].as_slice(), &ey[3]); + assert_eq!(sig.y[4].as_slice(), &ey[4]); + assert_eq!(sig.y[5].as_slice(), &ey[5]); + assert_eq!(sig.y[6].as_slice(), &ey[6]); + assert_eq!(sig.y[7].as_slice(), &ey[7]); + assert_eq!(sig.y[8].as_slice(), &ey[8]); + assert_eq!(sig.y[9].as_slice(), &ey[9]); + assert_eq!(sig.y[10].as_slice(), &ey[10]); + assert_eq!(sig.y[11].as_slice(), &ey[11]); + assert_eq!(sig.y[12].as_slice(), &ey[12]); + assert_eq!(sig.y[13].as_slice(), &ey[13]); + assert_eq!(sig.y[14].as_slice(), &ey[14]); + assert_eq!(sig.y[15].as_slice(), &ey[15]); + assert_eq!(sig.y[16].as_slice(), &ey[16]); + assert_eq!(sig.y[17].as_slice(), &ey[17]); + assert_eq!(sig.y[18].as_slice(), &ey[18]); + assert_eq!(sig.y[19].as_slice(), &ey[19]); + assert_eq!(sig.y[20].as_slice(), &ey[20]); + assert_eq!(sig.y[21].as_slice(), &ey[21]); + assert_eq!(sig.y[22].as_slice(), &ey[22]); + assert_eq!(sig.y[23].as_slice(), &ey[23]); + assert_eq!(sig.y[24].as_slice(), &ey[24]); + assert_eq!(sig.y[25].as_slice(), &ey[25]); + assert_eq!(sig.y[26].as_slice(), &ey[26]); + assert_eq!(sig.y[27].as_slice(), &ey[27]); + assert_eq!(sig.y[28].as_slice(), &ey[28]); + assert_eq!(sig.y[29].as_slice(), &ey[29]); + assert_eq!(sig.y[30].as_slice(), &ey[30]); + assert_eq!(sig.y[31].as_slice(), &ey[31]); + assert_eq!(sig.y[32].as_slice(), &ey[32]); + assert_eq!(sig.y[33].as_slice(), &ey[33]); + } +} diff --git a/lms/lms/ots/util.rs b/lms/lms/ots/util.rs new file mode 100644 index 00000000..a0552d0a --- /dev/null +++ b/lms/lms/ots/util.rs @@ -0,0 +1,56 @@ +use std::iter::IntoIterator; + +/// Returns an iterator over the w-bit Winternitz coefficients of the inout bytes +/// Implements the Coef function from section 3.1.3 of RFC8554 +/// https://datatracker.ietf.org/doc/html/rfc8554#section-3.1.3 +pub(crate) fn coefs<'a>( + bytes: impl IntoIterator, + w: usize, +) -> impl Iterator { + let mask: u8 = match w { + 1 => 0x01, + 2 => 0x03, + 4 => 0x0f, + 8 => 0xff, + _ => panic!("invalid bit width: {}", w), + }; + + let entries_per_byte: usize = 8 / w; + bytes + .into_iter() + .cloned() + .flat_map(move |byte| (0..entries_per_byte).map(move |i| (byte >> (8 - w - i * w)) & mask)) +} + +#[cfg(test)] +mod tests { + use crate::ots::util::coefs; + + #[test] + fn coef_test_w1() { + let s = [0x12, 0x34]; + let cs = coefs(&s, 1).collect::>(); + assert_eq!(cs, vec![0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0]); + } + + #[test] + fn coef_test_w2() { + let s = [0x12, 0x34]; + let cs: Vec = coefs(&s, 2).collect::>(); + assert_eq!(cs, vec![0, 1, 0, 2, 0, 3, 1, 0]); + } + + #[test] + fn coef_test_w4() { + let s = [0x12, 0x34]; + let cs: Vec = coefs(&s, 4).collect::>(); + assert_eq!(cs, vec![1, 2, 3, 4]); + } + + #[test] + fn coef_test_w8() { + let s = [0x12, 0x34]; + let cs: Vec = coefs(&s, 8).collect::>(); + assert_eq!(cs, vec![0x12, 0x34]); + } +} diff --git a/lms/lms/types.rs b/lms/lms/types.rs new file mode 100644 index 00000000..acba22b3 --- /dev/null +++ b/lms/lms/types.rs @@ -0,0 +1,13 @@ +//! All types related to LMS + +use crate::constants::ID_LEN; + +/// Anything that has a corresponding `lmots_algorithm_type` or +/// `lms_algorithm_type` will implement this trait. +pub trait Typecode { + /// The associated enum value for the algorithm type. + const TYPECODE: u32; +} + +/// The 16 byte identifier I from the LM-OTS algorithm. +pub type Identifier = [u8; ID_LEN]; From 0b497dc45e9178df0ca2a73073356db686a82c4c Mon Sep 17 00:00:00 2001 From: Will Song Date: Mon, 5 Feb 2024 23:11:52 -0500 Subject: [PATCH 2/5] code review from tony --- .github/workflows/lms.yml | 2 +- lms/.github/workflows/lms-rust.yml | 52 ------------------------------ lms/Cargo.toml | 9 ++++-- lms/{lms => src}/constants.rs | 0 lms/{lms => src}/error.rs | 0 lms/{lms => src}/lib.rs | 0 lms/src/lms/error.rs | 21 ++++++++++++ lms/{lms => src}/lms/keypair.rs | 8 ++--- lms/{lms => src}/lms/mod.rs | 9 +++--- lms/{lms => src}/lms/modes.rs | 0 lms/{lms => src}/lms/private.rs | 36 ++++++++++----------- lms/{lms => src}/lms/public.rs | 47 ++++++++++++++------------- lms/{lms => src}/lms/signature.rs | 7 ++-- lms/src/ots/error.rs | 21 ++++++++++++ lms/{lms => src}/ots/keypair.rs | 8 ++--- lms/{lms => src}/ots/mod.rs | 15 +++++---- lms/{lms => src}/ots/modes.rs | 0 lms/{lms => src}/ots/private.rs | 16 ++++----- lms/{lms => src}/ots/public.rs | 34 +++++++++---------- lms/{lms => src}/ots/signature.rs | 6 ++-- lms/{lms => src}/ots/util.rs | 0 lms/{lms => src}/types.rs | 0 22 files changed, 144 insertions(+), 147 deletions(-) delete mode 100644 lms/.github/workflows/lms-rust.yml rename lms/{lms => src}/constants.rs (100%) rename lms/{lms => src}/error.rs (100%) rename lms/{lms => src}/lib.rs (100%) create mode 100644 lms/src/lms/error.rs rename lms/{lms => src}/lms/keypair.rs (52%) rename lms/{lms => src}/lms/mod.rs (84%) rename lms/{lms => src}/lms/modes.rs (100%) rename lms/{lms => src}/lms/private.rs (95%) rename lms/{lms => src}/lms/public.rs (85%) rename lms/{lms => src}/lms/signature.rs (98%) create mode 100644 lms/src/ots/error.rs rename lms/{lms => src}/ots/keypair.rs (52%) rename lms/{lms => src}/ots/mod.rs (94%) rename lms/{lms => src}/ots/modes.rs (100%) rename lms/{lms => src}/ots/private.rs (93%) rename lms/{lms => src}/ots/public.rs (80%) rename lms/{lms => src}/ots/signature.rs (99%) rename lms/{lms => src}/ots/util.rs (100%) rename lms/{lms => src}/types.rs (100%) diff --git a/.github/workflows/lms.yml b/.github/workflows/lms.yml index 4914ade5..f2dc0701 100644 --- a/.github/workflows/lms.yml +++ b/.github/workflows/lms.yml @@ -10,7 +10,7 @@ on: defaults: run: - working-directory: . + working-directory: lms env: CARGO_INCREMENTAL: 0 diff --git a/lms/.github/workflows/lms-rust.yml b/lms/.github/workflows/lms-rust.yml deleted file mode 100644 index 9297ac02..00000000 --- a/lms/.github/workflows/lms-rust.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: lms-rust -on: - pull_request: - paths: - - ".github/workflows/lms-rust.yml" - - "lms/**" - - "Cargo.*" - push: - branches: main - -defaults: - run: - working-directory: . - -env: - CARGO_INCREMENTAL: 0 - RUSTFLAGS: "-Dwarnings" - -jobs: - clippy: - runs-on: ubuntu-latest - strategy: - matrix: - rust: - - 1.73.0 # MSRV for RustCrypto - - stable - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ matrix.rust }} - components: clippy - - run: cargo clippy --no-default-features - - run: cargo clippy - - run: cargo clippy --all-features - - test: - runs-on: ubuntu-latest - strategy: - matrix: - rust: - - 1.73.0 # MSRV for RustCrypto - - stable - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@master - with: - toolchain: ${{ matrix.rust }} - - run: cargo check --all-features - - run: cargo test --no-default-features - - run: cargo test - - run: cargo test --all-features diff --git a/lms/Cargo.toml b/lms/Cargo.toml index d817869f..fead2919 100644 --- a/lms/Cargo.toml +++ b/lms/Cargo.toml @@ -2,16 +2,21 @@ name = "lms-signature" version = "0.1.0" edition = "2021" +license = "Apache-2.0 OR MIT" +repository = "https://github.com/RustCrypto/signatures/tree/master/lms" +readme = "README.md" +rust-version = "1.73" +categories = ["cryptography"] +keywords = ["crypto", "signature"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] name = "lms" -path = "lms/lib.rs" +path = "src/lib.rs" edition = "2021" [dependencies] -anyhow = "1.0.75" digest = "0.10.7" generic-array = {version = "0.14.4", features = ["zeroize"]} rand = "0.8.5" diff --git a/lms/lms/constants.rs b/lms/src/constants.rs similarity index 100% rename from lms/lms/constants.rs rename to lms/src/constants.rs diff --git a/lms/lms/error.rs b/lms/src/error.rs similarity index 100% rename from lms/lms/error.rs rename to lms/src/error.rs diff --git a/lms/lms/lib.rs b/lms/src/lib.rs similarity index 100% rename from lms/lms/lib.rs rename to lms/src/lib.rs diff --git a/lms/src/lms/error.rs b/lms/src/lms/error.rs new file mode 100644 index 00000000..c64cde31 --- /dev/null +++ b/lms/src/lms/error.rs @@ -0,0 +1,21 @@ +//! LMS Signing error + +use std::error::Error; +use std::fmt::{Display, Formatter, Result}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LmsSigningError { + OutOfPrivateKeys, +} + +impl Display for LmsSigningError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + Self::OutOfPrivateKeys => { + write!(f, "private key has been exhausted") + } + } + } +} + +impl Error for LmsSigningError {} diff --git a/lms/lms/lms/keypair.rs b/lms/src/lms/keypair.rs similarity index 52% rename from lms/lms/lms/keypair.rs rename to lms/src/lms/keypair.rs index 505e7a5f..0f8109f8 100644 --- a/lms/lms/lms/keypair.rs +++ b/lms/src/lms/keypair.rs @@ -1,11 +1,11 @@ use crate::lms::modes::LmsMode; -use crate::lms::private::PrivateKey; -use crate::lms::public::PublicKey; +use crate::lms::private::SigningKey; +use crate::lms::public::VerifyingKey; use signature::Keypair; // implements the Keypair trait for PrivateKey -impl Keypair for PrivateKey { - type VerifyingKey = PublicKey; +impl Keypair for SigningKey { + type VerifyingKey = VerifyingKey; fn verifying_key(&self) -> Self::VerifyingKey { self.public() diff --git a/lms/lms/lms/mod.rs b/lms/src/lms/mod.rs similarity index 84% rename from lms/lms/lms/mod.rs rename to lms/src/lms/mod.rs index dc3832f0..9562ad71 100644 --- a/lms/lms/lms/mod.rs +++ b/lms/src/lms/mod.rs @@ -1,5 +1,6 @@ //! Everything related to LMS (and not LM-OTS) +pub mod error; mod keypair; pub(crate) mod modes; mod private; @@ -9,8 +10,8 @@ pub mod signature; pub use modes::{ LmsMode, LmsSha256M32H10, LmsSha256M32H15, LmsSha256M32H20, LmsSha256M32H25, LmsSha256M32H5, }; -pub use private::PrivateKey; -pub use public::PublicKey; +pub use private::SigningKey; +pub use public::VerifyingKey; pub use signature::Signature; #[cfg(test)] @@ -19,13 +20,13 @@ mod tests { use super::*; - use crate::{lms::PrivateKey, ots::LmsOtsSha256N32W4}; + use crate::{lms::SigningKey, ots::LmsOtsSha256N32W4}; fn test_sign_and_verify() { let mut rng = rand::thread_rng(); // Generate a fresh keypair - let mut sk = PrivateKey::::new(&mut rng); + let mut sk = SigningKey::::new(&mut rng); let pk = sk.public(); let msg = "this is a test message".as_bytes(); diff --git a/lms/lms/lms/modes.rs b/lms/src/lms/modes.rs similarity index 100% rename from lms/lms/lms/modes.rs rename to lms/src/lms/modes.rs diff --git a/lms/lms/lms/private.rs b/lms/src/lms/private.rs similarity index 95% rename from lms/lms/lms/private.rs rename to lms/src/lms/private.rs index 9756f29b..618fb0c2 100644 --- a/lms/lms/lms/private.rs +++ b/lms/src/lms/private.rs @@ -1,10 +1,10 @@ use crate::constants::{D_INTR, D_LEAF, ID_LEN}; use crate::error::LmsDeserializeError; -use crate::lms::{LmsMode, PublicKey, Signature}; -use crate::ots::PrivateKey as OtsPrivateKey; +use crate::lms::error::LmsSigningError; +use crate::lms::{LmsMode, Signature, VerifyingKey}; +use crate::ots::SigningKey as OtsPrivateKey; use crate::types::{Identifier, Typecode}; -use anyhow::anyhow; use digest::{Digest, Output, OutputSizeUser}; use generic_array::{ArrayLength, GenericArray}; use rand::{CryptoRng, Rng}; @@ -14,20 +14,20 @@ use std::cmp::Ordering; use std::ops::Add; use typenum::{Sum, U28}; -/// Opaque struct representing a LMS public key +/// Opaque struct representing a LMS private key /// /// Note: there is no requirement to map specific LMS algorithms to specific /// LM-OTS algorithms so it must be parametrized. With the algorithms provided /// by this crate, this is done via /// [LmsSha256M32H10](crate::lms::LmsSha256M32H10)<[LmsOtsSha256N32W4](crate::ots::LmsOtsSha256N32W4)>. -pub struct PrivateKey { +pub struct SigningKey { id: Identifier, seed: Output, // Re-generate the leaf privkeys as-needed from a seed auth_tree: GenericArray, Mode::TreeLen>, // TODO: Decide whether/when to precompute q: u32, } -impl PrivateKey { +impl SigningKey { /// Creates a new private key with a random identifier using /// algorithm 5 from pub fn new(mut rng: impl Rng + CryptoRng) -> Self { @@ -88,8 +88,8 @@ impl PrivateKey { } /// this implements algorithm 1 from - pub fn public(&self) -> PublicKey { - PublicKey::::new(self.id, self.auth_tree[0].clone()) + pub fn public(&self) -> VerifyingKey { + VerifyingKey::::new(self.id, self.auth_tree[0].clone()) } /// Returns the 16-byte identifier of the key pair @@ -104,16 +104,14 @@ impl PrivateKey { } // this implements the algorithm from Appendix D in -impl RandomizedSignerMut> for PrivateKey { +impl RandomizedSignerMut> for SigningKey { fn try_sign_with_rng( &mut self, rng: &mut impl rand_core::CryptoRngCore, msg: &[u8], ) -> Result, Error> { if self.q >= Mode::LEAVES { - return Err(Error::from_source(anyhow!( - "private key has been exhausted" - ))); + return Err(Error::from_source(LmsSigningError::OutOfPrivateKeys)); } let mut ots_priv_key = @@ -136,13 +134,13 @@ impl RandomizedSignerMut> for PrivateKey { } /// Converts a [PrivateKey] into its byte representation -impl From> +impl From> for GenericArray::OutputSize, U28>> where ::OutputSize: Add, Sum<::OutputSize, U28>: ArrayLength, { - fn from(pk: PrivateKey) -> Self { + fn from(pk: SigningKey) -> Self { // Return u32(type) || u32(otstype) || u32(q) || id || seed GenericArray::from_exact_iter( std::iter::empty() @@ -157,7 +155,7 @@ where } /// Tries to parse a [PrivateKey] from an exact slice -impl<'a, Mode: LmsMode> TryFrom<&'a [u8]> for PrivateKey { +impl<'a, Mode: LmsMode> TryFrom<&'a [u8]> for SigningKey { type Error = LmsDeserializeError; fn try_from(pk: &'a [u8]) -> Result { @@ -202,7 +200,7 @@ impl<'a, Mode: LmsMode> TryFrom<&'a [u8]> for PrivateKey { #[cfg(test)] mod tests { - use super::PrivateKey; + use super::SigningKey; use crate::lms::modes::{LmsSha256M32H10, LmsSha256M32H5}; use crate::ots::modes::{LmsOtsSha256N32W4, LmsOtsSha256N32W8}; use hex_literal::hex; @@ -214,7 +212,7 @@ mod tests { let id = hex!("d08fabd4a2091ff0a8cb4ed834e74534"); let expected_k = hex!("32a58885cd9ba0431235466bff9651c6c92124404d45fa53cf161c28f1ad5a8e"); - let lms_priv = PrivateKey::>::new_from_seed(id, seed); + let lms_priv = SigningKey::>::new_from_seed(id, seed); let lms_pub = lms_priv.public(); assert_eq!(lms_pub.k(), expected_k); assert_eq!(lms_pub.id(), &id); @@ -226,7 +224,7 @@ mod tests { let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); let expected_k = hex!("a1cd035833e0e90059603f26e07ad2aad152338e7a5e5984bcd5f7bb4eba40b7"); - let lms_priv = PrivateKey::>::new_from_seed(id, seed); + let lms_priv = SigningKey::>::new_from_seed(id, seed); let lms_pub = lms_priv.public(); assert_eq!(lms_pub.k(), expected_k); assert_eq!(lms_pub.id(), &id); @@ -334,7 +332,7 @@ mod tests { let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); let _expected_k = hex!("a1cd035833e0e90059603f26e07ad2aad152338e7a5e5984bcd5f7bb4eba40b7"); - let mut lms_priv = PrivateKey::>::new_from_seed(id, seed); + let mut lms_priv = SigningKey::>::new_from_seed(id, seed); lms_priv.q = 4; let _lms_pub = lms_priv.public(); diff --git a/lms/lms/lms/public.rs b/lms/src/lms/public.rs similarity index 85% rename from lms/lms/lms/public.rs rename to lms/src/lms/public.rs index 526469f1..6493dd0a 100644 --- a/lms/lms/lms/public.rs +++ b/lms/src/lms/public.rs @@ -19,12 +19,12 @@ use digest::Output; #[derive(Debug)] /// Opaque struct representing a LMS public key -pub struct PublicKey { +pub struct VerifyingKey { pub(crate) id: Identifier, pub(crate) k: Output, } -impl Clone for PublicKey { +impl Clone for VerifyingKey { fn clone(&self) -> Self { Self { id: self.id, @@ -33,13 +33,13 @@ impl Clone for PublicKey { } } -impl PartialEq for PublicKey { +impl PartialEq for VerifyingKey { fn eq(&self, other: &Self) -> bool { self.id == other.id && self.k == other.k } } -impl PublicKey { +impl VerifyingKey { pub fn new(id: Identifier, k: Output) -> Self { Self { id, k } } @@ -55,7 +55,7 @@ impl PublicKey { } } -impl Verifier> for PublicKey { +impl Verifier> for VerifyingKey { fn verify(&self, msg: &[u8], signature: &Signature) -> Result<(), Error> { // Compute the LMS Public Key Candidate Tc from the signature, // message, identifier, pubtype, and ots_typecode, using @@ -97,14 +97,14 @@ impl Verifier> for PublicKey { } } -/// Converts a [PublicKey] into its byte representation -impl From> +/// Converts a [VerifyingKey] into its byte representation +impl From> for GenericArray::OutputSize, U24>> where ::OutputSize: Add, Sum<::OutputSize, U24>: ArrayLength, { - fn from(pk: PublicKey) -> Self { + fn from(pk: VerifyingKey) -> Self { // Return u32(type) || u32(otstype) || id || k GenericArray::from_exact_iter( std::iter::empty() @@ -117,8 +117,8 @@ where } } -/// Tries to parse a [PublicKey] from an exact slice -impl<'a, Mode: LmsMode> TryFrom<&'a [u8]> for PublicKey { +/// Tries to parse a [VerifyingKey] from an exact slice +impl<'a, Mode: LmsMode> TryFrom<&'a [u8]> for VerifyingKey { type Error = LmsDeserializeError; fn try_from(pk: &'a [u8]) -> Result { @@ -160,8 +160,8 @@ mod tests { use crate::{ lms::modes::*, - lms::PrivateKey, - lms::PublicKey, + lms::SigningKey, + lms::VerifyingKey, ots::{LmsOtsSha256N32W4, LmsOtsSha256N32W8}, }; use digest::OutputSizeUser; @@ -180,8 +180,8 @@ mod tests { #[test] fn test_pubkey_deserialize_kat1() { - let pk = PublicKey::>::try_from(&KAT1[..]).unwrap(); - let expected = PublicKey::> { + let pk = VerifyingKey::>::try_from(&KAT1[..]).unwrap(); + let expected = VerifyingKey::> { id: hex!("61a5d57d37f5e46bfb7520806b07a1b8"), k: hex!("50650e3b31fe4a773ea29a07f09cf2ea30e579f0df58ef8e298da0434cb2b878").into(), }; @@ -190,20 +190,20 @@ mod tests { #[test] fn test_pubkey_deserialize_kat1_wrong_lms_mode() { - let pk = PublicKey::>::try_from(&KAT1[..]); + let pk = VerifyingKey::>::try_from(&KAT1[..]); assert_eq!(pk, Err(crate::error::LmsDeserializeError::WrongAlgorithm)); } #[test] fn test_pubkey_deserialize_kat1_wrong_otsmode() { - let pk = PublicKey::>::try_from(&KAT1[..]); + let pk = VerifyingKey::>::try_from(&KAT1[..]); assert_eq!(pk, Err(crate::error::LmsDeserializeError::WrongAlgorithm)); } #[test] fn test_pubkey_deserialize_kat1_too_short() { let pk_bytes = &KAT1[..(KAT1.len() - 4)]; - let pk = PublicKey::>::try_from(pk_bytes); + let pk = VerifyingKey::>::try_from(pk_bytes); assert_eq!(pk, Err(crate::error::LmsDeserializeError::TooShort)); } @@ -212,7 +212,7 @@ mod tests { let mut pk_bytes = vec![42; 4]; pk_bytes.extend_from_slice(&KAT1[..]); - let pk = PublicKey::>::try_from(&pk_bytes[..]); + let pk = VerifyingKey::>::try_from(&pk_bytes[..]); assert_eq!(pk, Err(crate::error::LmsDeserializeError::TooLong)); } @@ -226,7 +226,8 @@ mod tests { 50650e3b31fe4a773ea29a07f09cf2ea 30e579f0df58ef8e298da0434cb2b878" ); - let pk = PublicKey::>::try_from(&pk_bytes[..]).unwrap(); + let pk = + VerifyingKey::>::try_from(&pk_bytes[..]).unwrap(); let pk_serialized: GenericArray = pk.clone().into(); let bytes = pk_serialized.as_slice(); assert_eq!(bytes, &pk_bytes[..]); @@ -246,7 +247,7 @@ mod tests { d152338e7a5e5984bcd5f7bb4eba40b7 " ); - let lms_priv = PrivateKey::>::new_from_seed(id, seed); + let lms_priv = SigningKey::>::new_from_seed(id, seed); let lms_pub = lms_priv.public(); let lms_pub_serialized: GenericArray = lms_pub.into(); let bytes = lms_pub_serialized.as_slice(); @@ -255,19 +256,19 @@ mod tests { fn test_serialize_deserialize_random() where - PublicKey: std::fmt::Debug, + VerifyingKey: std::fmt::Debug, ::OutputSize: Add, Sum<::OutputSize, U24>: ArrayLength, { let rng = rand::thread_rng(); - let lms_priv = PrivateKey::::new(rng); + let lms_priv = SigningKey::::new(rng); let lms_pub = lms_priv.public(); let lms_pub_serialized: GenericArray< u8, Sum<::OutputSize, U24>, > = lms_pub.clone().into(); let bytes = lms_pub_serialized.as_slice(); - let lms_pub_deserialized = PublicKey::::try_from(bytes).unwrap(); + let lms_pub_deserialized = VerifyingKey::::try_from(bytes).unwrap(); assert_eq!(lms_pub, lms_pub_deserialized); } diff --git a/lms/lms/lms/signature.rs b/lms/src/lms/signature.rs similarity index 98% rename from lms/lms/lms/signature.rs rename to lms/src/lms/signature.rs index ef1ba5dd..eab3b503 100644 --- a/lms/lms/lms/signature.rs +++ b/lms/src/lms/signature.rs @@ -137,7 +137,7 @@ mod tests { use std::ops::{Add, Mul}; use crate::lms::modes::*; - use crate::lms::{PrivateKey, PublicKey, Signature}; + use crate::lms::{Signature, SigningKey, VerifyingKey}; use crate::ots::modes::*; use generic_array::ArrayLength; use hex_literal::hex; @@ -249,7 +249,8 @@ mod tests { 6f7220746f207468652070656f706c65 2e0a" ); - let pk = PublicKey::>::try_from(&pk_bytes[..]).unwrap(); + let pk = + VerifyingKey::>::try_from(&pk_bytes[..]).unwrap(); let sig = Signature::>::try_from(&sig_bytes[..]).unwrap(); assert!(pk.verify(&msg[..], &sig).is_ok()); } @@ -266,7 +267,7 @@ mod tests { >: ArrayLength, { let mut rng = thread_rng(); - let mut sk = PrivateKey::::new(&mut rng); + let mut sk = SigningKey::::new(&mut rng); let pk = sk.public(); let msg = b"Hello, world!"; let sig = sk.sign_with_rng(&mut rng, msg); diff --git a/lms/src/ots/error.rs b/lms/src/ots/error.rs new file mode 100644 index 00000000..d219a9a7 --- /dev/null +++ b/lms/src/ots/error.rs @@ -0,0 +1,21 @@ +//! LM-OTS Signing error + +use std::error::Error; +use std::fmt::{Display, Formatter, Result}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum LmsOtsSigningError { + InvalidPrivateKey, +} + +impl Display for LmsOtsSigningError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result { + match self { + Self::InvalidPrivateKey => { + write!(f, "private key is no longer valid") + } + } + } +} + +impl Error for LmsOtsSigningError {} diff --git a/lms/lms/ots/keypair.rs b/lms/src/ots/keypair.rs similarity index 52% rename from lms/lms/ots/keypair.rs rename to lms/src/ots/keypair.rs index a66928a4..841f77a9 100644 --- a/lms/lms/ots/keypair.rs +++ b/lms/src/ots/keypair.rs @@ -1,11 +1,11 @@ use crate::ots::modes::LmsOtsMode; -use crate::ots::private::PrivateKey; -use crate::ots::public::PublicKey; +use crate::ots::private::SigningKey; +use crate::ots::public::VerifyingKey; use signature::Keypair; // implements the Keypair trait for PrivateKey -impl Keypair for PrivateKey { - type VerifyingKey = PublicKey; +impl Keypair for SigningKey { + type VerifyingKey = VerifyingKey; fn verifying_key(&self) -> Self::VerifyingKey { self.public() diff --git a/lms/lms/ots/mod.rs b/lms/src/ots/mod.rs similarity index 94% rename from lms/lms/ots/mod.rs rename to lms/src/ots/mod.rs index b60674ea..1cd53d8d 100644 --- a/lms/lms/ots/mod.rs +++ b/lms/src/ots/mod.rs @@ -1,5 +1,6 @@ //! Everything related to LM-OTS +pub mod error; mod keypair; pub(crate) mod modes; mod private; @@ -10,8 +11,8 @@ mod util; pub use modes::{ LmsOtsMode, LmsOtsSha256N32W1, LmsOtsSha256N32W2, LmsOtsSha256N32W4, LmsOtsSha256N32W8, }; -pub use private::PrivateKey; -pub use public::PublicKey; +pub use private::SigningKey; +pub use public::VerifyingKey; pub use signature::Signature; #[cfg(test)] @@ -20,7 +21,7 @@ pub mod tests { use crate::ots::modes::{ LmsOtsMode, LmsOtsSha256N32W1, LmsOtsSha256N32W2, LmsOtsSha256N32W4, LmsOtsSha256N32W8, }; - use crate::ots::private::PrivateKey; + use crate::ots::private::SigningKey; use digest::Digest; use digest::OutputSizeUser; use generic_array::{ArrayLength, GenericArray}; @@ -41,7 +42,7 @@ pub mod tests { Sum<::OutputSize, U2>: ArrayLength, { let mut rng = thread_rng(); - let mut sk = PrivateKey::::new(0, [0xcc; ID_LEN], &mut rng); + let mut sk = SigningKey::::new(0, [0xcc; ID_LEN], &mut rng); let pk = sk.public(); let msg = "this is a test message".as_bytes(); @@ -65,7 +66,7 @@ pub mod tests { Sum<::OutputSize, U2>: ArrayLength, { let mut rng = thread_rng(); - let mut sk = PrivateKey::::new(0, [0xcc; ID_LEN], &mut rng); + let mut sk = SigningKey::::new(0, [0xcc; ID_LEN], &mut rng); let mut pk = sk.public(); let msg = "this is a test message".as_bytes(); @@ -166,7 +167,7 @@ pub mod tests { let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); let q = 4; let y0 = hex!("11b3649023696f85150b189e50c00e98850ac343a77b3638319c347d7310269d"); - let mut sk = PrivateKey::::new_from_seed(q, id, seed); + let mut sk = SigningKey::::new_from_seed(q, id, seed); let _ = sk.public(); let c = hex!("0eb1ed54a2460d512388cad533138d240534e97b1e82d33bd927d201dfc24ebb"); @@ -189,7 +190,7 @@ pub mod tests { let k = hex!("4de1f6965bdabc676c5a4dc7c35f97f82cb0e31c68d04f1dad96314ff09e6b3d"); - let sk = PrivateKey::::new_from_seed(q, id, seed); + let sk = SigningKey::::new_from_seed(q, id, seed); let pk = sk.public(); // H(I||u32str(r)||u16str(D_LEAF)||OTS_PUB_HASH[r-2^h]) let x = ::Hasher::new() diff --git a/lms/lms/ots/modes.rs b/lms/src/ots/modes.rs similarity index 100% rename from lms/lms/ots/modes.rs rename to lms/src/ots/modes.rs diff --git a/lms/lms/ots/private.rs b/lms/src/ots/private.rs similarity index 93% rename from lms/lms/ots/private.rs rename to lms/src/ots/private.rs index 2cfaea04..2d4e4db7 100644 --- a/lms/lms/ots/private.rs +++ b/lms/src/ots/private.rs @@ -1,10 +1,10 @@ use crate::constants::{D_MESG, D_PBLC}; +use crate::ots::error::LmsOtsSigningError; use crate::ots::modes::LmsOtsMode; -use crate::ots::public::PublicKey; +use crate::ots::public::VerifyingKey; use crate::ots::signature::Signature; use crate::types::Identifier; -use anyhow::anyhow; use digest::{Digest, Output}; use generic_array::sequence::GenericSequence; use generic_array::GenericArray; @@ -16,14 +16,14 @@ use zeroize::Zeroize; #[derive(Debug)] /// Opaque struct representing an LM-OTS private key. Does not implement /// [Clone] because OTS keys are supposed to be one time use. -pub struct PrivateKey { +pub struct SigningKey { q: u32, id: Identifier, x: GenericArray, Mode::PLen>, valid: bool, } -impl PrivateKey { +impl SigningKey { /// Generate a private key, expanded pseudorandomly from a seed generated by `rng`. /// Uses the algorithm from appendix A // a key part of this code working is the DerefMut impl for GenericArray which we abuse in a similar manner to @@ -59,7 +59,7 @@ impl PrivateKey { } /// this implements algorithm 1 from - pub fn public(&self) -> PublicKey { + pub fn public(&self) -> VerifyingKey { let mut hasher = Mode::Hasher::new() .chain_update(self.id) .chain_update(self.q.to_be_bytes()) @@ -81,7 +81,7 @@ impl PrivateKey { hasher.update(&tmp); } - PublicKey { + VerifyingKey { id: self.id, q: self.q, k: hasher.finalize(), @@ -96,14 +96,14 @@ impl PrivateKey { } } -impl RandomizedSignerMut> for PrivateKey { +impl RandomizedSignerMut> for SigningKey { fn try_sign_with_rng( &mut self, rng: &mut impl CryptoRngCore, msg: &[u8], ) -> Result, Error> { if !self.valid { - return Err(Error::from_source(anyhow!("private key no longer valid"))); + return Err(Error::from_source(LmsOtsSigningError::InvalidPrivateKey)); } // Generate the message randomizer C diff --git a/lms/lms/ots/public.rs b/lms/src/ots/public.rs similarity index 80% rename from lms/lms/ots/public.rs rename to lms/src/ots/public.rs index 6c79cd5c..705ebdae 100644 --- a/lms/lms/ots/public.rs +++ b/lms/src/ots/public.rs @@ -1,4 +1,4 @@ -//! Contains the [PublicKey] type +//! Contains the [VerifyingKey] type use crate::constants::ID_LEN; use crate::error::LmsDeserializeError; @@ -16,14 +16,14 @@ use typenum::{Sum, U2, U24}; #[derive(Debug)] /// Opaque struct representing a LM-OTS public key -pub struct PublicKey { +pub struct VerifyingKey { pub(crate) q: u32, pub(crate) id: Identifier, pub(crate) k: Output, } // manual Clone impl because Mode is not Clone -impl Clone for PublicKey { +impl Clone for VerifyingKey { fn clone(&self) -> Self { Self { q: self.q, @@ -34,13 +34,13 @@ impl Clone for PublicKey { } // manual PartialEq impl because Mode is not PartialEq -impl PartialEq for PublicKey { +impl PartialEq for VerifyingKey { fn eq(&self, other: &Self) -> bool { self.q == other.q && self.id == other.id && self.k == other.k } } -impl Verifier> for PublicKey +impl Verifier> for VerifyingKey where // required to concat Q and cksm(Q) ::OutputSize: Add, @@ -60,14 +60,14 @@ where } } -/// Converts a [PublicKey] into its byte representation -impl From> +/// Converts a [VerifyingKey] into its byte representation +impl From> for GenericArray::OutputSize, U24>> where ::OutputSize: Add, Sum<::OutputSize, U24>: ArrayLength, { - fn from(pk: PublicKey) -> Self { + fn from(pk: VerifyingKey) -> Self { // Return u32str(type) || I || u32str(q) || K GenericArray::from_exact_iter( std::iter::empty() @@ -80,8 +80,8 @@ where } } -/// Tries to parse a [PublicKey] from an exact slice -impl<'a, Mode: LmsOtsMode> TryFrom<&'a [u8]> for PublicKey { +/// Tries to parse a [VerifyingKey] from an exact slice +impl<'a, Mode: LmsOtsMode> TryFrom<&'a [u8]> for VerifyingKey { type Error = LmsDeserializeError; fn try_from(pk: &'a [u8]) -> Result { @@ -120,28 +120,28 @@ mod tests { use crate::constants::ID_LEN; use crate::error::LmsDeserializeError; use crate::ots::modes::{LmsOtsSha256N32W4, LmsOtsSha256N32W8}; - use crate::ots::private::PrivateKey; - use crate::ots::public::PublicKey; + use crate::ots::private::SigningKey; + use crate::ots::public::VerifyingKey; use generic_array::GenericArray; use rand::thread_rng; #[test] fn test_serde() { let pk = - PrivateKey::::new(0, [0xbb; ID_LEN], &mut thread_rng()).public(); + SigningKey::::new(0, [0xbb; ID_LEN], &mut thread_rng()).public(); let pk_serialized: GenericArray = pk.clone().into(); let bytes = pk_serialized.as_slice(); - let pk_deserialized = PublicKey::::try_from(bytes); + let pk_deserialized = VerifyingKey::::try_from(bytes); assert!(pk_deserialized.is_ok()); let pk_deserialized = pk_deserialized.unwrap(); assert_eq!(pk, pk_deserialized); - let pk_wrongalgo = PublicKey::::try_from(bytes); - let pk_short = PublicKey::::try_from(&bytes[0..(bytes.len() - 1)]); + let pk_wrongalgo = VerifyingKey::::try_from(bytes); + let pk_short = VerifyingKey::::try_from(&bytes[0..(bytes.len() - 1)]); let mut long_bytes = pk_serialized.into_iter().collect::>(); long_bytes.push(0); - let pk_long = PublicKey::::try_from(long_bytes.as_slice()); + let pk_long = VerifyingKey::::try_from(long_bytes.as_slice()); assert_eq!(pk_wrongalgo, Err(LmsDeserializeError::WrongAlgorithm)); assert_eq!(pk_short, Err(LmsDeserializeError::TooShort)); diff --git a/lms/lms/ots/signature.rs b/lms/src/ots/signature.rs similarity index 99% rename from lms/lms/ots/signature.rs rename to lms/src/ots/signature.rs index cac479d9..5ffcfa3e 100644 --- a/lms/lms/ots/signature.rs +++ b/lms/src/ots/signature.rs @@ -11,7 +11,7 @@ use std::cmp::Ordering; use std::ops::{Add, Mul}; use typenum::{Prod, Sum, U1, U4}; -use super::PublicKey; +use super::VerifyingKey; #[derive(Debug, Eq)] /// Opaque struct representing a LM-OTS signature @@ -105,7 +105,7 @@ impl Signature { /// Returns a public key candidate for this signature as defined by /// algorithm 4b of the LMS RFC. The signature will always be valid for /// the returned public key candidate. - pub fn recover_pubkey(&self, id: Identifier, q: u32, msg: &[u8]) -> PublicKey { + pub fn recover_pubkey(&self, id: Identifier, q: u32, msg: &[u8]) -> VerifyingKey { // algorithm 4b // Q = H(I || u32str(q) || u16str(D_MESG) || C || message) @@ -144,7 +144,7 @@ impl Signature { // Kc = H(I || u32str(q) || u16str(D_PBLC) || z[0] || z[1] || ... || z[p-1]) hasher.update(&tmp); } - PublicKey { + VerifyingKey { id, q, k: hasher.finalize(), diff --git a/lms/lms/ots/util.rs b/lms/src/ots/util.rs similarity index 100% rename from lms/lms/ots/util.rs rename to lms/src/ots/util.rs diff --git a/lms/lms/types.rs b/lms/src/types.rs similarity index 100% rename from lms/lms/types.rs rename to lms/src/types.rs From 8e74c6b877949b27583375ee7619ba2d94159d08 Mon Sep 17 00:00:00 2001 From: Will Song Date: Fri, 16 Feb 2024 00:38:29 -0500 Subject: [PATCH 3/5] code review round 2 --- lms/.gitignore | 2 - lms/CHANGELOG.md | 3 - lms/Cargo.lock | 225 +++++++++++++++++++++++++++++++++++++++ lms/Cargo.toml | 2 +- lms/src/lms/error.rs | 14 +-- lms/src/lms/private.rs | 4 +- lms/src/lms/public.rs | 4 +- lms/src/lms/signature.rs | 4 +- lms/src/ots/error.rs | 14 +-- lms/src/ots/private.rs | 4 +- lms/src/ots/public.rs | 6 +- lms/src/ots/signature.rs | 8 +- 12 files changed, 249 insertions(+), 41 deletions(-) delete mode 100644 lms/.gitignore create mode 100644 lms/Cargo.lock diff --git a/lms/.gitignore b/lms/.gitignore deleted file mode 100644 index 1e7caa9e..00000000 --- a/lms/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -Cargo.lock -target/ diff --git a/lms/CHANGELOG.md b/lms/CHANGELOG.md index 41d27065..d6637e04 100644 --- a/lms/CHANGELOG.md +++ b/lms/CHANGELOG.md @@ -3,6 +3,3 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## 0.1.0 (2024-01-30) -- Initial release \ No newline at end of file diff --git a/lms/Cargo.lock b/lms/Cargo.lock new file mode 100644 index 00000000..28436ff7 --- /dev/null +++ b/lms/Cargo.lock @@ -0,0 +1,225 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.0-pre.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc17eb697364b18256ec92675ebe6b7b153d2f1041e568d74533c5d0fc1ca162" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common 0.1.6", +] + +[[package]] +name = "digest" +version = "0.11.0-pre.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3be3c52e023de5662dc05a32f747d09a1d6024fdd1f64b0850e373269efb43" +dependencies = [ + "crypto-common 0.2.0-pre.3", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hybrid-array" +version = "0.2.0-pre.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27fbaf242418fe980caf09ed348d5a6aeabe71fc1bd8bebad641f4591ae0a46d" +dependencies = [ + "typenum", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "lms" +version = "0.1.0" +dependencies = [ + "anyhow", + "digest 0.10.7", + "generic-array", + "hex", + "hex-literal", + "rand", + "rand_core", + "sha2", + "signature", + "static_assertions", + "typenum", + "zeroize", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "signature" +version = "2.3.0-pre.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9b544e1447a755027450a51bc29aa51772e071bd839aca18136a7dab1b93e" +dependencies = [ + "digest 0.11.0-pre.3", + "rand_core", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/lms/Cargo.toml b/lms/Cargo.toml index fead2919..c0ff68b4 100644 --- a/lms/Cargo.toml +++ b/lms/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lms-signature" -version = "0.1.0" +version = "0.0.0" edition = "2021" license = "Apache-2.0 OR MIT" repository = "https://github.com/RustCrypto/signatures/tree/master/lms" diff --git a/lms/src/lms/error.rs b/lms/src/lms/error.rs index c64cde31..0367de01 100644 --- a/lms/src/lms/error.rs +++ b/lms/src/lms/error.rs @@ -4,18 +4,12 @@ use std::error::Error; use std::fmt::{Display, Formatter, Result}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum LmsSigningError { - OutOfPrivateKeys, -} +pub struct LmsOutOfPrivateKeys {} -impl Display for LmsSigningError { +impl Display for LmsOutOfPrivateKeys { fn fmt(&self, f: &mut Formatter<'_>) -> Result { - match self { - Self::OutOfPrivateKeys => { - write!(f, "private key has been exhausted") - } - } + write!(f, "private key has been exhausted") } } -impl Error for LmsSigningError {} +impl Error for LmsOutOfPrivateKeys {} diff --git a/lms/src/lms/private.rs b/lms/src/lms/private.rs index 618fb0c2..7946288f 100644 --- a/lms/src/lms/private.rs +++ b/lms/src/lms/private.rs @@ -1,6 +1,6 @@ use crate::constants::{D_INTR, D_LEAF, ID_LEN}; use crate::error::LmsDeserializeError; -use crate::lms::error::LmsSigningError; +use crate::lms::error::LmsOutOfPrivateKeys; use crate::lms::{LmsMode, Signature, VerifyingKey}; use crate::ots::SigningKey as OtsPrivateKey; use crate::types::{Identifier, Typecode}; @@ -111,7 +111,7 @@ impl RandomizedSignerMut> for SigningKey { msg: &[u8], ) -> Result, Error> { if self.q >= Mode::LEAVES { - return Err(Error::from_source(LmsSigningError::OutOfPrivateKeys)); + return Err(Error::from_source(LmsOutOfPrivateKeys {})); } let mut ots_priv_key = diff --git a/lms/src/lms/public.rs b/lms/src/lms/public.rs index 6493dd0a..40573723 100644 --- a/lms/src/lms/public.rs +++ b/lms/src/lms/public.rs @@ -97,7 +97,7 @@ impl Verifier> for VerifyingKey { } } -/// Converts a [VerifyingKey] into its byte representation +/// Converts a [`VerifyingKey`] into its byte representation impl From> for GenericArray::OutputSize, U24>> where @@ -117,7 +117,7 @@ where } } -/// Tries to parse a [VerifyingKey] from an exact slice +/// Tries to parse a [`VerifyingKey`] from an exact slice impl<'a, Mode: LmsMode> TryFrom<&'a [u8]> for VerifyingKey { type Error = LmsDeserializeError; diff --git a/lms/src/lms/signature.rs b/lms/src/lms/signature.rs index eab3b503..bafe2773 100644 --- a/lms/src/lms/signature.rs +++ b/lms/src/lms/signature.rs @@ -1,4 +1,4 @@ -//! Contains the [Signature] type +//! Contains the [`Signature`] type use crate::error::LmsDeserializeError; use crate::lms::LmsMode; @@ -77,7 +77,7 @@ where } } -/// Tries to parse a [Signature] from an exact slice +/// Tries to parse a [`Signature`] from an exact slice impl TryFrom<&[u8]> for Signature { type Error = LmsDeserializeError; diff --git a/lms/src/ots/error.rs b/lms/src/ots/error.rs index d219a9a7..54cba2ea 100644 --- a/lms/src/ots/error.rs +++ b/lms/src/ots/error.rs @@ -4,18 +4,12 @@ use std::error::Error; use std::fmt::{Display, Formatter, Result}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum LmsOtsSigningError { - InvalidPrivateKey, -} +pub struct LmsOtsInvalidPrivateKey {} -impl Display for LmsOtsSigningError { +impl Display for LmsOtsInvalidPrivateKey { fn fmt(&self, f: &mut Formatter<'_>) -> Result { - match self { - Self::InvalidPrivateKey => { - write!(f, "private key is no longer valid") - } - } + write!(f, "private key is no longer valid") } } -impl Error for LmsOtsSigningError {} +impl Error for LmsOtsInvalidPrivateKey {} diff --git a/lms/src/ots/private.rs b/lms/src/ots/private.rs index 2d4e4db7..3bc93fc2 100644 --- a/lms/src/ots/private.rs +++ b/lms/src/ots/private.rs @@ -1,5 +1,5 @@ use crate::constants::{D_MESG, D_PBLC}; -use crate::ots::error::LmsOtsSigningError; +use crate::ots::error::LmsOtsInvalidPrivateKey; use crate::ots::modes::LmsOtsMode; use crate::ots::public::VerifyingKey; use crate::ots::signature::Signature; @@ -103,7 +103,7 @@ impl RandomizedSignerMut> for SigningKey msg: &[u8], ) -> Result, Error> { if !self.valid { - return Err(Error::from_source(LmsOtsSigningError::InvalidPrivateKey)); + return Err(Error::from_source(LmsOtsInvalidPrivateKey {})); } // Generate the message randomizer C diff --git a/lms/src/ots/public.rs b/lms/src/ots/public.rs index 705ebdae..6687cad7 100644 --- a/lms/src/ots/public.rs +++ b/lms/src/ots/public.rs @@ -1,4 +1,4 @@ -//! Contains the [VerifyingKey] type +//! Contains the [`VerifyingKey`] type use crate::constants::ID_LEN; use crate::error::LmsDeserializeError; @@ -60,7 +60,7 @@ where } } -/// Converts a [VerifyingKey] into its byte representation +/// Converts a [`VerifyingKey`] into its byte representation impl From> for GenericArray::OutputSize, U24>> where @@ -80,7 +80,7 @@ where } } -/// Tries to parse a [VerifyingKey] from an exact slice +/// Tries to parse a [`VerifyingKey`] from an exact slice impl<'a, Mode: LmsOtsMode> TryFrom<&'a [u8]> for VerifyingKey { type Error = LmsDeserializeError; diff --git a/lms/src/ots/signature.rs b/lms/src/ots/signature.rs index 5ffcfa3e..98a21995 100644 --- a/lms/src/ots/signature.rs +++ b/lms/src/ots/signature.rs @@ -1,4 +1,4 @@ -//! Contains the [Signature] type +//! Contains the [`Signature`] type use crate::constants::{D_MESG, D_PBLC}; use crate::error::LmsDeserializeError; @@ -37,13 +37,13 @@ impl PartialEq for Signature { } } -/// Useful type alias to get the [GenericArray] representation +/// Useful type alias to get the [`GenericArray`] representation pub type Output = GenericArray< u8, Sum::NLen, Sum<::PLen, U1>>, U4>, >; -/// Converts a [Signature] into its byte representation +/// Converts a [`Signature`] into its byte representation impl From> for Output where // required for Output @@ -63,7 +63,7 @@ where } } -/// Tries to parse a [Signature] from an exact slice +/// Tries to parse a [`Signature`] from an exact slice impl<'a, Mode: LmsOtsMode> TryFrom<&'a [u8]> for Signature { type Error = LmsDeserializeError; From 4c62128086677d601b742d884421d8a03b9bddd8 Mon Sep 17 00:00:00 2001 From: Tjaden Hess Date: Fri, 16 Feb 2024 12:46:42 -0600 Subject: [PATCH 4/5] Add KAT provenance comments and replace public KAT2 test case with one that increases coverage --- lms/src/lms/private.rs | 13 ++++++++++--- lms/src/lms/public.rs | 35 ++++++++++++++++------------------- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/lms/src/lms/private.rs b/lms/src/lms/private.rs index 7946288f..841130fe 100644 --- a/lms/src/lms/private.rs +++ b/lms/src/lms/private.rs @@ -206,8 +206,11 @@ mod tests { use hex_literal::hex; use signature::{RandomizedSignerMut, SignatureEncoding}; + // Known-Answer Test vectors from #[test] - fn test_pk_tree_kat1() { + // Generate Test Case 2 top-level LMS public key + // LM_SHA256_M32_H10 / LMOTS_SHA256_N32_W4 + fn test_pk_gen_rfc8554_testcase_2_top_level() { let seed = hex!("558b8966c48ae9cb898b423c83443aae014a72f1b1ab5cc85cf1d892903b5439"); let id = hex!("d08fabd4a2091ff0a8cb4ed834e74534"); let expected_k = hex!("32a58885cd9ba0431235466bff9651c6c92124404d45fa53cf161c28f1ad5a8e"); @@ -219,7 +222,9 @@ mod tests { } #[test] - fn test_pk_tree_kat2() { + // Generate Test Case 2 leaf-level LMS public key + // LM_SHA256_M32_H5 / LMOTS_SHA256_N32_W8 + fn test_pk_gen_rfc8554_testcase_2_leaf_level() { let seed = hex!("a1c4696e2608035a886100d05cd99945eb3370731884a8235e2fb3d4d71f2547"); let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); let expected_k = hex!("a1cd035833e0e90059603f26e07ad2aad152338e7a5e5984bcd5f7bb4eba40b7"); @@ -231,7 +236,9 @@ mod tests { } #[test] - fn test_kat_2() { + // Byte-for-byte signature equivalence test with RFC 8554 Test Case 2 + // Leaf-level LMS signature. LM_SHA256_M32_H5 / LMOTS_SHA256_N32_W8 + fn test_sign_rfc8554_testcase_2() { let expected_signature = [ 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x0e, 0xb1, 0xed, 0x54, 0xa2, 0x46, 0x0d, 0x51, 0x23, 0x88, 0xca, 0xd5, 0x33, 0x13, 0x8d, 0x24, 0x05, 0x34, 0xe9, 0x7b, diff --git a/lms/src/lms/public.rs b/lms/src/lms/public.rs index 40573723..73b01263 100644 --- a/lms/src/lms/public.rs +++ b/lms/src/lms/public.rs @@ -169,6 +169,9 @@ mod tests { use hex_literal::hex; use typenum::{Sum, U24}; + // RFC 8554 Appendix F. Test Case 1 + // Top-level LMS Public Key + // LM_SHA256_M32_H5 / LMOTS_SHA256_N32_W8 const KAT1: [u8; 56] = hex!( " 00000005 @@ -218,36 +221,30 @@ mod tests { #[test] fn test_kat1_round_trip() { - let pk_bytes = hex!( - " - 00000005 - 00000004 - 61a5d57d37f5e46bfb7520806b07a1b8 - 50650e3b31fe4a773ea29a07f09cf2ea - 30e579f0df58ef8e298da0434cb2b878" - ); - let pk = - VerifyingKey::>::try_from(&pk_bytes[..]).unwrap(); + let pk = VerifyingKey::>::try_from(&KAT1[..]).unwrap(); let pk_serialized: GenericArray = pk.clone().into(); let bytes = pk_serialized.as_slice(); - assert_eq!(bytes, &pk_bytes[..]); + assert_eq!(bytes, &KAT1[..]); } + // RFC 8554 Appendix F. Test Case 2 + // Top-level LMS Public Key + // LM_SHA256_M32_H10 / LMOTS_SHA256_N32_W4 #[test] fn test_kat2() { // Tests that the serialized public key from RFC seed matches the expected value - let seed = hex!("a1c4696e2608035a886100d05cd99945eb3370731884a8235e2fb3d4d71f2547"); - let id = hex!("215f83b7ccb9acbcd08db97b0d04dc2b"); + let seed = hex!("558b8966c48ae9cb898b423c83443aae014a72f1b1ab5cc85cf1d892903b5439"); + let id = hex!("d08fabd4a2091ff0a8cb4ed834e74534"); let expected_pubkey = hex!( " - 00000005 - 00000004 - 215f83b7ccb9acbcd08db97b0d04dc2b - a1cd035833e0e90059603f26e07ad2aa - d152338e7a5e5984bcd5f7bb4eba40b7 + 00000006 + 00000003 + d08fabd4a2091ff0a8cb4ed834e74534 + 32a58885cd9ba0431235466bff9651c6 + c92124404d45fa53cf161c28f1ad5a8e " ); - let lms_priv = SigningKey::>::new_from_seed(id, seed); + let lms_priv = SigningKey::>::new_from_seed(id, seed); let lms_pub = lms_priv.public(); let lms_pub_serialized: GenericArray = lms_pub.into(); let bytes = lms_pub_serialized.as_slice(); From 58c15ce4e09ccb70be4735552b6b1289c20f9877 Mon Sep 17 00:00:00 2001 From: Will Song Date: Wed, 6 Mar 2024 14:24:28 -0500 Subject: [PATCH 5/5] Update .github/workflows/lms.yml Co-authored-by: Tony Arcieri --- .github/workflows/lms.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lms.yml b/.github/workflows/lms.yml index f2dc0701..39a3d94b 100644 --- a/.github/workflows/lms.yml +++ b/.github/workflows/lms.yml @@ -1,4 +1,4 @@ -name: lms-rust +name: lms on: pull_request: paths: