diff --git a/.github/workflows/workspace.yml b/.github/workflows/workspace.yml index f171d1e..bd3cd5d 100644 --- a/.github/workflows/workspace.yml +++ b/.github/workflows/workspace.yml @@ -21,7 +21,7 @@ jobs: timeout-minutes: 45 steps: - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@1.79 + - uses: dtolnay/rust-toolchain@1.81 with: components: clippy - run: cargo clippy -- -D warnings diff --git a/.github/workflows/x-wing.yml b/.github/workflows/x-wing.yml new file mode 100644 index 0000000..11a38f3 --- /dev/null +++ b/.github/workflows/x-wing.yml @@ -0,0 +1,90 @@ +name: ml-kem + +on: + pull_request: + paths: + - ".github/workflows/x-wing.yml" + - "ml-kem/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: x-wing + +env: + RUSTFLAGS: "-Dwarnings" + CARGO_INCREMENTAL: 0 + +jobs: + set-msrv: + uses: RustCrypto/actions/.github/workflows/set-msrv.yml@master + with: + msrv: 1.81.0 + + minimal-versions: + # temporarily disabled as requested by Tony (https://github.com/RustCrypto/KEMs/pull/15#pullrequestreview-2006378802) + if: false + uses: RustCrypto/actions/.github/workflows/minimal-versions.yml@master + with: + working-directory: ${{ github.workflow }} + + no_std: + needs: set-msrv + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - ${{needs.set-msrv.outputs.msrv}} + - stable + target: + - thumbv7em-none-eabi + - wasm32-unknown-unknown + steps: + - uses: actions/checkout@v4 + - uses: RustCrypto/actions/cargo-cache@master + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + targets: ${{ matrix.target }} + - run: cargo build --no-default-features --target ${{ matrix.target }} + + test: + needs: set-msrv + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - ${{needs.set-msrv.outputs.msrv}} + - stable + steps: + - uses: actions/checkout@v4 + - uses: RustCrypto/actions/cargo-cache@master + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + - run: cargo build --benches + - run: cargo build --benches --all-features + - run: cargo test --no-default-features + - run: cargo test + - run: cargo test --all-features + + cross: + needs: set-msrv + strategy: + matrix: + include: + - target: powerpc-unknown-linux-gnu + rust: ${{needs.set-msrv.outputs.msrv}} + - target: powerpc-unknown-linux-gnu + rust: stable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + targets: ${{ matrix.target }} + - uses: RustCrypto/actions/cross-install@master + - run: cross test --release --target ${{ matrix.target }} --all-features diff --git a/Cargo.lock b/Cargo.lock index af7301d..3788f1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,7 @@ dependencies = [ "platforms", "rustc_version", "subtle", + "zeroize", ] [[package]] @@ -518,6 +519,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ml-kem" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97befee0c869cb56f3118f49d0f9bb68c9e3f380dec23c1100aedc4ec3ba239a" +dependencies = [ + "hybrid-array", + "kem", + "rand_core", + "sha3", +] + [[package]] name = "num-bigint" version = "0.4.5" @@ -1064,6 +1077,21 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "x-wing" +version = "0.1.0" +dependencies = [ + "hex", + "hex-literal", + "ml-kem 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand", + "rand_core", + "serde", + "serde_json", + "sha3", + "x25519-dalek", +] + [[package]] name = "x25519-dalek" version = "2.0.1" @@ -1072,6 +1100,8 @@ checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" dependencies = [ "curve25519-dalek", "rand_core", + "serde", + "zeroize", ] [[package]] @@ -1079,3 +1109,17 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index bd8b8df..4333dcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,6 @@ [workspace] resolver = "2" -members = [ - "dhkem", - "ml-kem", -] +members = ["dhkem", "ml-kem", "x-wing"] [profile.bench] debug = true diff --git a/README.md b/README.md index f64ce22..3ba0f42 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ commonly used in transport encryption protocols (e.g. [TLS]) and hybrid cryptosy |----------------------|---------------------------------------------------------------------------------------------|------------------------------------------------------------------------------|--------------------| | [`dhkem`](./dhkem) | [![crates.io](https://img.shields.io/crates/v/dhkem.svg)](https://crates.io/crates/dhkem) | [![Documentation](https://docs.rs/dhkem/badge.svg)](https://docs.rs/dhkem) | Diffie-Hellman KEM | | [`ml-kem`](./ml-kem) | [![crates.io](https://img.shields.io/crates/v/ml-kem.svg)](https://crates.io/crates/ml-kem) | [![Documentation](https://docs.rs/ml-kem/badge.svg)](https://docs.rs/ml-kem) | Module Lattice KEM | +| [`x-wing`](./x-wing) | [![crates.io](https://img.shields.io/crates/v/x-wing.svg)](https://crates.io/crates/x-wing) | [![Documentation](https://docs.rs/x-wing/badge.svg)](https://docs.rs/x-wing) | Hybrid PQ KEM | ## License diff --git a/x-wing/Cargo.toml b/x-wing/Cargo.toml new file mode 100644 index 0000000..30a388d --- /dev/null +++ b/x-wing/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "x-wing" +description = """ +Pure Rust implementation of the X-Wing Key-Encapsulation Mechanism +""" +version = "0.1.0" +edition = "2021" +rust-version = "1.81" +license = "Apache-2.0 OR MIT" +readme = "README.md" +homepage = "https://github.com/RustCrypto/KEMs/tree/master/ml-kem" +repository = "https://github.com/RustCrypto/KEMs" +categories = ["cryptography", "no-std"] +keywords = ["crypto", "x-wing", "xwing", "kem", "post-quantum"] + +[dependencies] +rand_core = { version = "0.6.4", default-features = false } +x25519-dalek = { version = "2.0", features = [ + "static_secrets", + "reusable_secrets", +] } +ml-kem = { version = "*", features = ["deterministic"] } +sha3 = { version = "0.10", default-features = false } + +[dev-dependencies] +rand_core = { version = "0.6.4" } +hex-literal = "0.4" +hex = { version = "0.4.3", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +rand = "0.8" + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/x-wing/LICENSE-APACHE b/x-wing/LICENSE-APACHE new file mode 100644 index 0000000..5e45977 --- /dev/null +++ b/x-wing/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 RustCrypto + +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/x-wing/LICENSE-MIT b/x-wing/LICENSE-MIT new file mode 100644 index 0000000..c4c4d9d --- /dev/null +++ b/x-wing/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) 2024 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/x-wing/README.md b/x-wing/README.md new file mode 100644 index 0000000..3b112fe --- /dev/null +++ b/x-wing/README.md @@ -0,0 +1,67 @@ +# [RustCrypto]: ML-KEM + +[![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] + +Pure Rust implementation of X-Wing, a general-purpose post-quantum/traditional +hybrid key encapsulation mechanism (PQ/T KEM) built on X25519 and ML-KEM-768. Using the crates [x25519_dalek][X25519-DALEK] and [ml-kem][ML-KEM]. + +Current implementation matches the [draft RFC][RFC-DRAFT] version 04. + +[Documentation][docs-link] + +## About + + +## ⚠️ Security Warning + +The implementation contained in this crate has never been independently audited! + +USE AT YOUR OWN RISK! + +## Minimum Supported Rust Version + +This crate requires **Rust 1.81** at a minimum. + +We may change the MSRV in the future, but it will be accompanied by a minor +version bump. + +## 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/x-wing +[crate-link]: https://crates.io/crates/x-wing +[docs-image]: https://docs.rs/x-wing/badge.svg +[docs-link]: https://docs.rs/x-wing/ +[build-image]: https://github.com/RustCrypto/KEMs/actions/workflows/x-wing.yml/badge.svg +[build-link]: https://github.com/RustCrypto/KEMs/actions/workflows/x-wing.yml +[license-image]: https://img.shields.io/badge/license-Apache2.0/MIT-blue.svg +[rustc-image]: https://img.shields.io/badge/rustc-1.81+-blue.svg +[chat-image]: https://img.shields.io/badge/zulip-join_chat-blue.svg +[chat-link]: https://rustcrypto.zulipchat.com/#narrow/stream/406484-KEMs + +[//]: # (links) + +[RustCrypto]: https://github.com/rustcrypto +[RFC-DRAFT]: https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem +[X-WING-PAPER]: https://eprint.iacr.org/2024/039.pdf +[X25519-DALEK]: https://crates.io/crates/x25519-dalek +[ML-KEM]: https://crates.io/crates/ml-kem diff --git a/x-wing/rustfmt.toml b/x-wing/rustfmt.toml new file mode 100644 index 0000000..0660b41 --- /dev/null +++ b/x-wing/rustfmt.toml @@ -0,0 +1,8 @@ +# Take a preview on the style of https://rust-lang.github.io/rfcs/3309-style-team.html + +# The default setting now is to order the imports blocks alphabetically. +# The setting will combine the blocks, split them in std, external and crate and order them alphabetically. +group_imports = "StdExternalCrate" + +# Combine the import per module inplace of the default of do nothing. +imports_granularity = "Module" diff --git a/x-wing/src/lib.rs b/x-wing/src/lib.rs new file mode 100644 index 0000000..83c5ad4 --- /dev/null +++ b/x-wing/src/lib.rs @@ -0,0 +1,374 @@ +#![cfg_attr(not(test), no_std)] +#![cfg_attr(docsrs, feature(doc_auto_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" +)] +#![warn(clippy::pedantic)] // Be pedantic by default +#![allow(clippy::similar_names)] // So we can use the names as in the RFC +#![deny(missing_docs)] // Require all public interfaces to be documented + +//! # Usage +//! +//! This crate implements the X-Wing Key Encapsulation Method (X-Wing-KEM) algorithm. +//! X-Wing-KEM is a KEM in the sense that it creates an (decapsulation key, encapsulation key) pair, +//! such that anyone can use the encapsulation key to establish a shared key with the holder of the +//! decapsulation key. X-Wing-KEM is a general-purpose hybrid post-quantum KEM, combining x25519 and ML-KEM-768. +//! +//! ``` +//! # use rand; +//! let mut rng = rand::thread_rng(); +//! +//! let (sk, pk) = x_wing::generate_key_pair(&mut rng); +//! +//! let (ss_sender, ct) = pk.encapsulate(&mut rng); +//! +//! let ss_receiver = sk.decapsulate(&ct); +//! +//! assert_eq!(ss_sender, ss_receiver); +//! ``` +//! +//! [X-Wing draft]: https://datatracker.ietf.org/doc/html/draft-connolly-cfrg-xwing-kem + +use array::Array; +use kem::Kem; +use ml_kem::kem::{Decapsulate, Encapsulate}; +use ml_kem::{array, kem, EncodedSizeUser, KemCore, MlKem768, MlKem768Params}; +use rand_core::CryptoRngCore; +use sha3::digest::core_api::XofReaderCoreWrapper; +use sha3::digest::{ExtendableOutput, XofReader}; +use sha3::{Sha3_256, Shake128, Shake128ReaderCore}; +use x25519_dalek::{x25519, X25519_BASEPOINT_BYTES}; + +type MlKem768DecapsulationKey = as ml_kem::KemCore>::DecapsulationKey; +type MlKem768EncapsulationKey = as ml_kem::KemCore>::EncapsulationKey; + +const X_WING_LABEL: &[u8; 6] = br"\.//^\"; + +const PUBLIC_KEY_SIZE: usize = 1216; + +// The naming convention of variables matches the RFC. +// ss -> Shared Secret +// ct -> Cipher Text +// ek -> Ephemeral Key +// pk -> Public Key +// sk -> Secret Key +// Postfixes: +// _m -> ML-Kem related key +// _x -> x25519 related key + +/// X-Wing encapsulation or public key +#[derive(Clone, PartialEq)] +pub struct EncapsulationKey { + pk_m: MlKem768EncapsulationKey, + pk_x: x25519_dalek::PublicKey, +} + +/// The X-Wing encapsulation or public key +impl EncapsulationKey { + /// Generates the shared secret and the `Chipertext` + #[allow(clippy::missing_panics_doc)] + pub fn encapsulate(&self, rng: &mut impl CryptoRngCore) -> ([u8; 32], Ciphertext) { + // Swapped order of operations, so that used of the rng matches the usage of the eseed + let (ct_m, ss_m) = self.pk_m.encapsulate(rng).expect("Operation is Infallible"); + + // TODO: Check if we can use the `EphemeralSecret` API + let ek_x: [u8; 32] = generate(rng); + let ct_x = x25519(ek_x, X25519_BASEPOINT_BYTES); + let ss_x = x25519(ek_x, self.pk_x.to_bytes()); + + let ss = combiner(&ss_m.into(), &ss_x, &ct_x, &self.pk_x); + + let ct = Ciphertext { + ct_m: ct_m.into(), + ct_x, + }; + + (ss, ct) + } + + /// Covert the key to the following format: + /// ML-KEM-768 public key(1184 bytes) | X25519 public key(32 bytes) + #[must_use] + pub fn as_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] { + let mut buffer = [0u8; PUBLIC_KEY_SIZE]; + buffer[0..1184].copy_from_slice(&self.pk_m.as_bytes()); + buffer[1184..1216].copy_from_slice(self.pk_x.as_bytes()); + buffer + } +} + +impl From<&[u8; PUBLIC_KEY_SIZE]> for EncapsulationKey { + fn from(value: &[u8; PUBLIC_KEY_SIZE]) -> Self { + let mut pk_m = [0; 1184]; + pk_m.copy_from_slice(&value[0..1184]); + let pk_m = MlKem768EncapsulationKey::from_bytes(&pk_m.into()); + + let mut pk_x = [0; 32]; + pk_x.copy_from_slice(&value[1184..]); + let pk_x = x25519_dalek::PublicKey::from(pk_x); + EncapsulationKey { pk_m, pk_x } + } +} + +const PRIVATE_KEY_SIZE: usize = 32; + +/// X-Wing decapsulation key or private key +#[derive(Clone, PartialEq, Eq)] +pub struct DecapsulationKey { + sk: [u8; PRIVATE_KEY_SIZE], +} + +impl DecapsulationKey { + /// Generate a new `DecapsulationKey` using the provided RNG + pub fn generate(rng: &mut impl CryptoRngCore) -> DecapsulationKey { + let sk = generate(rng); + DecapsulationKey { sk } + } + + /// Provide the matching `EncapsulationKey` + #[must_use] + pub fn encapsulation_key(&self) -> EncapsulationKey { + let (_sk_m, _sk_x, pk_m, pk_x) = self.expand_key(); + EncapsulationKey { pk_m, pk_x } + } + + /// Decapsulate the given ciphertext and recover the shared secret + #[must_use] + #[allow(clippy::missing_panics_doc)] + pub fn decapsulate(&self, ct: &Ciphertext) -> [u8; 32] { + let (sk_m, sk_x, _pk_m, pk_x) = self.expand_key(); + + let ss_m = sk_m + .decapsulate(&Array::from(ct.ct_m)) + .expect("Operation is infallible"); + + let ss_x = x25519(sk_x.to_bytes(), ct.ct_x); + + combiner(&ss_m.into(), &ss_x, &ct.ct_x, &pk_x) + } + + fn expand_key( + &self, + ) -> ( + MlKem768DecapsulationKey, + x25519_dalek::StaticSecret, + MlKem768EncapsulationKey, + x25519_dalek::PublicKey, + ) { + use sha3::digest::Update; + let mut shaker = Shake128::default(); + shaker.update(&self.sk); + let mut expanded = shaker.finalize_xof(); + + let d = read_from(&mut expanded); + let z = read_from(&mut expanded); + + let (sk_m, pk_m) = MlKem768::generate_deterministic(&d.into(), &z.into()); + let sk_x = read_from(&mut expanded); + + let sk_x = x25519_dalek::StaticSecret::from(sk_x); + let pk_x = x25519_dalek::PublicKey::from(&sk_x); + + (sk_m, sk_x, pk_m, pk_x) + } + + /// Private key + #[must_use] + pub fn as_bytes(&self) -> &[u8; PRIVATE_KEY_SIZE] { + &self.sk + } +} + +impl From<[u8; PRIVATE_KEY_SIZE]> for DecapsulationKey { + fn from(sk: [u8; PRIVATE_KEY_SIZE]) -> Self { + DecapsulationKey { sk } + } +} + +fn read_from(reader: &mut XofReaderCoreWrapper) -> [u8; N] { + let mut data = [0; N]; + reader.read(&mut data); + data +} + +fn generate(rng: &mut impl CryptoRngCore) -> [u8; N] { + let mut random = [0; N]; + rng.fill_bytes(&mut random); + random +} + +/// X-Wing ciphertext +#[derive(Clone, PartialEq, Eq)] +pub struct Ciphertext { + ct_m: [u8; 1088], + ct_x: [u8; 32], +} + +impl Ciphertext { + /// Covert the ciphertext to the following format: + /// ML-KEM-768 ciphertext(1088 bytes) | X25519 ciphertext(32 bytes) + #[must_use] + pub fn as_bytes(&self) -> [u8; 1120] { + let mut buffer = [0; 1120]; + buffer[0..1088].copy_from_slice(&self.ct_m); + buffer[1088..].copy_from_slice(&self.ct_x); + buffer + } +} + +impl From<&[u8; 1120]> for Ciphertext { + fn from(value: &[u8; 1120]) -> Self { + let mut ct_m = [0; 1088]; + ct_m.copy_from_slice(&value[0..1088]); + let mut ct_x = [0; 32]; + ct_x.copy_from_slice(&value[1088..]); + + Ciphertext { ct_m, ct_x } + } +} + +/// Generate a X-Wing key pair +pub fn generate_key_pair(rng: &mut impl CryptoRngCore) -> (DecapsulationKey, EncapsulationKey) { + let sk = DecapsulationKey::generate(rng); + let pk = sk.encapsulation_key(); + (sk, pk) +} + +fn combiner( + ss_m: &[u8; 32], + ss_x: &[u8; 32], + ct_x: &[u8; 32], + pk_x: &x25519_dalek::PublicKey, +) -> [u8; 32] { + use sha3::Digest; + + let mut hasher = Sha3_256::new(); + hasher.update(X_WING_LABEL); + hasher.update(ss_m); + hasher.update(ss_x); + hasher.update(ct_x); + hasher.update(pk_x.as_bytes()); + hasher.finalize().into() +} + +#[cfg(test)] +mod tests { + use rand_core::{impls, CryptoRng, OsRng, RngCore}; + use serde::Deserialize; + + use super::*; + + struct SeedRng { + seed: Vec, + } + + impl SeedRng { + fn new(seed: Vec) -> SeedRng { + SeedRng { seed } + } + } + + impl RngCore for SeedRng { + fn next_u32(&mut self) -> u32 { + impls::next_u32_via_fill(self) + } + + fn next_u64(&mut self) -> u64 { + impls::next_u64_via_fill(self) + } + + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.try_fill_bytes(dest).unwrap() + } + + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + dest.copy_from_slice(&self.seed[0..dest.len()]); + self.seed.drain(0..dest.len()); + Ok(()) + } + } + + #[derive(Deserialize)] + struct TestVector { + #[serde(deserialize_with = "hex::serde::deserialize")] + seed: Vec, + + #[serde(deserialize_with = "hex::serde::deserialize")] + eseed: Vec, + + #[serde(deserialize_with = "hex::serde::deserialize")] + ss: [u8; 32], + + #[serde(deserialize_with = "hex::serde::deserialize")] + sk: [u8; 32], + + #[serde(deserialize_with = "hex::serde::deserialize")] + pk: Vec, //[u8; PUBLIC_KEY_SIZE], + + #[serde(deserialize_with = "hex::serde::deserialize")] + ct: Vec, //[u8; 1120], + } + + impl CryptoRng for SeedRng {} + + /// Test with test vectors from: https://github.com/dconnolly/draft-connolly-cfrg-xwing-kem/blob/main/spec/test-vectors.json + #[test] + fn rfc_test_vectors() { + let test_vectors = + serde_json::from_str::>(include_str!("test-vectors.json")).unwrap(); + + for test_vector in test_vectors { + run_test(test_vector) + } + } + + fn run_test(test_vector: TestVector) { + let mut seed = SeedRng::new(test_vector.seed); + let (sk, pk) = generate_key_pair(&mut seed); + + assert_eq!(sk.as_bytes().to_vec(), test_vector.sk); + assert_eq!(pk.as_bytes().to_vec(), test_vector.pk); + + let mut eseed = SeedRng::new(test_vector.eseed); + let (ss, ct) = pk.encapsulate(&mut eseed); + + assert_eq!(ss, test_vector.ss); + assert_eq!(ct.as_bytes().to_vec(), test_vector.ct); + + let ss = sk.decapsulate(&ct); + assert_eq!(ss, test_vector.ss); + } + + #[test] + fn ciphertext_serialize() { + let mut rng = OsRng; + + let ct_a = Ciphertext { + ct_m: generate(&mut rng), + ct_x: generate(&mut rng), + }; + + let bytes = ct_a.as_bytes(); + + let ct_b = Ciphertext::from(&bytes); + + assert!(ct_a == ct_b); + } + + #[test] + fn key_serialize() { + let sk = DecapsulationKey::generate(&mut OsRng); + let pk = sk.encapsulation_key(); + + let sk_bytes = sk.as_bytes(); + let pk_bytes = pk.as_bytes(); + + let sk_b = DecapsulationKey::from(sk_bytes.clone()); + let pk_b = EncapsulationKey::from(&pk_bytes.clone()); + + assert!(sk == sk_b); + assert!(pk == pk_b); + } +} diff --git a/x-wing/src/test-vectors.json b/x-wing/src/test-vectors.json new file mode 100644 index 0000000..1c3735a --- /dev/null +++ b/x-wing/src/test-vectors.json @@ -0,0 +1,26 @@ +[ + { + "seed": "7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26", + "eseed": "3cb1eea988004b93103cfb0aeefd2a686e01fa4a58e8a3639ca8a1e3f9ae57e235b8cc873c23dc62b8d260169afa2f75ab916a58d974918835d25e6a435085b2", + "ss": "555a071a8b7520ae95f8e635de8a5f87dbddcbef900576aad29ecdda5459c15a", + "sk": "7f9c2ba4e88f827d616045507605853ed73b8093f6efbc88eb1a6eacfa66ef26", + "pk": "ec367a5c586f3817a98698b5fc4082907a8873909a7e79ce5d18c84d425362c7956aeb610b9b68949107335ba676142aadd93ed27211c57c2d319c805a01204f5c2948158759f06327825379ac8428113e59c215a582a50d0c89505390ecf868e9270cf96c4fa0f94793b5998b38a1e3c9039bf4147695c08413a7e190b5d9a8c22a4759bda51fd11064a21c4dadc3bc28510697f3a442205214a6cd3f54a9ba45a309437d372654e2c6226e8640810128597abe9042932be6af1eb71a6ef156a9baa4c0c05764a8314fc1565d1825a5eb3604f278bc175b0666af85a13d97c650a571564eca080a36727bf76460c81a842895e87c9d4fc9c57fc6b149692eed526fb632cd476232a9f3035b4c96d6a14f8cf92e2735a766c7a168e6034369b6c17750afcc483af5654b82439f6b9a136cb4f47986dab4c427327675061d7b130572e2071f22339a997cf1e1618133ac8b8acd1d7177943c0d1971c84fc48cce7c4c00b95a9f77414c4c07fb3b0c6d51144d36cc8be4ae9b236f89accdd4336bcff11f4fc997ef13c01bb45d4001b1949749ebf14e469788ebdbbeced68ba149ca81aab111d0756f1074b7e60031da437709027c4676edc35318a74b1308a8f2b6aef905668bb031a6403ab7a328ba74b9231866e287424b42acd1d69b6eab657f2340f433717e581a048ac9be5196fedc36ec212de48149bbec9e07ccc8b1f50293e78e469079a3d3588ae146c1859ced376dc13040c4535f253cb40a61b8be95b8b6606d2f607c1035a23566ade289391829ae61cacd36d247a3a864bab43b23198481f10f9a5b25b64cb6314baaa0282c59792fe987687b06cb23b397302962cacb9f7327301310c7e66b9f5aab93b0f9ba9b5633a1db72fa637c4f6611ca9117788bb335b80dd0c989af6b0d8fc9b5c3707a1d848b220a3002b612c294a004c4b52ad1b4b57619d960a659646622a73de9a55de1191dcf8253b50bb2d6e0bed3ab12c4bb81b2826afec87dabccb56b74bdd4c844005097ac94cafea715a57b6e20b49e49869bfdc8015e37a0b3f942f9467b7c749f76c951623340660bbd88c16dfbf5176ca855689bbf7287391935b71eda6ef8bab6a2ea6e3095a1f2719d10b205130982942c1bbad0bb6c1901879587ac3a290ff20043010e181337eb2a20eda44b24e07f12255bbe78279adc51de276d2e602b72dc1ed7489240ab2c4e672b527082e363b0b5f51ffbbb79d724435484ca0c7874aff654d61a254eb7ae420b4d0a9958a48144e013972cda7f8adcc7c36206725221a79426e7c798e99cb645198c506194c3da36415501ea6bccb377921f0172cf9634232b211d626074020cdec29c4d59248c405688f15d6bc556f72bb01d11ae0b2167d33bb2389a2d6dec911a3513fc680d21a265c3f3b190e983d5bab1ae471802024edfd96a2cd51176261107c29f5050ab52ca7210db8668bb80064744cb4236e3ac6df26477c8d80ac9a60ca8796f95c5acd960b2f541027c2378ac15708070acfa528a8473248458cb3cf23108949369009b523a945fc70cf3c3add61c4fbbdba91d74c954682182d30071e71648f1b266ea343ab97547c9a3462969ca911a67667e1cb88467942eea1ae5d06ac215e64de876fda67c22f74ffe26ff8b56cf606ff799d4a89bb6cee3f79506960abcda4e65d8197e0c992244dae91c21068915647f844f49", + "ct": "b45085dc0c2abecd811415924ade853ae88c8dcf8007e6d79bae036648290472989d6f2187bc6d39d0f739d315fc03cd8a373ad8927b0db7d419385c9b867b351815a95e7f0f915e7356eacce50d328a572565c538b282dc539e4d4b106ba5add0656efb8bd670a32e89fb642eae8235fdc181b2a3ae21d5f3374ce6955484c4fa9dd0a8e454f73e840fa5085070d10789e3cc1f6b4274fad17c041c23a8c512e3be23962de5028f427273f5a53dcf43425e9183d304abf22b306fb6add4c89a7b54fa93d50393882bad23e06c58c03cbb765a9d1324be9fe7b399b7a0f7486b8b03fe186dc5e9ee9738f48e7ef3127a6db992097263dbc51fb227dfab0aae2758d8cfd8573c227e19d245503518ee7f533976236075d50f95b5bd101c670714209f264c01e31b80295fea54f42e1c62856042bafbe72e1ef8abe12f58b02e4eb6378bc0e13339395b6faf95e2738c509975bc1806d1cbad3e586cfa2ba09b2bde20dfb0aaba2cdb583ae33c812109a1095adc697befcbd0be0aafee1e41979be026747c918646d38874320aaf404f28cda6d6d7a7a5386f487983a69064b8bc1fc0a2998a55bb442cfa9b61581263b33f5ae25c4a1efdd890c3fae4481995eaabf1d4a27addc239b99bb8aefec73a9f9c15819026d35d48e11de426f7f113e8fe843db011934c8052300cca9fc870f390648ab47ff543629949c5459fae763871e949a4d2f61caf9f6afcfbc00e5b71f85c791ae04d4db90ed09811382a8a2a9707f76cbeaa371eb64d2a8d82e1f65b42e0928e5afa288062ca0b28317c9b36b27f14161d84d71db377efc6f0f2d7b57594e8fc432c2dbcbc4f55fc3563894a5be4ad40a2aa34ca48db0df5b6d8ae51777bf7c6925a40e651629351e86480594f438ee3a34daa7a2581e0f573489e71b23bf76dcf8fd3d9c29ca6bcc699753d54b876adb0c0514ae887e1029ef195fc3cddb51d03cb518f8dad5044e2299f601b961fa38da47d1e940b58e864cf5dbe85a21dafc40b2355144307d09bd2bf8b1c762e7bd5e27308d903e165ecc6176b74564329bf37e1ce9257d113897c0099aaa17937735dd13931c5742f5cceaec475c1886bfef42252a7ad66f4d4b925faec8e1a9ce0623a895e9c00c57781e66404311720bb94ff0c019081f9b846d72451179308f17d4c7ac324a5bbbb914411840364b9b65f6e189c60ef842c155df1f96b84f03521803d3cb7016629b4c8159fb0ad3ce1da5e49ceba56f6881be8432200c86e291a4cd3b5ea9001e99b418b9d44a3fa0cedb6acf3feef30df4307480967e765530d6183add3a198d796a4535abbd8be92d8c2f9ec4217fd459326f0f090764b57207d4cb108af34abf120c182011e66393edf2f446f606acb5b0ad5afb4ea5866e4d4158280885bd0ad4deced058ced8035afc85d1e03c00b7c23b4e74abe8ba12b86a027064bf88443aadb38c82bc621b6880d3e88f6c3bcb03a015d1cc306f7d575ee778cd1b52902be555b4e02b74cfd310bd83ab4c81f97fc12e56f17576740ce2a32fc5145030145cfb97e63e0e41d354274a079d3e6fb2e15" + }, + { + "seed": "badfd6dfaac359a5efbb7bcc4b59d538df9a04302e10c8bc1cbf1a0b3a5120ea", + "eseed": "17cda7cfad765f5623474d368ccca8af0007cd9f5e4c849f167a580b14aabdefaee7eef47cb0fca9767be1fda69419dfb927e9df07348b196691abaeb580b32d", + "ss": "d99c3cd6cde624a73b2f80d9be695c5ab804a42fcca392bc2fd8504b81b2bf6e", + "sk": "badfd6dfaac359a5efbb7bcc4b59d538df9a04302e10c8bc1cbf1a0b3a5120ea", + "pk": "8b92541ff9b60f49baf1282229853e3249427656bda5440c6f258df3db3382c2b5674590fd713397954b785694807938653603666b103ea8709b002a420a7bee64161015631c6c32e7c6a53d525eb44940733667447439d830280e7ba75f501958a09ce8e3b7d0038a7bbc22e7025700686662f128ce0771d92a8c8af4cf2a414c11b19c695b234bb447471c67aa5a94bad89e192563bb33662fda91d24845f0e3b08eac79d03635fe81610e80bb8bcb92fc05cc5a12c7d7a5340a81c031d55a6b81b7469ca14c72cf2634b686cac7060a759700d01885119a557a3a23487a5432dce5932f3a3b0c85972b5caf711c5f21066ff7011743d16a22da06b7e8ba22d390746034617b2f47c0c392f926a9665ba89244f5b30a1e9388c2a4749c9b70c7ba3a2c18c32481373a35acf8157a561613ddb693040b8f319876a7c165ab53aee1b4071d782f84e6cb415565ed4c0c00222e1a5c41bd8485eb40a4053183f2753f1c5243a536624f940029307d5d984934d05ce623066bc29ea6b93a3da99c927218f366ae98e168b30c536dec49f108c9f353073e347137604597053f20a03abd71280d235022a03c9d0a43ee29340845ca51f848056ca98506b91f61b26b9582a0556748a56f36d115a0365da0390b22db09d8c1029d3494ce811878e68e56295c38712c16f098f89b5a5953b568e7618af069a63c0f39ca70f73a0f2cbb150a64884e2c3d491b5013689741715b02227176d767715060da0439f39698283428e4b939f9fbbde627ce1be73c19fb54d082b7399383b5ac44e1205dfae12e6672601a4cc89a3a80677447e4c53b127b05c528a0e95cb512eb9dc8799ec00c38e427bf5ff94775877b87109c69b0bd367223bfe6a65f8b889eeb7a2ad80ea6001654d350cd665420d34f80d99fef72cb124089331585d6c5bd6d057f8e319352db71878196e6587d2d018f1496043bac9fd9106d53c30510b6abefd80d3d6319f1cbb5937b3d808706f888c2c07840d3a0c53565603909073660b83fc318c775b34909001054b82f9a0cc8155a3b002a12a414bad1a69dc2aa1aa489af7965a16989148467539352dd1794356473a3aca6668b5cd006ab972236d7484536ec1029d845fb386c9e4a539e593bfb225f558459e28274fccb0a05ac47528b459171c76764a47ed8b461f3124c174aa0c51778299a0a299882a63e88510d9adb479c30b47950974b9556cef689bf5006354c4fe11684a9d057f09c213ea63c32228ce5657f336a88bda34fff435d5a682a44312da5bc8d8a6b7bfc1a60fab9ce7a268c00dc040f04d0b651adf4e31ad6b36981b7a1ccd666a80b0f89928b81b686ce3c6b18e4a921c0a4f4d26bf0cb568db77908424b47d8031bb860c19a1f5eb131e838aa5e628bf29b2dfadc0c6e6c8dcb8277b783b61b69a303007f3a49018f3592c505618f3c3aeaf775995c6097671ea0cab7633c612a24a144dc81abc28fcc62b2b69a1e93d8b6e93ca81a2b9cd49bc1b5373af699aaefac7ba66c110198ad91555b9a46ac19322870322e16ec8cfee384c241bc21e28d5a48af52d68f37a8bb0669b7b1630889083f5e6bcd99007183014304553ed8d602a3b12df2b26e846c4f1a04eed59a7f1855399a47636186d461695ee4ff1f2558e47c05824395bb484b686c33a4dbc1d068a4423693afc0154b88c241340fa9bf6623011dea34", + "ct": "04f8b82b60ce1b47119f9b199c02a0be5b709394e6171c8b272509371681701c4d99bc4bee7f16772bb596271f8c80ed215cf777ce1507785a7506b3cc8a9ee516f0f3a90f4af6cff30115a944dc2856b6ec40ff9678dcfd81563ea5db1a4d269c8f1a7fdfbb9f6b44bdaee6a7126a143097e758463ad827298990b34170f655591e640a3b9db75610918534b3741283e2a0c50c716a8263e8dda9db348e7bab6d1a249f27fb899d05a3bc1cbb2ab0f7b1a378b1803bb83af411cf434fba3cd28bcb19ebedb675111359a3aa8575d3868e945fbada53bc9e555a0988820a39728b35930762b28509efc7bfd8d046cca32a9896c8071d56c8a508a26bdcd58e64696e15813ab66dabbfedb0d6bfa2078cf98131142765c6f9f54a501da1518a9a33206ee0fc7145c3cbca30e8b92354b21cecf6e5aced0ce9909a4204f3b007119dcdb303f1ff5a3352b78dff6f42a4ffcabe220938665316044fe533f01000e54f8e03f912b7b5263d329cfed70dfcf5059fb795426be342b6a849940469579d0f464aa9bba2dbe170c3532f0d8ece7bd71b993ca07c09ad010f4966525bdc43a1e2db0bd1b05aad5f70ded58df621ee73503f15037e42a130ab2962fcde2b1f10a518ed3c4cd37d1f1ba5e6f21231f4ff2f3925b5536186c57ae4efcbaef6ff59c261f1b61cc759c44863efe438b2ca3ad150adfcd5b7cbdf15ea8d2d7213d362a6364e558c94454425f5705844b2e0fdaa4965aa082c6b2e0e8256ce530752f79697af3fd5fd2c42a5bd64633106be2b26f370253c92e8212e9dfcad3ca10ccb69e0c8ebe6607c98bf0c75329949601133e554bf86cfecbc6c2093062655b6be07311ce7aaf48d24ea4448c45838af1fdf564c8af1a5948ff26d3e61ab3edf6c89570c78ceda2042eeae695143b592313d2c010b0911c796a6c90bba167a3705c5f7ccaab2ff21ed8ca281949b5b16f8a09be2ed7736903a85c20cf7f6bd48797a20f5225fe90c9ee10a81f8acfd6a81747dbe63753b5e594507e14e0a228ea99d4ae51d8caac0dc2f769989c967ebc80dc44c9e2f140ec29eab1d372f2370b0f66779337578b91354140c2998f19028c1e5f7634e94bfd470f3fbd9ab3f853093262bd403fbb18c1d28d8ac1b90f7b3e99936868bc0ce192ac338cf32baf87aeb9b71de977502c77230d4881ad30998c24b767790f16cc56c41e1781debb5c6f9d37b27120a3f225bfdf64ed65807fe988ea2c3001f711070a8354ba92468b115445af1bd8a9942287f4ffccaacaa715c34bc24901687f257808e48d7a2b6c49f4dbe46fc628819f09f89ac553da58e06e03553b30cdd3b715d4367b768da472fc2e734a47ab79ea9e30ad475a4de23321f8c6c9551e57629180cdde9923a90add52165195c7e670bd5989558307eaa0e6513d0225ba1fa213b319c378649bba7a1ddc146b5528d9230f0bc13c2922565d01ce23b57c6058dcc0a3153bdd6c2f0fa5394fb0a506cded4ab50a587a63cec6db3393790e0de35c4c309d88a41c91bdf6e0e03200693c9651e469aee6f91c98bea4127ae66312f4ae3ea155b67" + }, + { + "seed": "ef58538b8d23f87732ea63b02b4fa0f4873360e2841928cd60dd4cee8cc0d4c9", + "eseed": "22a96188d032675c8ac850933c7aff1533b94c834adbb69c6115bad4692d8619f90b0cdf8a7b9c264029ac185b70b83f2801f2f4b3f70c593ea3aeeb613a7f1b", + "ss": "3c111a7476821d18c4c05192298a881c46de82a25035e1fbc1ec399cd5a29924", + "sk": "ef58538b8d23f87732ea63b02b4fa0f4873360e2841928cd60dd4cee8cc0d4c9", + "pk": "9512abd457939c591d51687e8bc2ac7beace759bccb208b113729902d98543479384ac627ca21424304ad9281119f3460c975b6c3612812c5bd1f2422e3773a5c21c6d3862d6c3777ee31822a5718402543cf06476171cbf5895332a444e06c35a4c217d57768574a89dc0a58c8160efd847d0861579c278648b92a863b65c688eb41c1adf453d9494cf14f60c339c0b3e51a937d16cc1491314e013d8cacde2bcbf05d5994658579c834be29b5f6a1a601e24552a46822cf99838a57154916415dc54ea3363d751ced5b01160677417c81be3ba67c9b190bf29c85567b3051b17d6d75b97b883b043c442da3994910296a44f17a187bb51367fc88756baab782a45550b3e1c733bbac4b8a668b0ab586f64450d6a05219047785c1956a086207ac0a636d4bb398abf1649adf0e2069bb7ad063273575ac7947cc25beb768312464874c4f2091a3c434d93867943b0a3ebc22962aa2b6d5b284e0133f3309fde0c848c5436dd4772b1bcb81793affee19ab76c598d76b851282ac13ccfc7d4c6a56b2c336c70bc87221dc163c5518ebb3a5807e918033aacc2a3ad5e555131bb6d61c6b1446882a19c36bf142b7e7375a6893799b835a1a755383184c0027dbbfc193e546c9389cc53216280ab9c39323d11d468ca255e0af805712932656bba50f92528d435cca903d6981a1c2b4c9cb30cbdb4bd5f1a5ed84855ad076a18477f202bc699a412c4471a98d9b5974935a5151ef3167c7f8baccfd9432504bc7c6081491887d872c25fa7a6dd80cb3946832e828cf3406a1a0522ab3b63bdfb102d13ac2f193e7e7c82f6ec3804e2220c83bd83eb1bd0d65e175c81e7e176b3f6116d6140ac409ba4a30d1ef61e7fa0bb88ac209588c75bfa01aab803f55a8f77dc6eee6c0bf34c2c99e9aa846a2a590a757182ca8fd7536c0920e39a4fa3f763ea307f67172bacd6c2bc8937856b0eb56959bcfac111f964620081b7d380d3000f27f88a26da2db8f12493f641b09c9f65a563ebb913e267b1132ba46734cb111cb230d39f1bface22666b42529a01041bccd132bb0852995ccd273c384748a78848c284f5447eecaf9fa6c244977b676a1d2c26a8dc65c7974c89482189f4d545fd57348c6b670853cc6b383773925eaf336103aa5da5609b4da3c91ce54d2832974f3c445b5424a6182ca792b76de08e96518abaa94afc51646169301ce49722c13b5a1bc2ecac1a2408589531cc371a6064268db2bb8be783790efb00e38037cbc294e8820c4a9694785b3a8653320dfa296741b9658c6c7c41c91b0875d205191238b28ca860a48231f0733d87254ad5572d11e2955645589f0b19651a943ee20c1c7387b706083e093711eb099f4c32e4f26f4ee9ad3e74aeb4624c42c030cd66ad4087cb0cfa5f33323a286379c2090df78b1b04f1cc0cf931f6d6a49bcc6540e4c59373c6ebe64ca38886d75451e2192bfd20a7aeb528dcf1437665294099c09b2560d66ca900d33dea091d1c9a346c66731941cf57522b6fe33f0a27085e23b72e689990f14622111506dc14e2427db9285fbc82a90f9a23510746fd6caaf8418b59d622e16ca1edd1c1a452b48b0ab62a7369bd1722d3413a868b1c6ccdf2e301ec1f272ba68a1b5e02d743257be8ba94e9eb0f3ec27ce36306ca7b11ab356886212ddf193c9915762936f787a3bf665fc381c954c3bb0297da7836", + "ct": "5dae4188bb92ba700d830d8dc6aaf6ef699dcd48ac93c3f5b1e4b0ffc22cf21dfcb51be22532844f061bf3b9393c96dad49050bc9244584675c6919232893288f7218ba84d77f21b84cdccc514c147b54372b60bd8ee81c3dc96f0bee2eeadddca79e3d483f7d9e71fed01f5d6399ad9d7ede107b859b0f4031ea1376691ce6bfe36d1a211ceee3361035117e12ec81e3ab6362cfb7b87a0262279a2efd632a704dcea32fa1036a49e631d6d253ef49be6b46b7844b963bc05e7ca76b203594a2fa5f2812659a5e7aa74531128343f086c80cc69f3cd90ef4fecf1336f796b0c1c3d15cc54b4b7ee653f4c3b1e3cf97a4c1b223da34e90d6905974ab4e544ce2af8fdbefe8d5a3e8479ee07afb4ef546dc86d22e62081a27c14613ec1d284f850e96c4b4a8283f5b307d4f43776f86d9e5a7dbcf3073d468ed6f8cb82f8a0b13947703fd15c6911c31379e1d1ccc11560044554a4403cad162a4d86ef22be6a794a240af90e8367affbe4f84c8a4cd76934c1aea726efbf156108de461a276ac34baabc8bdb33085ce5dd9b6d89ef66f06c0590ae7a0cb8fd79a16ee4bdf48aa9788093e6e2f6fa63e837aa80072c2fbe5503a22b9cfff87d67b4f7508885966e8b5332299fe95f6e023d813e8e44f79d271eb725687d2dd5e06c0fd34cf5ce9108709960a4d23ef8e8f3d3ba65f5ff8bb57d07c6398ba46f7a81eba115fa2132066c96fbd1f7bce0749d1f961516c2d86b6f98f3b6f5d4a0dbb530cd7c59f4cad1554dd434bf44be5559614aa1b72ef41b179d667827c458b4196675dd538e288bdaebfbe57f13d65ed6cfcf38b5017dcd38c60a8dec0685944c86535b6a3c8a206bacea9d26b4d34cf1d28b4e6aead08f5ba668c0e8bd032ace996f112f7ae5d176ac0bccc2ff8b25e15ff7b7350e045d41b499179c1a754184fffaff17fa97025437bf590eb142e472d17a331e0a54045928a73ed641ac57f1b88b1392198949e625d0b2c5e7e9d721d02d609eca58a3fd830e6daa97ed59cd5466e356203fbe33e5947bf8ece898ecd2740ded4bb9e5633554fbe4a72b5564207099e13937bac1e7d7abfa843e95f6293282db0cd1dcd5cf1f3333d982343bcd6de04e9f6ded75b7f395b97790c564fc6be98ef5e18d9ab26c193d6ea2afb4a5771255745b727b8734bc06111b4a1d589d56a176cc5f5ea84d36f0402a9830aa5d7029bc65e3042dc85c66bc8a5c848690f8a847ad4ee837fd8eb01d5b5e29c5781afbd321351073f73b2fc7517f87e4a72e0206212ae393a7ce82a8144d4ab3b7eadd2efed21a6913ea298a0813d1b26c61a0f93623fa587b12bbacd163a76365023b2fc2b09aeea255dedca9d94b17ac0577435cc627d53992fafe73b956443b2ac1b8f9572c8b3f648f4e1b6427f90b45709f6c18d87e9e8f4d85e747abd90d7889ba92a27620f2ef6e674ec2f289da1b4e1c9ade9623582ff69aaad4f7131382fc15b0f2132ab0df74d769421d1d1677619cff13f1c2097bc6b098a7bbca0111658f0fa64de6b6e1c3c8e03db5971a445992227c825590688d203523f527161137334" + } +] \ No newline at end of file