diff --git a/.github/workflows/module-lattice.yml b/.github/workflows/module-lattice.yml new file mode 100644 index 0000000..3610e71 --- /dev/null +++ b/.github/workflows/module-lattice.yml @@ -0,0 +1,61 @@ +name: module-lattice + +on: + pull_request: + paths: + - ".github/workflows/module-lattice.yml" + - "module-lattice/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: module-lattice + +env: + RUSTFLAGS: "-Dwarnings" + CARGO_INCREMENTAL: 0 + +# Cancels CI jobs when new commits are pushed to a PR branch +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + set-msrv: + uses: RustCrypto/actions/.github/workflows/set-msrv.yml@master + with: + msrv: 1.85.0 + + minimal-versions: + uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master + with: + working-directory: ${{ github.workflow }} + + build-no-std: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + targets: thumbv7em-none-eabi + - run: cargo check --target thumbv7em-none-eabi --release + + test: + needs: set-msrv + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - ${{needs.set-msrv.outputs.msrv}} + - stable + steps: + - uses: actions/checkout@v6 + - uses: RustCrypto/actions/cargo-cache@master + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - run: cargo test + - run: cargo test --release diff --git a/Cargo.lock b/Cargo.lock index b98642c..4a13879 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -786,6 +786,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "module-lattice" +version = "0.0.0" +dependencies = [ + "hybrid-array", + "num-traits", + "zeroize", +] + [[package]] name = "num-bigint" version = "0.4.6" diff --git a/Cargo.toml b/Cargo.toml index c01e6f8..4d326c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "dhkem", "frodo-kem", "ml-kem", + "module-lattice", "x-wing" ] diff --git a/ml-kem/LICENSE-MIT b/ml-kem/LICENSE-MIT index c4c4d9d..839f655 100644 --- a/ml-kem/LICENSE-MIT +++ b/ml-kem/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2024 RustCrypto Developers +Copyright (c) 2024-2026 RustCrypto Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/module-lattice/CHANGELOG.md b/module-lattice/CHANGELOG.md new file mode 100644 index 0000000..1d013ff --- /dev/null +++ b/module-lattice/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). diff --git a/module-lattice/Cargo.toml b/module-lattice/Cargo.toml new file mode 100644 index 0000000..00ccf93 --- /dev/null +++ b/module-lattice/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "module-lattice" +description = """ +Functionality shared between the `ml-kem` and `ml-dsa` crates, including linear algebra with degree-256 polynomials over +a prime-order field, vectors of such polynomials, and NTT polynomials / vectors, as well as packing of polynomials into +coefficients with a specified number of bits. +""" +version = "0.0.0" +edition = "2024" +rust-version = "1.85" +license = "Apache-2.0 OR MIT" +readme = "README.md" +homepage = "https://github.com/RustCrypto/KEMs/tree/master/module-lattice" +repository = "https://github.com/RustCrypto/KEMs" +categories = ["cryptography", "no-std"] +keywords = ["crypto", "kyber", "lattice", "post-quantum"] + +[dependencies] +hybrid-array = { version = "0.4", features = ["extra-sizes"] } +num-traits = { version = "0.2", default-features = false } + +# optional dependencies +zeroize = { version = "1.8.1", optional = true, default-features = false } + +[features] +zeroize = ["hybrid-array/zeroize", "dep:zeroize"] diff --git a/module-lattice/LICENSE-APACHE b/module-lattice/LICENSE-APACHE new file mode 100644 index 0000000..78173fa --- /dev/null +++ b/module-lattice/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/module-lattice/LICENSE-MIT b/module-lattice/LICENSE-MIT new file mode 100644 index 0000000..839f655 --- /dev/null +++ b/module-lattice/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2024-2026 RustCrypto Developers + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/module-lattice/README.md b/module-lattice/README.md new file mode 100644 index 0000000..07bfdda --- /dev/null +++ b/module-lattice/README.md @@ -0,0 +1,71 @@ +# [RustCrypto]: Module Lattice + +[![crate][crate-image]][crate-link] +[![Docs][docs-image]][docs-link] +[![Build Status][build-image]][build-link] +![Apache2/MIT licensed][license-image] +![Rust Version][rustc-image] +[![Project Chat][chat-image]][chat-link] +[![HAZMAT][hazmat-image]][hazmat-link] + +Functionality shared between the [`ml-kem`] and [`ml-dsa`] crates, which provide implementations of post-quantum secure +algorithms for key encapsulation and digital signatures respectively. + +## About + +The "ML" in ML-KEM and ML-DSA stands for "module lattice". This crate contains the following common functionality for +these algorithms: +- Linear algebra with degree-256 polynomials over a prime-order field, vectors of such polynomials, and NTT + polynomials / vectors. +- Packing of polynomials into coefficients with a specified number of bits. +- Utility functions such as truncating integers, flattening arrays of arrays, and unflattening arrays into arrays + of arrays. + +## ⚠️ Warning: [Hazmat!][hazmat-link] + +This crate is intended solely for the purposes of implementing the `ml-kem` and `ml-dsa` crates and should not be used +outside of that purpose. + +## Minimum Supported Rust Version (MSRV) Policy + +MSRV increases are not considered breaking changes and can happen in patch +releases. + +The crate MSRV accounts for all supported targets and crate feature +combinations, excluding explicitly unstable features. + +## License + +Licensed under either of: + +- [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) +- [MIT license](http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[//]: # (badges) + +[crate-image]: https://img.shields.io/crates/v/module-lattice?logo=rust +[crate-link]: https://crates.io/crates/module-lattice +[docs-image]: https://docs.rs/module-lattice/badge.svg +[docs-link]: https://docs.rs/module-lattice/ +[build-image]: https://github.com/RustCrypto/KEMs/actions/workflows/module-lattice.yml/badge.svg +[build-link]: https://github.com/RustCrypto/KEMs/actions/workflows/module-lattice.yml +[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg +[rustc-image]: https://img.shields.io/badge/rustc-1.85+-blue.svg +[chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg +[chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/406484-KEMs +[hazmat-image]: https://img.shields.io/badge/crypto-hazmat%E2%9A%A0-red.svg +[hazmat-link]: https://github.com/RustCrypto/meta/blob/master/HAZMAT.md + +[//]: # (links) + +[RustCrypto]: https://github.com/rustcrypto +[`ml-kem`]: https://docs.rs/ml-kem +[`ml-dsa`]: https://docs.rs/ml-dsa diff --git a/module-lattice/src/algebra.rs b/module-lattice/src/algebra.rs new file mode 100644 index 0000000..32aaa63 --- /dev/null +++ b/module-lattice/src/algebra.rs @@ -0,0 +1,444 @@ +use super::util::Truncate; + +use core::fmt::Debug; +use core::ops::{Add, Mul, Neg, Sub}; +use hybrid_array::{Array, ArraySize, typenum::U256}; +use num_traits::PrimInt; + +#[cfg(feature = "zeroize")] +use zeroize::Zeroize; + +pub trait Field: Copy + Default + Debug + PartialEq { + type Int: PrimInt + Default + Debug + From + Into + Into + Truncate; + type Long: PrimInt + From; + type LongLong: PrimInt; + + const Q: Self::Int; + const QL: Self::Long; + const QLL: Self::LongLong; + + const BARRETT_SHIFT: usize; + const BARRETT_MULTIPLIER: Self::LongLong; + + fn small_reduce(x: Self::Int) -> Self::Int; + fn barrett_reduce(x: Self::Long) -> Self::Int; +} + +/// The `define_field` macro creates a zero-sized struct and an implementation of the Field trait +/// for that struct. The caller must specify: +/// +/// * `$field`: The name of the zero-sized struct to be created +/// * `$q`: The prime number that defines the field. +/// * `$int`: The primitive integer type to be used to represent members of the field +/// * `$long`: The primitive integer type to be used to represent products of two field members. +/// This type should have roughly twice the bits of `$int`. +/// * `$longlong`: The primitive integer type to be used to represent products of three field +/// members. This type should have roughly four times the bits of `$int`. +#[macro_export] +macro_rules! define_field { + ($field:ident, $int:ty, $long:ty, $longlong:ty, $q:literal) => { + #[derive(Copy, Clone, Default, Debug, PartialEq)] + pub struct $field; + + impl Field for $field { + type Int = $int; + type Long = $long; + type LongLong = $longlong; + + const Q: Self::Int = $q; + const QL: Self::Long = $q; + const QLL: Self::LongLong = $q; + + #[allow(clippy::as_conversions)] + const BARRETT_SHIFT: usize = 2 * (Self::Q.ilog2() + 1) as usize; + #[allow(clippy::integer_division_remainder_used)] + const BARRETT_MULTIPLIER: Self::LongLong = (1 << Self::BARRETT_SHIFT) / Self::QLL; + + fn small_reduce(x: Self::Int) -> Self::Int { + if x < Self::Q { x } else { x - Self::Q } + } + + fn barrett_reduce(x: Self::Long) -> Self::Int { + let x: Self::LongLong = x.into(); + let product = x * Self::BARRETT_MULTIPLIER; + let quotient = product >> Self::BARRETT_SHIFT; + let remainder = x - quotient * Self::QLL; + Self::small_reduce(Truncate::truncate(remainder)) + } + } + }; +} + +/// An `Elem` is a member of the specified prime-order field. Elements can be added, +/// subtracted, multiplied, and negated, and the overloaded operators will ensure both that the +/// integer values remain in the field, and that the reductions are done efficiently. For +/// addition and subtraction, a simple conditional subtraction is used; for multiplication, +/// Barrett reduction. +#[derive(Copy, Clone, Default, Debug, PartialEq)] +pub struct Elem(pub F::Int); + +impl Elem { + pub const fn new(x: F::Int) -> Self { + Self(x) + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for Elem +where + F::Int: Zeroize, +{ + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +impl Neg for Elem { + type Output = Elem; + + fn neg(self) -> Elem { + Elem(F::small_reduce(F::Q - self.0)) + } +} + +impl Add> for Elem { + type Output = Elem; + + fn add(self, rhs: Elem) -> Elem { + Elem(F::small_reduce(self.0 + rhs.0)) + } +} + +impl Sub> for Elem { + type Output = Elem; + + fn sub(self, rhs: Elem) -> Elem { + Elem(F::small_reduce(self.0 + F::Q - rhs.0)) + } +} + +impl Mul> for Elem { + type Output = Elem; + + fn mul(self, rhs: Elem) -> Elem { + let lhs: F::Long = self.0.into(); + let rhs: F::Long = rhs.0.into(); + let prod = lhs * rhs; + Elem(F::barrett_reduce(prod)) + } +} + +/// A `Polynomial` is a member of the ring `R_q = Z_q[X] / (X^256)` of degree-256 polynomials +/// over the finite field with prime order `q`. Polynomials can be added, subtracted, negated, +/// and multiplied by field elements. We do not define multiplication of polynomials here. +#[derive(Clone, Default, Debug, PartialEq)] +pub struct Polynomial(pub Array, U256>); + +impl Polynomial { + pub const fn new(x: Array, U256>) -> Self { + Self(x) + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for Polynomial +where + F::Int: Zeroize, +{ + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +impl Add<&Polynomial> for &Polynomial { + type Output = Polynomial; + + fn add(self, rhs: &Polynomial) -> Polynomial { + Polynomial( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(&x, &y)| x + y) + .collect(), + ) + } +} + +impl Sub<&Polynomial> for &Polynomial { + type Output = Polynomial; + + fn sub(self, rhs: &Polynomial) -> Polynomial { + Polynomial( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(&x, &y)| x - y) + .collect(), + ) + } +} + +impl Mul<&Polynomial> for Elem { + type Output = Polynomial; + + fn mul(self, rhs: &Polynomial) -> Polynomial { + Polynomial(rhs.0.iter().map(|&x| self * x).collect()) + } +} + +impl Neg for &Polynomial { + type Output = Polynomial; + + fn neg(self) -> Polynomial { + Polynomial(self.0.iter().map(|&x| -x).collect()) + } +} + +/// A `Vector` is a vector of polynomials from `R_q` of length `K`. Vectors can be +/// added, subtracted, negated, and multiplied by field elements. +#[derive(Clone, Default, Debug, PartialEq)] +pub struct Vector(pub Array, K>); + +impl Vector { + pub const fn new(x: Array, K>) -> Self { + Self(x) + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for Vector +where + F::Int: Zeroize, +{ + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +impl Add<&Vector> for &Vector { + type Output = Vector; + + fn add(self, rhs: &Vector) -> Vector { + Vector( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(x, y)| x + y) + .collect(), + ) + } +} + +impl Sub<&Vector> for &Vector { + type Output = Vector; + + fn sub(self, rhs: &Vector) -> Vector { + Vector( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(x, y)| x - y) + .collect(), + ) + } +} + +impl Mul<&Vector> for Elem { + type Output = Vector; + + fn mul(self, rhs: &Vector) -> Vector { + Vector(rhs.0.iter().map(|x| self * x).collect()) + } +} + +impl Neg for &Vector { + type Output = Vector; + + fn neg(self) -> Vector { + Vector(self.0.iter().map(|x| -x).collect()) + } +} + +/// An `NttPolynomial` is a member of the NTT algebra `T_q = Z_q[X]^256` of 256-tuples of field +/// elements. NTT polynomials can be added and +/// subtracted, negated, and multiplied by scalars. +/// We do not define multiplication of NTT polynomials here. We also do not define the +/// mappings between normal polynomials and NTT polynomials (i.e., between `R_q` and `T_q`). +#[derive(Clone, Default, Debug, PartialEq)] +pub struct NttPolynomial(pub Array, U256>); + +impl NttPolynomial { + pub const fn new(x: Array, U256>) -> Self { + Self(x) + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for NttPolynomial +where + F::Int: Zeroize, +{ + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +impl Add<&NttPolynomial> for &NttPolynomial { + type Output = NttPolynomial; + + fn add(self, rhs: &NttPolynomial) -> NttPolynomial { + NttPolynomial( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(&x, &y)| x + y) + .collect(), + ) + } +} + +impl Sub<&NttPolynomial> for &NttPolynomial { + type Output = NttPolynomial; + + fn sub(self, rhs: &NttPolynomial) -> NttPolynomial { + NttPolynomial( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(&x, &y)| x - y) + .collect(), + ) + } +} + +impl Mul<&NttPolynomial> for Elem { + type Output = NttPolynomial; + + fn mul(self, rhs: &NttPolynomial) -> NttPolynomial { + NttPolynomial(rhs.0.iter().map(|&x| self * x).collect()) + } +} + +impl Mul<&NttPolynomial> for &NttPolynomial { + type Output = NttPolynomial; + + // Algorithm 45 MultiplyNTT + fn mul(self, rhs: &NttPolynomial) -> NttPolynomial { + NttPolynomial::new( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(&x, &y)| x * y) + .collect(), + ) + } +} + +impl Neg for &NttPolynomial { + type Output = NttPolynomial; + + fn neg(self) -> NttPolynomial { + NttPolynomial(self.0.iter().map(|&x| -x).collect()) + } +} + +/// An `NttVector` is a vector of polynomials from `T_q` of length `K`. NTT vectors can be +/// added and subtracted. If multiplication is defined for NTT polynomials, then NTT vectors +/// can be multiplied by NTT polynomials, and "multiplied" with each other to produce a dot +/// product. +#[derive(Clone, Default, Debug, PartialEq)] +pub struct NttVector(pub Array, K>); + +impl NttVector { + pub const fn new(x: Array, K>) -> Self { + Self(x) + } +} + +#[cfg(feature = "zeroize")] +impl Zeroize for NttVector +where + F::Int: Zeroize, +{ + fn zeroize(&mut self) { + self.0.zeroize(); + } +} + +impl Add<&NttVector> for &NttVector { + type Output = NttVector; + + fn add(self, rhs: &NttVector) -> NttVector { + NttVector( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(x, y)| x + y) + .collect(), + ) + } +} + +impl Sub<&NttVector> for &NttVector { + type Output = NttVector; + + fn sub(self, rhs: &NttVector) -> NttVector { + NttVector( + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(x, y)| x - y) + .collect(), + ) + } +} + +impl Mul<&NttVector> for &NttPolynomial +where + for<'a> &'a NttPolynomial: Mul<&'a NttPolynomial, Output = NttPolynomial>, +{ + type Output = NttVector; + + fn mul(self, rhs: &NttVector) -> NttVector { + NttVector(rhs.0.iter().map(|x| self * x).collect()) + } +} + +impl Mul<&NttVector> for &NttVector +where + for<'a> &'a NttPolynomial: Mul<&'a NttPolynomial, Output = NttPolynomial>, +{ + type Output = NttPolynomial; + + fn mul(self, rhs: &NttVector) -> NttPolynomial { + self.0 + .iter() + .zip(rhs.0.iter()) + .map(|(x, y)| x * y) + .fold(NttPolynomial::default(), |x, y| &x + &y) + } +} + +/// A K x L matrix of NTT-domain polynomials. Each vector represents a row of the matrix, so that +/// multiplying on the right just requires iteration. Multiplication on the right by vectors +/// is the only defined operation, and is only defined when multiplication of NTT polynomials +/// is defined. +#[derive(Clone, Default, Debug, PartialEq)] +pub struct NttMatrix(pub Array, K>); + +impl NttMatrix { + pub const fn new(x: Array, K>) -> Self { + Self(x) + } +} + +impl Mul<&NttVector> for &NttMatrix +where + for<'a> &'a NttPolynomial: Mul<&'a NttPolynomial, Output = NttPolynomial>, +{ + type Output = NttVector; + + fn mul(self, rhs: &NttVector) -> NttVector { + NttVector(self.0.iter().map(|x| x * rhs).collect()) + } +} diff --git a/module-lattice/src/encode.rs b/module-lattice/src/encode.rs new file mode 100644 index 0000000..2de525b --- /dev/null +++ b/module-lattice/src/encode.rs @@ -0,0 +1,208 @@ +use core::fmt::Debug; +use core::ops::{Div, Mul, Rem}; +use hybrid_array::{ + Array, + typenum::{Gcd, Gcf, Prod, Quot, U0, U8, U32, U256, Unsigned}, +}; +use num_traits::One; + +use super::algebra::{Elem, Field, NttPolynomial, NttVector, Polynomial, Vector}; +use super::util::{Flatten, Truncate, Unflatten}; + +/// An array length with other useful properties +pub trait ArraySize: hybrid_array::ArraySize + PartialEq + Debug {} + +impl ArraySize for T where T: hybrid_array::ArraySize + PartialEq + Debug {} + +/// An integer that can describe encoded polynomials. +pub trait EncodingSize: ArraySize { + type EncodedPolynomialSize: ArraySize; + type ValueStep: ArraySize; + type ByteStep: ArraySize; +} + +type EncodingUnit = Quot, Gcf>; + +pub type EncodedPolynomialSize = ::EncodedPolynomialSize; +pub type EncodedPolynomial = Array>; + +impl EncodingSize for D +where + D: ArraySize + Mul + Gcd + Mul, + Prod: ArraySize, + Prod: Div>, + EncodingUnit: Div + Div, + Quot, D>: ArraySize, + Quot, U8>: ArraySize, +{ + type EncodedPolynomialSize = Prod; + type ValueStep = Quot, D>; + type ByteStep = Quot, U8>; +} + +pub type DecodedValue = Array, U256>; + +/// An integer that can describe encoded vectors. +pub trait VectorEncodingSize: EncodingSize +where + K: ArraySize, +{ + type EncodedVectorSize: ArraySize; + + fn flatten(polys: Array, K>) -> EncodedVector; + fn unflatten(vec: &EncodedVector) -> Array<&EncodedPolynomial, K>; +} + +pub type EncodedVectorSize = >::EncodedVectorSize; +pub type EncodedVector = Array>; + +impl VectorEncodingSize for D +where + D: EncodingSize, + K: ArraySize, + D::EncodedPolynomialSize: Mul, + Prod: + ArraySize + Div + Rem, +{ + type EncodedVectorSize = Prod; + + fn flatten(polys: Array, K>) -> EncodedVector { + polys.flatten() + } + + fn unflatten(vec: &EncodedVector) -> Array<&EncodedPolynomial, K> { + vec.unflatten() + } +} + +// FIPS 203: Algorithm 4 ByteEncode_d +// FIPS 204: Algorithm 16 SimpleBitPack +pub fn byte_encode(vals: &DecodedValue) -> EncodedPolynomial { + let val_step = D::ValueStep::USIZE; + let byte_step = D::ByteStep::USIZE; + + let mut bytes = EncodedPolynomial::::default(); + + let vc = vals.chunks(val_step); + let bc = bytes.chunks_mut(byte_step); + for (v, b) in vc.zip(bc) { + let mut x = 0u128; + for (j, vj) in v.iter().enumerate() { + let vj: u128 = vj.0.into(); + x |= vj << (D::USIZE * j); + } + + let xb = x.to_le_bytes(); + b.copy_from_slice(&xb[..byte_step]); + } + + bytes +} + +// FIPS 203: Algorithm 5 ByteDecode_d(F) +// FIPS 204: Algorithm 18 SimpleBitUnpack +pub fn byte_decode(bytes: &EncodedPolynomial) -> DecodedValue { + let val_step = D::ValueStep::USIZE; + let byte_step = D::ByteStep::USIZE; + let mask = (F::Int::one() << D::USIZE) - F::Int::one(); + + let mut vals = DecodedValue::default(); + + let vc = vals.chunks_mut(val_step); + let bc = bytes.chunks(byte_step); + for (v, b) in vc.zip(bc) { + let mut xb = [0u8; 16]; + xb[..byte_step].copy_from_slice(b); + + let x = u128::from_le_bytes(xb); + for (j, vj) in v.iter_mut().enumerate() { + let val = F::Int::truncate(x >> (D::USIZE * j)); + vj.0 = val & mask; + + // Special case for FIPS 203 + if D::USIZE == 12 { + vj.0 = vj.0 % F::Q; + } + } + } + + vals +} + +pub trait Encode { + type EncodedSize: ArraySize; + fn encode(&self) -> Array; + fn decode(enc: &Array) -> Self; +} + +impl Encode for Polynomial { + type EncodedSize = D::EncodedPolynomialSize; + + fn encode(&self) -> Array { + byte_encode::(&self.0) + } + + fn decode(enc: &Array) -> Self { + Self(byte_decode::(enc)) + } +} + +impl Encode for Vector +where + F: Field, + K: ArraySize, + D: VectorEncodingSize, +{ + type EncodedSize = D::EncodedVectorSize; + + fn encode(&self) -> Array { + let polys = self.0.iter().map(|x| Encode::::encode(x)).collect(); + >::flatten(polys) + } + + fn decode(enc: &Array) -> Self { + let unfold = >::unflatten(enc); + Self( + unfold + .iter() + .map(|&x| as Encode>::decode(x)) + .collect(), + ) + } +} + +impl Encode for NttPolynomial { + type EncodedSize = D::EncodedPolynomialSize; + + fn encode(&self) -> Array { + byte_encode::(&self.0) + } + + fn decode(enc: &Array) -> Self { + Self(byte_decode::(enc)) + } +} + +impl Encode for NttVector +where + F: Field, + D: VectorEncodingSize, + K: ArraySize, +{ + type EncodedSize = D::EncodedVectorSize; + + fn encode(&self) -> Array { + let polys = self.0.iter().map(|x| Encode::::encode(x)).collect(); + >::flatten(polys) + } + + fn decode(enc: &Array) -> Self { + let unfold = >::unflatten(enc); + Self( + unfold + .iter() + .map(|&x| as Encode>::decode(x)) + .collect(), + ) + } +} diff --git a/module-lattice/src/lib.rs b/module-lattice/src/lib.rs new file mode 100644 index 0000000..874d1e1 --- /dev/null +++ b/module-lattice/src/lib.rs @@ -0,0 +1,27 @@ +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/meta/master/logo.svg" +)] +//#![deny(missing_docs)] // TODO: Require all public interfaces to be documented +#![warn(clippy::pedantic)] // Be pedantic by default +#![warn(clippy::integer_division_remainder_used)] // Be judicious about using `/` and `%` + +// XXX(RLB) There are no unit tests in this crate right now, because the algebra and encode/decode +// routines all require a field, and the concrete field definitions are down in the dependent +// modules. Maybe we should pull the field definitions up into this module so that we can verify +// that everything works. That might also let us make private some of the tools used to build +// things up. + +/// Linear algebra with degree-256 polynomials over a prime-order field, vectors of such +/// polynomials, and NTT polynomials / vectors +pub mod algebra; + +/// Packing of polynomials into coefficients with a specified number of bits. +pub mod encode; + +/// Utility functions such as truncating integers, flattening arrays of arrays, and unflattening +/// arrays into arrays of arrays. +pub mod util; diff --git a/module-lattice/src/util.rs b/module-lattice/src/util.rs new file mode 100644 index 0000000..89d5fac --- /dev/null +++ b/module-lattice/src/util.rs @@ -0,0 +1,154 @@ +use core::mem::ManuallyDrop; +use core::ops::{Div, Mul, Rem}; +use core::ptr; +use hybrid_array::{ + Array, ArraySize, + typenum::{Prod, Quot, U0, Unsigned}, +}; + +/// Safely truncate an unsigned integer value to shorter representation +pub trait Truncate { + fn truncate(x: T) -> Self; +} + +macro_rules! define_truncate { + ($from:ident, $to:ident) => { + impl Truncate<$from> for $to { + fn truncate(x: $from) -> $to { + // This line is marked unsafe because the `unwrap_unchecked` call is UB when its + // `self` argument is `Err`. It never will be, because we explicitly zeroize the + // high-order bits before converting. We could have used `unwrap()`, but chose to + // avoid the possibility of panic. + unsafe { (x & $from::from($to::MAX)).try_into().unwrap_unchecked() } + } + } + }; +} + +define_truncate!(u128, u32); +define_truncate!(u64, u32); +define_truncate!(usize, u8); +define_truncate!(usize, u16); + +/// Defines a sequence of sequences that can be merged into a bigger overall seequence +pub(crate) trait Flatten { + type OutputSize: ArraySize; + + fn flatten(self) -> Array; +} + +impl Flatten> for Array, N> +where + N: ArraySize, + M: ArraySize + Mul, + Prod: ArraySize, +{ + type OutputSize = Prod; + + // This is the reverse transmute between [T; K*N] and [[T; K], M], which is guaranteed to be + // safe by the Rust memory layout of these types. + fn flatten(self) -> Array { + let whole = ManuallyDrop::new(self); + unsafe { ptr::read(whole.as_ptr().cast()) } + } +} + +/// Defines a sequence that can be split into a sequence of smaller sequences of uniform size +pub(crate) trait Unflatten +where + M: ArraySize, +{ + type Part; + + fn unflatten(self) -> Array; +} + +impl Unflatten for Array +where + T: Default, + N: ArraySize + Div + Rem, + M: ArraySize, + Quot: ArraySize, +{ + type Part = Array>; + + // This requires some unsafeness, but it is the same as what is done in Array::split. + // Basically, this is doing transmute between [T; K*N] and [[T; K], M], which is guaranteed to + // be safe by the Rust memory layout of these types. + fn unflatten(self) -> Array { + let part_size = Quot::::USIZE; + let whole = ManuallyDrop::new(self); + Array::from_fn(|i| unsafe { ptr::read(whole.as_ptr().add(i * part_size).cast()) }) + } +} + +impl<'a, T, N, M> Unflatten for &'a Array +where + T: Default, + N: ArraySize + Div + Rem, + M: ArraySize, + Quot: ArraySize, +{ + type Part = &'a Array>; + + // This requires some unsafeness, but it is the same as what is done in Array::split. + // Basically, this is doing transmute between [T; K*N] and [[T; K], M], which is guaranteed to + // be safe by the Rust memory layout of these types. + fn unflatten(self) -> Array { + let part_size = Quot::::USIZE; + let mut ptr: *const T = self.as_ptr(); + Array::from_fn(|_i| unsafe { + let part = &*(ptr.cast()); + ptr = ptr.add(part_size); + part + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use hybrid_array::{ + Array, + typenum::{U2, U5}, + }; + + #[test] + fn flatten() { + let flat: Array = Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + let unflat2: Array, _> = Array([ + Array([1, 2]), + Array([3, 4]), + Array([5, 6]), + Array([7, 8]), + Array([9, 10]), + ]); + let unflat5: Array, _> = + Array([Array([1, 2, 3, 4, 5]), Array([6, 7, 8, 9, 10])]); + + // Flatten + let actual = unflat2.flatten(); + assert_eq!(flat, actual); + + let actual = unflat5.flatten(); + assert_eq!(flat, actual); + + // Unflatten + let actual: Array, U5> = flat.unflatten(); + assert_eq!(unflat2, actual); + + let actual: Array, U2> = flat.unflatten(); + assert_eq!(unflat5, actual); + + // Unflatten on references + let actual: Array<&Array, U5> = (&flat).unflatten(); + for (i, part) in actual.iter().enumerate() { + assert_eq!(&unflat2[i], *part); + } + + let actual: Array<&Array, U2> = (&flat).unflatten(); + for (i, part) in actual.iter().enumerate() { + assert_eq!(&unflat5[i], *part); + } + } +}