From d9c304cf919182eff7e12f2dea5660c37c13d0c0 Mon Sep 17 00:00:00 2001 From: ashWhiteHat Date: Mon, 16 Oct 2023 03:47:41 +0900 Subject: [PATCH] ed448: Ed448 Implementation (#727) --- .github/workflows/ed448.yml | 56 ++++++ Cargo.lock | 12 ++ Cargo.toml | 1 + README.md | 4 +- ed25519/README.md | 4 +- ed25519/src/pkcs8.rs | 5 +- ed25519/tests/pkcs8.rs | 6 +- ed448/Cargo.toml | 35 ++++ ed448/LICENSE-APACHE | 201 +++++++++++++++++++++ ed448/LICENSE-MIT | 25 +++ ed448/README.md | 53 ++++++ ed448/src/hex.rs | 69 ++++++++ ed448/src/lib.rs | 142 +++++++++++++++ ed448/src/pkcs8.rs | 282 ++++++++++++++++++++++++++++++ ed448/src/serde.rs | 121 +++++++++++++ ed448/tests/examples/pkcs8-v1.der | Bin 0 -> 73 bytes ed448/tests/examples/pkcs8-v1.pem | 4 + ed448/tests/examples/pubkey.der | Bin 0 -> 69 bytes ed448/tests/examples/pubkey.pem | 3 + ed448/tests/hex.rs | 54 ++++++ ed448/tests/pkcs8.rs | 57 ++++++ ed448/tests/serde.rs | 24 +++ 22 files changed, 1150 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ed448.yml create mode 100644 ed448/Cargo.toml create mode 100644 ed448/LICENSE-APACHE create mode 100644 ed448/LICENSE-MIT create mode 100644 ed448/README.md create mode 100644 ed448/src/hex.rs create mode 100644 ed448/src/lib.rs create mode 100644 ed448/src/pkcs8.rs create mode 100644 ed448/src/serde.rs create mode 100644 ed448/tests/examples/pkcs8-v1.der create mode 100644 ed448/tests/examples/pkcs8-v1.pem create mode 100644 ed448/tests/examples/pubkey.der create mode 100644 ed448/tests/examples/pubkey.pem create mode 100644 ed448/tests/hex.rs create mode 100644 ed448/tests/pkcs8.rs create mode 100644 ed448/tests/serde.rs diff --git a/.github/workflows/ed448.yml b/.github/workflows/ed448.yml new file mode 100644 index 0000000..f2374ba --- /dev/null +++ b/.github/workflows/ed448.yml @@ -0,0 +1,56 @@ +name: ed448 +on: + pull_request: + paths: + - "ed448/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: ed448 + +env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Dwarnings" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + target: + - thumbv7em-none-eabi + - wasm32-unknown-unknown + toolchain: + - 1.60.0 # MSRV + - stable + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + targets: ${{ matrix.target }} + toolchain: ${{ matrix.toolchain }} + - run: cargo build --target ${{ matrix.target }} --release --no-default-features + - run: cargo build --target ${{ matrix.target }} --release --no-default-features --features alloc + # TODO(tarcieri): re-enable the following when MSRV is 1.65 + #- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pem + #- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features pkcs8 + #- run: cargo build --target ${{ matrix.target }} --release --no-default-features --features alloc,pem,pkcs8 + + test: + strategy: + matrix: + toolchain: + - 1.65.0 # Technically MSRV is 1.60, but we have 1.65 dev-dependencies (i.e. ring-compat) + - stable + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.toolchain }} + - run: cargo test --release --no-default-features + - run: cargo test --release + - run: cargo test --release --all-features diff --git a/Cargo.lock b/Cargo.lock index 707e745..481f067 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,6 +227,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ed448-signature" +version = "0.1.0" +dependencies = [ + "bincode", + "hex-literal", + "pkcs8", + "serde", + "serde_bytes", + "signature", +] + [[package]] name = "elliptic-curve" version = "0.13.6" diff --git a/Cargo.toml b/Cargo.toml index fd55cc2..267ffae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "dsa", "ecdsa", + "ed448", "ed25519", "rfc6979" ] diff --git a/README.md b/README.md index b620d3f..93160c4 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,8 @@ and can be easily used for bare-metal or lightweight WebAssembly programming. |-------------|-----------|-----------|---------------|-------| | [`dsa`] | [DSA](https://en.wikipedia.org/wiki/Digital_Signature_Algorithm) | [![crates.io](https://img.shields.io/crates/v/dsa.svg)](https://crates.io/crates/dsa) | [![Documentation](https://docs.rs/dsa/badge.svg)](https://docs.rs/dsa) | [![dsa build](https://github.com/RustCrypto/signatures/workflows/dsa/badge.svg?branch=master&event=push)](https://github.com/RustCrypto/signatures/actions?query=workflow%3Adsa) | [`ecdsa`] | [ECDSA](https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm) | [![crates.io](https://img.shields.io/crates/v/ecdsa.svg)](https://crates.io/crates/ecdsa) | [![Documentation](https://docs.rs/ecdsa/badge.svg)](https://docs.rs/ecdsa) | [![ecdsa build](https://github.com/RustCrypto/signatures/workflows/ecdsa/badge.svg?branch=master&event=push)](https://github.com/RustCrypto/signatures/actions?query=workflow%3Aecdsa) | -| [`ed25519`] | [Ed25519](https://en.wikipedia.org/wiki/EdDSA) | [![crates.io](https://img.shields.io/crates/v/ed25519.svg)](https://crates.io/crates/ed25519) | [![Documentation](https://docs.rs/ed25519/badge.svg)](https://docs.rs/ed25519) | [![ed25519 build](https://github.com/RustCrypto/signatures/workflows/ed25519/badge.svg?branch=master&event=push)](https://github.com/RustCrypto/signatures/actions?query=workflow%3Aed25519) +| [`ed448`] | [Ed448](https://en.wikipedia.org/wiki/EdDSA#Ed448) | [![crates.io](https://img.shields.io/crates/v/ed448-signature.svg)](https://crates.io/crates/ed448-signature) | [![Documentation](https://docs.rs/ed448-signature/badge.svg)](https://docs.rs/ed448-signature) | [![ed448 build](https://github.com/RustCrypto/signatures/workflows/ed448-signature/badge.svg?branch=master&event=push)](https://github.com/RustCrypto/signatures/actions?query=workflow%3Aed448-signature) | +| [`ed25519`] | [Ed25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519) | [![crates.io](https://img.shields.io/crates/v/ed25519.svg)](https://crates.io/crates/ed25519) | [![Documentation](https://docs.rs/ed25519/badge.svg)](https://docs.rs/ed25519) | [![ed25519 build](https://github.com/RustCrypto/signatures/workflows/ed25519/badge.svg?branch=master&event=push)](https://github.com/RustCrypto/signatures/actions?query=workflow%3Aed25519) | [`rfc6979`] | [RFC6979](https://datatracker.ietf.org/doc/html/rfc6979) | [![crates.io](https://img.shields.io/crates/v/rfc6979.svg)](https://crates.io/crates/rfc6979) | [![Documentation](https://docs.rs/rfc6979/badge.svg)](https://docs.rs/rfc6979) | [![rfc6979 build](https://github.com/RustCrypto/signatures/actions/workflows/rfc6979.yml/badge.svg)](https://github.com/RustCrypto/signatures/actions/workflows/rfc6979.yml) NOTE: for RSA signatures see @@ -51,6 +52,7 @@ dual licensed as above, without any additional terms or conditions. [`dsa`]: ./dsa [`ecdsa`]: ./ecdsa +[`ed448`]: ./ed448 [`ed25519`]: ./ed25519 [`rfc6979`]: ./rfc6979 diff --git a/ed25519/README.md b/ed25519/README.md index d425cde..de24ae2 100644 --- a/ed25519/README.md +++ b/ed25519/README.md @@ -37,7 +37,7 @@ for the default feature set. - All on-by-default features of this library are covered by SemVer - MSRV is considered exempt from SemVer as noted above -- The `pkcs8` module is exempted as it uses a pre-1.0 dependency, however, +- The `pkcs8` module is exempted as it uses a pre-1.0 dependency, however, breaking changes to this module will be accompanied by a minor version bump. ## License @@ -74,7 +74,7 @@ dual licensed as above, without any additional terms or conditions. [//]: # (footnotes) -[1]: https://en.wikipedia.org/wiki/EdDSA +[1]: https://en.wikipedia.org/wiki/EdDSA#Ed25519 [2]: https://tools.ietf.org/html/rfc8032 [3]: https://docs.rs/ed25519/latest/ed25519/struct.Signature.html [4]: https://docs.rs/signature/latest/signature/trait.Signer.html diff --git a/ed25519/src/pkcs8.rs b/ed25519/src/pkcs8.rs index 686f1cd..92039ae 100644 --- a/ed25519/src/pkcs8.rs +++ b/ed25519/src/pkcs8.rs @@ -14,7 +14,9 @@ //! Please lock to a specific minor version of the `ed25519` crate to avoid //! breaking changes when using this module. -pub use pkcs8::{spki, DecodePrivateKey, DecodePublicKey, Error, PrivateKeyInfo, Result}; +pub use pkcs8::{ + spki, DecodePrivateKey, DecodePublicKey, Error, ObjectIdentifier, PrivateKeyInfo, Result, +}; #[cfg(feature = "alloc")] pub use pkcs8::{spki::EncodePublicKey, EncodePrivateKey}; @@ -23,7 +25,6 @@ pub use pkcs8::{spki::EncodePublicKey, EncodePrivateKey}; pub use pkcs8::der::{asn1::BitStringRef, Document, SecretDocument}; use core::fmt; -use pkcs8::ObjectIdentifier; #[cfg(feature = "pem")] use { diff --git a/ed25519/tests/pkcs8.rs b/ed25519/tests/pkcs8.rs index c44388b..7291311 100644 --- a/ed25519/tests/pkcs8.rs +++ b/ed25519/tests/pkcs8.rs @@ -22,7 +22,7 @@ fn decode_pkcs8_v1() { let keypair = KeypairBytes::from_pkcs8_der(PKCS8_V1_DER).unwrap(); // Extracted with: - // $ openssl asn1parse -inform der -in tests/examples/p256-priv.der + // $ openssl asn1parse -inform der -in tests/examples/pkcs8-v1.der assert_eq!( keypair.secret_key, &hex!("D4EE72DBF913584AD5B6D8F1F769F8AD3AFE7C28CBF1D4FBE097A88F44755842")[..] @@ -36,7 +36,7 @@ fn decode_pkcs8_v2() { let keypair = KeypairBytes::from_pkcs8_der(PKCS8_V2_DER).unwrap(); // Extracted with: - // $ openssl asn1parse -inform der -in tests/examples/p256-priv.der + // $ openssl asn1parse -inform der -in tests/examples/pkcs8-v2.der assert_eq!( keypair.secret_key, &hex!("D4EE72DBF913584AD5B6D8F1F769F8AD3AFE7C28CBF1D4FBE097A88F44755842")[..] @@ -53,7 +53,7 @@ fn decode_public_key() { let public_key = PublicKeyBytes::from_public_key_der(PUBLIC_KEY_DER).unwrap(); // Extracted with: - // $ openssl pkey -inform der -in pkcs8-v1.der -pubout -text + // $ openssl pkey -inform der -in tests/examples/pkcs8-v1.der -pubout -text assert_eq!( public_key.as_ref(), &hex!("19BF44096984CDFE8541BAC167DC3B96C85086AA30B6B6CB0C5C38AD703166E1") diff --git a/ed448/Cargo.toml b/ed448/Cargo.toml new file mode 100644 index 0000000..390f84a --- /dev/null +++ b/ed448/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "ed448-signature" +version = "0.1.0" +edition = "2021" +authors = ["RustCrypto Developers"] +license = "Apache-2.0 OR MIT" +description = """ +Edwards Digital Signature Algorithm (EdDSA) over Curve448 (as specified in RFC 7748) +support library providing signature type definitions and PKCS#8 private key +decoding/encoding support +""" +documentation = "https://docs.rs/ed448-signature" +repository = "https://github.com/RustCrypto/signatures/tree/master/ed448-signature" +readme = "README.md" +categories = ["cryptography", "no-std"] +keywords = ["crypto", "curve448", "ecc", "signature", "signing"] + +[dependencies] +signature = { version = "2", default-features = false } + +# optional dependencies +pkcs8 = { version = "0.10", optional = true } +serde = { version = "1", optional = true, default-features = false } +serde_bytes = { version = "0.11", optional = true } + +[dev-dependencies] +hex-literal = "0.4" +bincode = "1" + +[features] +default = ["std"] +alloc = ["pkcs8?/alloc"] +pem = ["alloc", "pkcs8/pem"] +serde_bytes = ["serde", "dep:serde_bytes"] +std = ["signature/std"] diff --git a/ed448/LICENSE-APACHE b/ed448/LICENSE-APACHE new file mode 100644 index 0000000..c394d8a --- /dev/null +++ b/ed448/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2018-2022 RustCrypto Developers + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/ed448/LICENSE-MIT b/ed448/LICENSE-MIT new file mode 100644 index 0000000..d8d87fe --- /dev/null +++ b/ed448/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2018-2023 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/ed448/README.md b/ed448/README.md new file mode 100644 index 0000000..f27885b --- /dev/null +++ b/ed448/README.md @@ -0,0 +1,53 @@ +# [RustCrypto]: Ed448 + +[Edwards Digital Signature Algorithm (EdDSA)][1] over Curve448 as specified +in [RFC 7748][2]. + +## About + +This crate doesn't contain an implementation of Ed448. + +These traits allow crates which produce and consume Ed448 signatures +to be written abstractly in such a way that different signer/verifier +providers can be plugged in, enabling support for using different +Ed448 implementations, including HSMs or Cloud KMS services. + +## Minimum Supported Rust Version + +This crate requires **Rust 1.60** at a minimum. + +Our policy is to allow MSRV to be raised in future released without that +qualifing as a SemVer-breaking change, but it will be accompanied by a minor +version bump, ensuring if you lock to a minor version MSRV will be preserved +for the default feature set. + +## SemVer Policy + +- All on-by-default features of this library are covered by SemVer +- MSRV is considered exempt from SemVer as noted above +- The `pkcs8` module is exempted as it uses a pre-1.0 dependency, however, + breaking changes to this module will be accompanied by a minor version bump. + +## License + +All crates licensed under either of + + * [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) + * [MIT license](http://opensource.org/licenses/MIT) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in the work by you, as defined in the Apache-2.0 license, shall be +dual licensed as above, without any additional terms or conditions. + +[//]: # (links) + +[RustCrypto]: https://github.com/RustCrypto + +[//]: # (footnotes) + +[1]: https://en.wikipedia.org/wiki/EdDSA#Ed448 +[2]: https://tools.ietf.org/html/rfc7748 diff --git a/ed448/src/hex.rs b/ed448/src/hex.rs new file mode 100644 index 0000000..d576298 --- /dev/null +++ b/ed448/src/hex.rs @@ -0,0 +1,69 @@ +//! Hexadecimal encoding support +use crate::{Error, Signature}; +use core::{fmt, str}; + +impl fmt::LowerHex for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for component in [&self.R.0, &self.s.0] { + for byte in component { + write!(f, "{:02x}", byte)?; + } + } + Ok(()) + } +} + +impl fmt::UpperHex for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for component in [&self.R.0, &self.s.0] { + for byte in component { + write!(f, "{:02X}", byte)?; + } + } + Ok(()) + } +} + +/// Decode a signature from hexadecimal. +/// +/// Upper and lower case hexadecimal are both accepted, however mixed case is +/// rejected. +impl str::FromStr for Signature { + type Err = Error; + + fn from_str(hex: &str) -> signature::Result { + if hex.as_bytes().len() != Signature::BYTE_SIZE * 2 { + return Err(Error::new()); + } + + let mut upper_case = None; + + // Ensure all characters are valid and case is not mixed + for &byte in hex.as_bytes() { + match byte { + b'0'..=b'9' => (), + b'a'..=b'z' => match upper_case { + Some(true) => return Err(Error::new()), + Some(false) => (), + None => upper_case = Some(false), + }, + b'A'..=b'Z' => match upper_case { + Some(true) => (), + Some(false) => return Err(Error::new()), + None => upper_case = Some(true), + }, + _ => return Err(Error::new()), + } + } + + let mut result = [0u8; Self::BYTE_SIZE]; + for (digit, byte) in hex.as_bytes().chunks_exact(2).zip(result.iter_mut()) { + *byte = str::from_utf8(digit) + .ok() + .and_then(|s| u8::from_str_radix(s, 16).ok()) + .ok_or_else(Error::new)?; + } + + Self::try_from(&result[..]) + } +} diff --git a/ed448/src/lib.rs b/ed448/src/lib.rs new file mode 100644 index 0000000..2500e5a --- /dev/null +++ b/ed448/src/lib.rs @@ -0,0 +1,142 @@ +#![no_std] +#![doc = include_str!("../README.md")] +#![allow(non_snake_case)] +#![forbid(unsafe_code)] + +mod hex; + +#[cfg(feature = "pkcs8")] +pub mod pkcs8; + +#[cfg(feature = "serde")] +mod serde; + +pub use signature::{self, Error, SignatureEncoding}; + +use core::fmt; + +/// Size of a single component of an Ed448 signature. +const COMPONENT_SIZE: usize = 57; + +/// Size of an `R` or `s` component of an Ed448 signature when serialized +/// as bytes. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct ComponentBytes([u8; COMPONENT_SIZE]); + +impl Default for ComponentBytes { + fn default() -> Self { + ComponentBytes([0; COMPONENT_SIZE]) + } +} + +/// Ed448 signature serialized as a byte array. +pub type SignatureBytes = [u8; Signature::BYTE_SIZE]; + +/// Ed448 signature. +/// +/// This type represents a container for the byte serialization of an Ed448 +/// signature, and does not necessarily represent well-formed field or curve +/// elements. +/// +/// Signature verification libraries are expected to reject invalid field +/// elements at the time a signature is verified. +#[derive(Copy, Clone, Eq, PartialEq)] +#[repr(C)] +pub struct Signature { + R: ComponentBytes, + s: ComponentBytes, +} + +impl Signature { + /// Size of an encoded Ed448 signature in bytes. + pub const BYTE_SIZE: usize = COMPONENT_SIZE * 2; + + /// Parse an Ed448 signature from a byte slice. + pub fn from_bytes(bytes: &SignatureBytes) -> Self { + let mut R = ComponentBytes::default(); + let mut s = ComponentBytes::default(); + + let components = bytes.split_at(COMPONENT_SIZE); + R.0.copy_from_slice(components.0); + s.0.copy_from_slice(components.1); + + Self { R, s } + } + + /// Parse an Ed448 signature from a byte slice. + /// + /// # Returns + /// - `Ok` on success + /// - `Err` if the input byte slice is not 64-bytes + pub fn from_slice(bytes: &[u8]) -> signature::Result { + SignatureBytes::try_from(bytes) + .map(Into::into) + .map_err(|_| Error::new()) + } + + /// Bytes for the `R` component of a signature. + pub fn r_bytes(&self) -> &ComponentBytes { + &self.R + } + + /// Bytes for the `s` component of a signature. + pub fn s_bytes(&self) -> &ComponentBytes { + &self.s + } + + /// Return the inner byte array. + pub fn to_bytes(&self) -> SignatureBytes { + let mut ret = [0u8; Self::BYTE_SIZE]; + let (R, s) = ret.split_at_mut(COMPONENT_SIZE); + R.copy_from_slice(&self.R.0); + s.copy_from_slice(&self.s.0); + ret + } +} + +impl From for SignatureBytes { + fn from(sig: Signature) -> SignatureBytes { + sig.to_bytes() + } +} + +impl From<&Signature> for SignatureBytes { + fn from(sig: &Signature) -> SignatureBytes { + sig.to_bytes() + } +} + +impl From for Signature { + fn from(bytes: SignatureBytes) -> Self { + Signature::from_bytes(&bytes) + } +} + +impl From<&SignatureBytes> for Signature { + fn from(bytes: &SignatureBytes) -> Self { + Signature::from_bytes(bytes) + } +} + +impl TryFrom<&[u8]> for Signature { + type Error = Error; + + fn try_from(bytes: &[u8]) -> signature::Result { + Self::from_slice(bytes) + } +} + +impl fmt::Debug for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ed448_signature::Signature") + .field("R", self.r_bytes()) + .field("s", self.s_bytes()) + .finish() + } +} + +impl fmt::Display for Signature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:X}", self) + } +} diff --git a/ed448/src/pkcs8.rs b/ed448/src/pkcs8.rs new file mode 100644 index 0000000..0c23412 --- /dev/null +++ b/ed448/src/pkcs8.rs @@ -0,0 +1,282 @@ +//! PKCS#8 private key support. +//! +//! Implements Ed448 PKCS#8 private keys as described in RFC8410 Section 7: +//! +//! +//! ## SemVer Notes +//! +//! The `pkcs8` module of this crate is exempted from SemVer as it uses a +//! pre-1.0 dependency (the `pkcs8` crate). +//! +//! However, breaking changes to this module will be accompanied by a minor +//! version bump. +//! +//! Please lock to a specific minor version of the `ed448` crate to avoid +//! breaking changes when using this module. + +pub use pkcs8::{ + spki, DecodePrivateKey, DecodePublicKey, Error, ObjectIdentifier, PrivateKeyInfo, Result, +}; + +#[cfg(feature = "alloc")] +pub use pkcs8::{spki::EncodePublicKey, EncodePrivateKey}; + +#[cfg(feature = "alloc")] +pub use pkcs8::der::{asn1::BitStringRef, Document, SecretDocument}; + +use core::fmt; + +/// Algorithm [`ObjectIdentifier`] for the Ed448 digital signature algorithm +/// (`id-Ed448`). +/// +/// +pub const ALGORITHM_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.101.113"); + +/// Ed448 Algorithm Identifier. +pub const ALGORITHM_ID: pkcs8::AlgorithmIdentifierRef<'static> = pkcs8::AlgorithmIdentifierRef { + oid: ALGORITHM_OID, + parameters: None, +}; + +/// Ed448 keypair serialized as bytes. +/// +/// This type is primarily useful for decoding/encoding PKCS#8 private key +/// files (either DER or PEM) encoded using the following traits: +/// +/// - [`DecodePrivateKey`]: decode DER or PEM encoded PKCS#8 private key. +/// - [`EncodePrivateKey`]: encode DER or PEM encoded PKCS#8 private key. +/// +/// PKCS#8 private key files encoded with PEM begin with: +/// +/// ```text +/// -----BEGIN PRIVATE KEY----- +/// ``` +/// +/// Note that this type operates on raw bytes and performs no validation that +/// keys represent valid Ed448 field elements. +pub struct KeypairBytes { + /// Ed448 secret key. + /// + /// Little endian serialization of an element of the Curve448 scalar + /// field, prior to "clamping" (i.e. setting/clearing bits to ensure the + /// scalar is actually a valid field element) + pub secret_key: [u8; Self::BYTE_SIZE / 2], + + /// Ed448 public key (if available). + /// + /// Compressed Edwards-y encoded curve point. + pub public_key: Option, +} + +impl KeypairBytes { + /// Size of an Ed448 keypair when serialized as bytes. + const BYTE_SIZE: usize = 114; + + /// Parse raw keypair from a 114-byte input. + pub fn from_bytes(bytes: &[u8; Self::BYTE_SIZE]) -> Self { + let (sk, pk) = bytes.split_at(Self::BYTE_SIZE / 2); + + Self { + secret_key: sk.try_into().expect("secret key size error"), + public_key: Some(PublicKeyBytes( + pk.try_into().expect("public key size error"), + )), + } + } + + /// Serialize as a 114-byte keypair. + /// + /// # Returns + /// + /// - `Some(bytes)` if the `public_key` is present. + /// - `None` if the `public_key` is absent (i.e. `None`). + pub fn to_bytes(&self) -> Option<[u8; Self::BYTE_SIZE]> { + if let Some(public_key) = &self.public_key { + let mut result = [0u8; Self::BYTE_SIZE]; + let (sk, pk) = result.split_at_mut(Self::BYTE_SIZE / 2); + sk.copy_from_slice(&self.secret_key); + pk.copy_from_slice(public_key.as_ref()); + Some(result) + } else { + None + } + } +} + +#[cfg(feature = "alloc")] +impl EncodePrivateKey for KeypairBytes { + fn to_pkcs8_der(&self) -> Result { + // Serialize private key as nested OCTET STRING + let mut private_key = [0u8; 2 + (Self::BYTE_SIZE / 2)]; + private_key[0] = 0x04; + private_key[1] = 0x39; + private_key[2..].copy_from_slice(&self.secret_key); + + let private_key_info = PrivateKeyInfo { + algorithm: ALGORITHM_ID, + private_key: &private_key, + public_key: self.public_key.as_ref().map(|pk| pk.0.as_slice()), + }; + + let result = SecretDocument::encode_msg(&private_key_info)?; + + #[cfg(feature = "zeroize")] + private_key.zeroize(); + + Ok(result) + } +} + +impl TryFrom> for KeypairBytes { + type Error = Error; + + fn try_from(private_key: PrivateKeyInfo<'_>) -> Result { + private_key.algorithm.assert_algorithm_oid(ALGORITHM_OID)?; + + if private_key.algorithm.parameters.is_some() { + return Err(Error::ParametersMalformed); + } + + // Ed448 PKCS#8 keys are represented as a nested OCTET STRING + // (i.e. an OCTET STRING within an OCTET STRING). + // + // This match statement checks and removes the inner OCTET STRING + // header value: + // + // - 0x04: OCTET STRING tag + // - 0x39: 57-byte length + let secret_key = match private_key.private_key { + [0x04, 0x39, rest @ ..] => rest.try_into().map_err(|_| Error::KeyMalformed), + _ => Err(Error::KeyMalformed), + }?; + + let public_key = private_key + .public_key + .map(|bytes| bytes.try_into().map_err(|_| Error::KeyMalformed)) + .transpose()? + .map(PublicKeyBytes); + + Ok(Self { + secret_key, + public_key, + }) + } +} + +impl fmt::Debug for KeypairBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("KeypairBytes") + .field("public_key", &self.public_key) + .finish_non_exhaustive() + } +} + +/// Ed448 public key serialized as bytes. +/// +/// This type is primarily useful for decoding/encoding SPKI public key +/// files (either DER or PEM) encoded using the following traits: +/// +/// - [`DecodePublicKey`]: decode DER or PEM encoded PKCS#8 private key. +/// - [`EncodePublicKey`]: encode DER or PEM encoded PKCS#8 private key. +/// +/// SPKI public key files encoded with PEM begin with: +/// +/// ```text +/// -----BEGIN PUBLIC KEY----- +/// ``` +/// +/// Note that this type operates on raw bytes and performs no validation that +/// public keys represent valid compressed Ed448 y-coordinates. +#[derive(Clone, Copy, Eq, PartialEq)] +pub struct PublicKeyBytes(pub [u8; Self::BYTE_SIZE]); + +impl PublicKeyBytes { + /// Size of an Ed448 public key when serialized as bytes. + const BYTE_SIZE: usize = 57; + + /// Returns the raw bytes of the public key. + pub fn to_bytes(&self) -> [u8; Self::BYTE_SIZE] { + self.0 + } +} + +impl AsRef<[u8; Self::BYTE_SIZE]> for PublicKeyBytes { + fn as_ref(&self) -> &[u8; Self::BYTE_SIZE] { + &self.0 + } +} + +#[cfg(feature = "alloc")] +impl EncodePublicKey for PublicKeyBytes { + fn to_public_key_der(&self) -> spki::Result { + pkcs8::SubjectPublicKeyInfoRef { + algorithm: ALGORITHM_ID, + subject_public_key: BitStringRef::new(0, &self.0)?, + } + .try_into() + } +} + +impl TryFrom> for PublicKeyBytes { + type Error = spki::Error; + + fn try_from(spki: spki::SubjectPublicKeyInfoRef<'_>) -> spki::Result { + spki.algorithm.assert_algorithm_oid(ALGORITHM_OID)?; + + if spki.algorithm.parameters.is_some() { + return Err(spki::Error::KeyMalformed); + } + + spki.subject_public_key + .as_bytes() + .ok_or(spki::Error::KeyMalformed)? + .try_into() + .map(Self) + .map_err(|_| spki::Error::KeyMalformed) + } +} + +impl fmt::Debug for PublicKeyBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("PublicKeyBytes(")?; + + for &byte in self.as_ref() { + write!(f, "{:02X}", byte)?; + } + + f.write_str(")") + } +} + +#[cfg(feature = "pem")] +#[cfg(test)] +mod tests { + use super::{KeypairBytes, PublicKeyBytes}; + use hex_literal::hex; + + const SECRET_KEY_BYTES: [u8; 57] = + hex!("8A57471AA375074DC7D75EA2252E9933BB15C107E4F9A2F9CFEA6C418BEBB0774D1ABB671B58B96EFF95F35D63F2418422A59C7EAE3E00D70F"); + + const PUBLIC_KEY_BYTES: [u8; 57] = + hex!("f27f9809412035541b681c69fbe69b9d25a6af506d914ecef7d973fca04ccd33a8b96a0868211382ca08fe06b72e8c0cb3297f3a9d6bc02380"); + + #[test] + fn to_bytes() { + let valid_keypair = KeypairBytes { + secret_key: SECRET_KEY_BYTES, + public_key: Some(PublicKeyBytes(PUBLIC_KEY_BYTES)), + }; + + assert_eq!( + valid_keypair.to_bytes().unwrap(), + hex!("8A57471AA375074DC7D75EA2252E9933BB15C107E4F9A2F9CFEA6C418BEBB0774D1ABB671B58B96EFF95F35D63F2418422A59C7EAE3E00D70Ff27f9809412035541b681c69fbe69b9d25a6af506d914ecef7d973fca04ccd33a8b96a0868211382ca08fe06b72e8c0cb3297f3a9d6bc02380") + ); + + let invalid_keypair = KeypairBytes { + secret_key: SECRET_KEY_BYTES, + public_key: None, + }; + + assert_eq!(invalid_keypair.to_bytes(), None); + } +} diff --git a/ed448/src/serde.rs b/ed448/src/serde.rs new file mode 100644 index 0000000..d855cce --- /dev/null +++ b/ed448/src/serde.rs @@ -0,0 +1,121 @@ +//! `serde` support. + +use crate::{Signature, SignatureBytes}; +use ::serde::{de, ser, Deserialize, Serialize}; +use core::fmt; + +impl Serialize for Signature { + fn serialize(&self, serializer: S) -> Result { + use ser::SerializeTuple; + + let mut seq = serializer.serialize_tuple(Signature::BYTE_SIZE)?; + + for byte in self.to_bytes() { + seq.serialize_element(&byte)?; + } + + seq.end() + } +} + +impl<'de> Deserialize<'de> for Signature { + fn deserialize>(deserializer: D) -> Result { + struct ByteArrayVisitor; + + impl<'de> de::Visitor<'de> for ByteArrayVisitor { + type Value = [u8; Signature::BYTE_SIZE]; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("bytestring of length 64") + } + + fn visit_seq(self, mut seq: A) -> Result<[u8; Signature::BYTE_SIZE], A::Error> + where + A: de::SeqAccess<'de>, + { + use de::Error; + let mut arr = [0u8; Signature::BYTE_SIZE]; + + for (i, byte) in arr.iter_mut().enumerate() { + *byte = seq + .next_element()? + .ok_or_else(|| Error::invalid_length(i, &self))?; + } + + Ok(arr) + } + } + + deserializer + .deserialize_tuple(Signature::BYTE_SIZE, ByteArrayVisitor) + .map(Into::into) + } +} + +#[cfg(feature = "serde_bytes")] +impl serde_bytes::Serialize for Signature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_bytes(&self.to_bytes()) + } +} + +#[cfg(feature = "serde_bytes")] +impl<'de> serde_bytes::Deserialize<'de> for Signature { + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + struct ByteArrayVisitor; + + impl<'de> de::Visitor<'de> for ByteArrayVisitor { + type Value = SignatureBytes; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("bytestring of length 64") + } + + fn visit_bytes(self, bytes: &[u8]) -> Result + where + E: de::Error, + { + use de::Error; + + bytes + .try_into() + .map_err(|_| Error::invalid_length(bytes.len(), &self)) + } + } + + deserializer + .deserialize_bytes(ByteArrayVisitor) + .map(Into::into) + } +} + +#[cfg(test)] +mod tests { + use crate::{Signature, SignatureBytes}; + use hex_literal::hex; + + const SIGNATURE_BYTES: SignatureBytes = hex!( + "533a37f6bbe457251f023c0d88f976ae + 2dfb504a843e34d2074fd823d41a591f + 2b233f034f628281f2fd7a22ddd47d78 + 28c59bd0a21bfd3980ff0d2028d4b18a + 9df63e006c5d1c2d345b925d8dc00b41 + 04852db99ac5c7cdda8530a113a0f4db + b61149f05a7363268c71d95808ff2e65 + 2600" + ); + + #[test] + fn round_trip() { + let signature = Signature::from_bytes(&SIGNATURE_BYTES); + let serialized = bincode::serialize(&signature).unwrap(); + let deserialized = bincode::deserialize(&serialized).unwrap(); + assert_eq!(signature, deserialized); + } +} diff --git a/ed448/tests/examples/pkcs8-v1.der b/ed448/tests/examples/pkcs8-v1.der new file mode 100644 index 0000000000000000000000000000000000000000..4d37598dbf8ef7d59dbf10e03cf0b045cc8189f3 GIT binary patch literal 73 zcmV-P0Ji@yM*;x=Fa-t!D`jy6I|Mn3S4SG7bq7ty*IuF}E}1jC6~PDO`J(yH>TE%a f>#%oC8oOs3Sh;ThmGfO=@- literal 0 HcmV?d00001 diff --git a/ed448/tests/examples/pkcs8-v1.pem b/ed448/tests/examples/pkcs8-v1.pem new file mode 100644 index 0000000..60657c0 --- /dev/null +++ b/ed448/tests/examples/pkcs8-v1.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MEcCAQAwBQYDK2VxBDsEOYpXRxqjdQdNx9deoiUumTO7FcEH5Pmi+c/qbEGL67B3 +TRq7ZxtYuW7/lfNdY/JBhCKlnH6uPgDXDw== +-----END PRIVATE KEY----- diff --git a/ed448/tests/examples/pubkey.der b/ed448/tests/examples/pubkey.der new file mode 100644 index 0000000000000000000000000000000000000000..40975d9e00a1df9a9a99a2add33ae1766853fe39 GIT binary patch literal 69 zcmV-L0J{G$Lofvf11n{513Ccmf0zkDAT?AQXdG$#=9`@*rms+KkxtI{*>n7$OwBW> bxoQY#Arpei2>u4QE{qJbDStYhYrrFbhprr8 literal 0 HcmV?d00001 diff --git a/ed448/tests/examples/pubkey.pem b/ed448/tests/examples/pubkey.pem new file mode 100644 index 0000000..41b0218 --- /dev/null +++ b/ed448/tests/examples/pubkey.pem @@ -0,0 +1,3 @@ +-----BEGIN PUBLIC KEY----- +MCowBQYDK2VwAyEAGb9ECWmEzf6FQbrBZ9w7lshQhqowtrbLDFw4rXAxZuE= +-----END PUBLIC KEY----- diff --git a/ed448/tests/hex.rs b/ed448/tests/hex.rs new file mode 100644 index 0000000..3fc45c3 --- /dev/null +++ b/ed448/tests/hex.rs @@ -0,0 +1,54 @@ +//! Hexadecimal display/serialization tests. + +use ed448_signature::Signature; +use hex_literal::hex; +use std::str::FromStr; + +/// Test 1 signature from RFC 8032 ยง 7.4 +/// +const TEST_1_SIGNATURE: [u8; Signature::BYTE_SIZE] = hex!( + "533a37f6bbe457251f023c0d88f976ae + 2dfb504a843e34d2074fd823d41a591f + 2b233f034f628281f2fd7a22ddd47d78 + 28c59bd0a21bfd3980ff0d2028d4b18a + 9df63e006c5d1c2d345b925d8dc00b41 + 04852db99ac5c7cdda8530a113a0f4db + b61149f05a7363268c71d95808ff2e65 + 2600" +); + +#[test] +fn display() { + let sig = Signature::from_bytes(&TEST_1_SIGNATURE); + assert_eq!(sig.to_string(), "533A37F6BBE457251F023C0D88F976AE2DFB504A843E34D2074FD823D41A591F2B233F034F628281F2FD7A22DDD47D7828C59BD0A21BFD3980FF0D2028D4B18A9DF63E006C5D1C2D345B925D8DC00B4104852DB99AC5C7CDDA8530A113A0F4DBB61149F05A7363268C71D95808FF2E652600") +} + +#[test] +fn lower_hex() { + let sig = Signature::from_bytes(&TEST_1_SIGNATURE); + assert_eq!(format!("{:x}", sig), "533a37f6bbe457251f023c0d88f976ae2dfb504a843e34d2074fd823d41a591f2b233f034f628281f2fd7a22ddd47d7828c59bd0a21bfd3980ff0d2028d4b18a9df63e006c5d1c2d345b925d8dc00b4104852db99ac5c7cdda8530a113a0f4dbb61149f05a7363268c71d95808ff2e652600") +} + +#[test] +fn upper_hex() { + let sig = Signature::from_bytes(&TEST_1_SIGNATURE); + assert_eq!(format!("{:X}", sig), "533A37F6BBE457251F023C0D88F976AE2DFB504A843E34D2074FD823D41A591F2B233F034F628281F2FD7A22DDD47D7828C59BD0A21BFD3980FF0D2028D4B18A9DF63E006C5D1C2D345B925D8DC00B4104852DB99AC5C7CDDA8530A113A0F4DBB61149F05A7363268C71D95808FF2E652600") +} + +#[test] +fn from_str_lower() { + let sig = Signature::from_str("533a37f6bbe457251f023c0d88f976ae2dfb504a843e34d2074fd823d41a591f2b233f034f628281f2fd7a22ddd47d7828c59bd0a21bfd3980ff0d2028d4b18a9df63e006c5d1c2d345b925d8dc00b4104852db99ac5c7cdda8530a113a0f4dbb61149f05a7363268c71d95808ff2e652600").unwrap(); + assert_eq!(sig.to_bytes(), TEST_1_SIGNATURE); +} + +#[test] +fn from_str_upper() { + let sig = Signature::from_str("533A37F6BBE457251F023C0D88F976AE2DFB504A843E34D2074FD823D41A591F2B233F034F628281F2FD7A22DDD47D7828C59BD0A21BFD3980FF0D2028D4B18A9DF63E006C5D1C2D345B925D8DC00B4104852DB99AC5C7CDDA8530A113A0F4DBB61149F05A7363268C71D95808FF2E652600").unwrap(); + assert_eq!(sig.to_bytes(), TEST_1_SIGNATURE); +} + +#[test] +fn from_str_rejects_mixed_case() { + let result = Signature::from_str("533A37f6bbe457251f023c0d88f976ae2dfb504a843e34d2074fd823d41a591f2b233f034f628281f2fd7a22ddd47d7828c59bd0a21bfd3980ff0d2028d4b18a9df63e006c5d1c2d345b925d8dc00b4104852db99ac5c7cdda8530a113a0f4dbb61149f05a7363268c71d95808ff2e652600"); + assert!(result.is_err()); +} diff --git a/ed448/tests/pkcs8.rs b/ed448/tests/pkcs8.rs new file mode 100644 index 0000000..5ff4321 --- /dev/null +++ b/ed448/tests/pkcs8.rs @@ -0,0 +1,57 @@ +//! PKCS#8 private key tests + +#![cfg(feature = "pkcs8")] + +use ed448_signature::pkcs8::{DecodePrivateKey, DecodePublicKey, KeypairBytes, PublicKeyBytes}; +use hex_literal::hex; + +#[cfg(feature = "alloc")] +use ed448_signature::pkcs8::{EncodePrivateKey, EncodePublicKey}; + +/// Ed448 PKCS#8 v1 private key encoded as ASN.1 DER. +const PKCS8_V1_DER: &[u8] = include_bytes!("examples/pkcs8-v1.der"); + +/// Ed448 SubjectPublicKeyInfo encoded as ASN.1 DER. +const PUBLIC_KEY_DER: &[u8] = include_bytes!("examples/pubkey.der"); + +#[test] +fn decode_pkcs8_v1() { + let keypair = KeypairBytes::from_pkcs8_der(PKCS8_V1_DER).unwrap(); + + // Extracted with: + // $ openssl asn1parse -inform der -in tests/examples/pkcs8-v1.der + assert_eq!( + keypair.secret_key, + &hex!("8A57471AA375074DC7D75EA2252E9933BB15C107E4F9A2F9CFEA6C418BEBB0774D1ABB671B58B96EFF95F35D63F2418422A59C7EAE3E00D70F")[..] + ); + + assert_eq!(keypair.public_key, None); +} + +#[test] +fn decode_public_key() { + let public_key = PublicKeyBytes::from_public_key_der(PUBLIC_KEY_DER).unwrap(); + + // Extracted with: + // $ openssl pkey -inform der -in tests/examples/pkcs8-v1.der -pubout -text + assert_eq!( + public_key.as_ref(), + &hex!("f27f9809412035541b681c69fbe69b9d25a6af506d914ecef7d973fca04ccd33a8b96a0868211382ca08fe06b72e8c0cb3297f3a9d6bc02380") + ); +} + +#[cfg(feature = "alloc")] +#[test] +fn encode_pkcs8_v1() { + let pk = KeypairBytes::from_pkcs8_der(PKCS8_V1_DER).unwrap(); + let pk_der = pk.to_pkcs8_der().unwrap(); + assert_eq!(pk_der.as_bytes(), PKCS8_V1_DER); +} + +#[cfg(feature = "alloc")] +#[test] +fn encode_public_key() { + let pk = PublicKeyBytes::from_public_key_der(PUBLIC_KEY_DER).unwrap(); + let pk_der = pk.to_public_key_der().unwrap(); + assert_eq!(pk_der.as_ref(), PUBLIC_KEY_DER); +} diff --git a/ed448/tests/serde.rs b/ed448/tests/serde.rs new file mode 100644 index 0000000..60ba765 --- /dev/null +++ b/ed448/tests/serde.rs @@ -0,0 +1,24 @@ +//! Tests for serde serializers/deserializers + +#![cfg(feature = "serde")] + +use ed448_signature::{Signature, SignatureBytes}; +use hex_literal::hex; + +const EXAMPLE_SIGNATURE: SignatureBytes = hex!( + "3f3e3d3c3b3a393837363534333231302f2e2d2c2b2a292827262524232221201f1e1d1c1b1a191817161514131211100f0e0d0c0b0a090807" + "1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0efeeedecebeae9e8e7" +); + +#[test] +fn test_serialize() { + let signature = Signature::try_from(&EXAMPLE_SIGNATURE[..]).unwrap(); + let encoded_signature: Vec = bincode::serialize(&signature).unwrap(); + assert_eq!(&EXAMPLE_SIGNATURE[..], &encoded_signature[..]); +} + +#[test] +fn test_deserialize() { + let signature = bincode::deserialize::(&EXAMPLE_SIGNATURE).unwrap(); + assert_eq!(EXAMPLE_SIGNATURE, signature.to_bytes()); +}