From 0f416c44046be8b17dc9d7dca9de3e15c84699d2 Mon Sep 17 00:00:00 2001 From: rlee287 Date: Mon, 30 Oct 2023 15:43:24 -0700 Subject: [PATCH 1/8] Copy over the committing AEAD code from the previous PR on the `aead` crate --- Cargo.lock | 20 ++ Cargo.toml | 1 + README.md | 2 + committing-aead/Cargo.toml | 31 ++ committing-aead/LICENSE-APACHE | 201 ++++++++++++ committing-aead/LICENSE-MIT | 25 ++ committing-aead/README.md | 53 +++ committing-aead/src/lib.rs | 570 +++++++++++++++++++++++++++++++++ 8 files changed, 903 insertions(+) create mode 100644 committing-aead/Cargo.toml create mode 100644 committing-aead/LICENSE-APACHE create mode 100644 committing-aead/LICENSE-MIT create mode 100644 committing-aead/README.md create mode 100644 committing-aead/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 5d41facb..3747a340 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -204,6 +204,17 @@ dependencies = [ "digest", ] +[[package]] +name = "committing-aead" +version = "0.1.0" +dependencies = [ + "aead", + "digest", + "generic-array", + "hmac", + "subtle", +] + [[package]] name = "cpufeatures" version = "0.2.10" @@ -360,6 +371,15 @@ dependencies = [ "proc-macro-hack", ] +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "inout" version = "0.1.3" diff --git a/Cargo.toml b/Cargo.toml index 4b726a48..cddbf09d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ members = [ "deoxys", "eax", "mgm", + "committing-aead", ] diff --git a/README.md b/README.md index bf56003a..e93b244c 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ algorithms written in pure Rust. AEADs are high-level symmetric encryption primitives which defend against a wide range of potential attacks (i.e. [IND-CCA3]). +For situations where committing AEADs are needed, see the `committing-aead` crate. + ## Usage Crates functionality is expressed in terms of traits defined in the [`aead`] diff --git a/committing-aead/Cargo.toml b/committing-aead/Cargo.toml new file mode 100644 index 00000000..5c647aea --- /dev/null +++ b/committing-aead/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "committing-aead" +version = "0.1.0" +description = """ +Marker traits to indicate that an AEAD is committing, as well as pure Rust +implementations of the "padding fix" key-commitment scheme, the CTX scheme +(encryption only), and a modified version of CTX (named CTXish-HMAC) to +enable implementation of both encryption and decryption using the existing +AEAD trait interfaces. +""" +authors = ["RustCrypto Developers"] +edition = "2021" +license = "Apache-2.0 OR MIT" +readme = "README.md" +documentation = "https://docs.rs/committing-aead" +repository = "https://github.com/RustCrypto/AEADs/tree/master/committing-aead" +keywords = ["aead", "encryption", "committing-aead", "ctx"] +categories = ["cryptography", "no-std"] + +[dependencies] +aead = { version = "0.5", default-features = false } +generic-array = { version = "0.14", default-features = false } + +subtle = { version = "2.5", default-features = false } +digest = { version = "0.10.7", features = ["mac"] } +hmac = { version = "0.12.1" } + +[features] +default = ["std"] +alloc = ["aead/alloc"] +std = ["alloc", "aead/std"] diff --git a/committing-aead/LICENSE-APACHE b/committing-aead/LICENSE-APACHE new file mode 100644 index 00000000..78173fa2 --- /dev/null +++ b/committing-aead/LICENSE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/committing-aead/LICENSE-MIT b/committing-aead/LICENSE-MIT new file mode 100644 index 00000000..b7f57116 --- /dev/null +++ b/committing-aead/LICENSE-MIT @@ -0,0 +1,25 @@ +Copyright (c) 2019 The RustCrypto Project 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/committing-aead/README.md b/committing-aead/README.md new file mode 100644 index 00000000..d268230b --- /dev/null +++ b/committing-aead/README.md @@ -0,0 +1,53 @@ +# RustCrypto: Committing AEAD Wrappers + +Marker traits for committing AEADs, along with pure Rust implementations of a +number of constructions that wrap a generic AEAD and provide commitment properties. +The module documentation contains further explanations about committing AEADs and +when they are necessary. + +## About + +The following constructions are supported: + +| Committing AEAD Construction | Encryption? | Decryption? | Commitment Security | +|------------------------------|-------------|-------------|---------------------| +| ["Padding Fix"] | Yes | Yes | Key only | +| [CTX] | Yes | No | All inputs | +| CTXish-HMAC (see code docs) | Yes | Yes | All inputs | + +CTX decryption is not implemented because verifying the tag at decryption time +requires accessing the expected original tag of the wrapped AEAD, which is +currently not accessible with the current AEAD trait interfaces. CTXish-HMAC, +documented in the code docs, is a modification of CTX that does not require +recomputing the expected original tag, at the cost of additional tag overhead. + +## Security Notes + +No security audits of this crate have ever been performed. + +USE AT YOUR OWN RISK! + +## Minimum Supported Rust Version + +This crate requires **Rust 1.56** 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. + +["Padding Fix"]: https://eprint.iacr.org/2020/1456.pdf +[CTX]: https://eprint.iacr.org/2022/1260.pdf diff --git a/committing-aead/src/lib.rs b/committing-aead/src/lib.rs new file mode 100644 index 00000000..0f13dfc2 --- /dev/null +++ b/committing-aead/src/lib.rs @@ -0,0 +1,570 @@ +//! Committing AEAD marker traits and generic constructions. +//! +//! ## Why Committing AEADs? +//! +//! While AEADs provide confidentiality and integrity, many of them do not +//! provide a commitment for their inputs (which can equivalently be thought +//! of as collision resistance of an AEAD with respect to its inputs). The +//! lack of commitment properties has lead to breaks in real cryptographic +//! protocols, e.g. the Shadowsocks proxy ans improper implementations of the +//! password-authenticated key exchange [OPAQUE][2], as described in the +//! [partitioning oracle attacks][3] paper. +//! +//! Concrete examples of popular AEADs that lack commitment properties: +//! - AEADs using polynomial-based MACs (e.g. AES-GCM, AES-GCM-SIV, +//! and ChaCha20Poly1305) do not commit to their inputs. [This paper][1] +//! describes how to construct an AES-GCM ciphertext that decrypts correctly +//! under two different keys to two different, semantically meaningful +//! plaintexts. +//! - AEADs where decryption can be separated into parallel always-successful +//! plaintext recovery and tag computation+equality checking steps cannot +//! provide commitment when the tag computation function is not preimage +//! resistant. [This paper][5] provides concrete attacks against EAX, GCM, +//! SIV, CCM, and OCB3 that demonstrate that they are not key-commiting. +//! +//! [1]: https://eprint.iacr.org/2019/016.pdf +//! [2]: https://eprint.iacr.org/2018/163.pdf +//! [3]: https://www.usenix.org/system/files/sec21summer_len.pdf +//! [4]: https://eprint.iacr.org/2020/1456.pdf +//! [5]: https://eprint.iacr.org/2023/526.pdf + +#![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/RustCrypto/media/8f1a9894/logo.svg" +)] +#![forbid(unsafe_code)] +#![warn(clippy::unwrap_used, missing_docs, rust_2018_idioms)] + +#[cfg(feature = "alloc")] +extern crate alloc; + +#[cfg(feature = "std")] +extern crate std; + +use aead::AeadCore; + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +pub use padded_aead::PaddedAead; + +pub use ctx::CtxAead; +pub use ctx::CtxishHmacAead; + +/// Marker trait that signals that an AEAD commits to its key. +pub trait KeyCommittingAead: AeadCore {} +/// Marker trait that signals that an AEAD commits to all its inputs. +pub trait CommittingAead: AeadCore + KeyCommittingAead {} + +#[cfg(feature = "alloc")] +#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] +mod padded_aead { + use super::KeyCommittingAead; + use aead::{AeadCore, AeadInPlace, KeyInit, KeySizeUser}; + use core::ops::{Add, Mul}; + use generic_array::typenum::{Unsigned, U3}; + use generic_array::ArrayLength; + use subtle::{Choice, ConstantTimeEq}; + + #[derive(Debug, Clone)] + /// A wrapper around a non-committing AEAD that implements the + /// [padding fix][1] of prepending zeros to the plaintext before encryption + /// and verifying their presence upon decryption. Based on the formulas + /// of [this paper][2], we append `3*key_len` zeros to obtain `3/4*key_len` + /// bits of key commitment security. + /// + /// The padding fix paper proves that this construction is key-committing + /// for AES-GCM, ChaCha20Poly1305, and other AEADs that internally use + /// primitives that can be modelled as ideal. However, security is not + /// guaranteed with weak primitives. For example, e.g. HMAC-SHA-1 can be + /// used as a MAC in normal circumstances because HMAC does not require a + /// collision resistant hash, but an AEAD using HMAC-SHA-1 to provide + /// integrity cannot be made committing using this padding scheme. + /// + /// [1]: https://eprint.iacr.org/2020/1456.pdf + /// [2]: https://csrc.nist.gov/csrc/media/Events/2023/third-workshop-on-block-cipher-modes-of-operation/documents/accepted-papers/The%20Landscape%20of%20Committing%20Authenticated%20Encryption.pdf + pub struct PaddedAead { + inner_aead: Aead, + } + impl PaddedAead { + /// Extracts the inner Aead object. + #[inline] + pub fn into_inner(self) -> Aead { + self.inner_aead + } + } + + impl KeySizeUser for PaddedAead { + type KeySize = Aead::KeySize; + } + impl KeyInit for PaddedAead { + fn new(key: &aead::Key) -> Self { + PaddedAead { + inner_aead: Aead::new(key), + } + } + } + impl AeadCore for PaddedAead + where + Aead::CiphertextOverhead: Add<>::Output>, + Aead::KeySize: Mul, + >::Output>>::Output: + ArrayLength, + { + type NonceSize = Aead::NonceSize; + + type TagSize = Aead::TagSize; + + type CiphertextOverhead = + >::Output>>::Output; + } + + impl aead::Aead for PaddedAead + where + Self: AeadCore, + { + fn encrypt<'msg, 'aad>( + &self, + nonce: &aead::Nonce, + plaintext: impl Into>, + ) -> aead::Result> { + let padding_overhead = 3 * Aead::KeySize::to_usize(); + assert_eq!( + padding_overhead + Aead::CiphertextOverhead::to_usize(), + Self::CiphertextOverhead::to_usize() + ); + + let payload = plaintext.into(); + let mut padded_msg = alloc::vec![0x00; payload.msg.len()+3*Aead::KeySize::to_usize()]; + padded_msg[padding_overhead..].copy_from_slice(payload.msg); + + // Compiler can't see that Self::NonceSize == Aead::NonceSize + let nonce_recast = aead::Nonce::::from_slice(nonce.as_slice()); + + let tag_inner = self.inner_aead.encrypt_in_place_detached( + nonce_recast, + payload.aad, + &mut padded_msg, + )?; + // Append the tag to the end + padded_msg.extend(tag_inner); + Ok(padded_msg) + } + + fn decrypt<'msg, 'aad>( + &self, + nonce: &aead::Nonce, + ciphertext: impl Into>, + ) -> aead::Result> { + let padding_overhead = 3 * Aead::KeySize::to_usize(); + let total_overhead = padding_overhead + Aead::CiphertextOverhead::to_usize(); + assert_eq!(total_overhead, Self::CiphertextOverhead::to_usize()); + + let payload = ciphertext.into(); + + // Compiler can't see that Self::NonceSize == Aead::NonceSize + let nonce_recast = aead::Nonce::::from_slice(nonce.as_slice()); + + let (ctxt, tag) = payload + .msg + .split_at(payload.msg.len() - Aead::TagSize::to_usize()); + let tag_recast = aead::Tag::::from_slice(tag); + + if ctxt.len() < total_overhead { + return Err(aead::Error); + } + + let mut ptxt_vec = alloc::vec::Vec::from(ctxt); + + // Avoid timing side channel by not returning early + let mut decryption_is_ok = Choice::from( + match self.inner_aead.decrypt_in_place_detached( + nonce_recast, + payload.aad, + &mut ptxt_vec, + tag_recast, + ) { + Ok(_) => 1, + Err(_) => 0, + }, + ); + // Check padding now + for byte in ptxt_vec.drain(..padding_overhead) { + decryption_is_ok &= byte.ct_eq(&0); + } + if decryption_is_ok.into() { + Ok(ptxt_vec) + } else { + Err(aead::Error) + } + } + } + impl aead::AeadMut for PaddedAead + where + Self: AeadCore, + { + fn encrypt<'msg, 'aad>( + &mut self, + nonce: &aead::Nonce, + plaintext: impl Into>, + ) -> aead::Result> { + let padding_overhead = 3 * Aead::KeySize::to_usize(); + assert_eq!( + padding_overhead + Aead::CiphertextOverhead::to_usize(), + Self::CiphertextOverhead::to_usize() + ); + + let payload = plaintext.into(); + let mut padded_msg = alloc::vec![0x00; payload.msg.len()+3*Aead::KeySize::to_usize()]; + padded_msg[padding_overhead..].copy_from_slice(payload.msg); + + // Compiler can't see that Self::NonceSize == Aead::NonceSize + let nonce_recast = aead::Nonce::::from_slice(nonce.as_slice()); + + let tag_inner = self.inner_aead.encrypt_in_place_detached( + nonce_recast, + payload.aad, + &mut padded_msg, + )?; + // Append the tag to the end + padded_msg.extend(tag_inner); + Ok(padded_msg) + } + fn decrypt<'msg, 'aad>( + &mut self, + nonce: &aead::Nonce, + ciphertext: impl Into>, + ) -> aead::Result> { + let padding_overhead = 3 * Aead::KeySize::to_usize(); + let total_overhead = padding_overhead + Aead::CiphertextOverhead::to_usize(); + assert_eq!(total_overhead, Self::CiphertextOverhead::to_usize()); + + let payload = ciphertext.into(); + + // Compiler can't see that Self::NonceSize == Aead::NonceSize + let nonce_recast = aead::Nonce::::from_slice(nonce.as_slice()); + + let (ctxt, tag) = payload + .msg + .split_at(payload.msg.len() - Aead::TagSize::to_usize()); + let tag_recast = aead::Tag::::from_slice(tag); + + if ctxt.len() < total_overhead { + return Err(aead::Error); + } + + let mut ptxt_vec = alloc::vec::Vec::from(ctxt); + + // Avoid timing side channel by not returning early + let mut decryption_is_ok = Choice::from( + match self.inner_aead.decrypt_in_place_detached( + nonce_recast, + payload.aad, + &mut ptxt_vec, + tag_recast, + ) { + Ok(_) => 1, + Err(_) => 0, + }, + ); + // Check padding now + for byte in ptxt_vec.drain(..padding_overhead) { + decryption_is_ok &= byte.ct_eq(&0); + } + if decryption_is_ok.into() { + Ok(ptxt_vec) + } else { + Err(aead::Error) + } + } + } + impl KeyCommittingAead for PaddedAead where Self: AeadCore {} +} + +mod ctx { + use super::{CommittingAead, KeyCommittingAead}; + use aead::{AeadCore, AeadInPlace, KeyInit, KeySizeUser}; + use core::ops::Add; + use digest::core_api::BlockSizeUser; + use digest::{Digest, FixedOutput, Mac}; + use generic_array::typenum::Unsigned; + use generic_array::ArrayLength; + use hmac::SimpleHmac; + use subtle::Choice; + + #[derive(Debug, Clone)] + /// Implementation of the encryption portion of the + /// [CTX scheme](https://eprint.iacr.org/2022/1260.pdf). + /// + /// CTX wraps an AEAD and replaces the tag with + /// `H(key || nonce || aad || orig_tag)`, which is shown in the paper to + /// commit to all AEAD inputs as long as the hash is collision resistant. + /// This provides `hash_output_len/2` bits of commitment security. + /// + /// Unfortunately, there is currently no way to get the expected tag of the + /// inner AEAD using the current trait interfaces, so this struct only + /// implements the encryption direction. This may still be useful for + /// interfacing with other programs that use the CTX committing AE scheme. + pub struct CtxAead { + inner_aead: Aead, + hasher: CrHash, + } + impl CtxAead { + /// Extracts the inner Aead object. + #[inline] + pub fn into_inner(self) -> Aead { + self.inner_aead + } + } + + impl KeySizeUser for CtxAead { + type KeySize = Aead::KeySize; + } + impl KeyInit for CtxAead { + fn new(key: &aead::Key) -> Self { + CtxAead { + inner_aead: Aead::new(key), + hasher: Digest::new_with_prefix(key), + } + } + } + + impl AeadCore for CtxAead { + type NonceSize = Aead::NonceSize; + + type TagSize = CrHash::OutputSize; + + type CiphertextOverhead = Aead::CiphertextOverhead; + } + + // TODO: don't see a way to provide impls for both AeadInPlace + // and AeadMutInPlace, as having both would conflict with the blanket impl + // Choose AeadInPlace because all the current rustcrypto/AEADs do not have + // a mutable state + impl AeadInPlace + for CtxAead + where + Self: AeadCore, + { + fn encrypt_in_place_detached( + &self, + nonce: &aead::Nonce, + associated_data: &[u8], + buffer: &mut [u8], + ) -> aead::Result> { + // Compiler can't see that Self::NonceSize == Aead::NonceSize + let nonce_recast = aead::Nonce::::from_slice(nonce.as_slice()); + + let tag_inner = + self.inner_aead + .encrypt_in_place_detached(nonce_recast, associated_data, buffer)?; + + let mut tag_computer = self.hasher.clone(); + tag_computer.update(nonce); + tag_computer.update(associated_data); + tag_computer.update(tag_inner); + let final_tag = tag_computer.finalize(); + + // Compiler can't see that Self::TagSize == Digest::OutputSize + let tag_recast = aead::Tag::::clone_from_slice(final_tag.as_slice()); + Ok(tag_recast) + } + + /// Unimplemented decryption of the message in-place, which panics if + /// called + #[allow(unused_variables)] + fn decrypt_in_place_detached( + &self, + nonce: &aead::Nonce, + associated_data: &[u8], + buffer: &mut [u8], + tag: &aead::Tag, + ) -> aead::Result<()> { + // Compiler can't see that Self::NonceSize == Aead::NonceSize + let _nonce_recast = aead::Nonce::::from_slice(nonce.as_slice()); + unimplemented!("Cannot get inner AEAD tag using current AEAD interfaces") + // Remaning steps: compute expected tag during decryption, repeat + // hasher steps analogously to encryption phase, and compare tag + } + } + + impl KeyCommittingAead for CtxAead where Self: AeadCore + {} + impl CommittingAead for CtxAead where Self: AeadCore {} + + #[derive(Debug, Clone)] + /// Implementation of a modified version of the CTX scheme. + /// + /// Instead of returning tag `H(key || nonce || aad || orig_tag)`, we return + /// `orig_tag || HMAC_key(nonce || aad || orig_tag)`. The AEAD API requires + /// that we treat the underlying AEAD as a black box, without access to the + /// expected tag at decryption time, so we have to also send it along with + /// the commitment to the other inputs to the AEAD. (Ideally, the need to + /// send `orig_tag` as well can be removed in a future version of the + /// crate.) At decryption time, we verify both `orig_tag` and the hash + /// commitment. + /// + /// ## Security analysis for the modified construction + /// + /// HMAC invokes the underlying hash function twice such that the inputs to + /// the hash functions are computed only by XOR, concatenation, and hashing. + /// Thus, if we trust the underlying hash function to serve as a commitment + /// to its inputs, we can also trust HMAC-hash to commit to its inputs and + /// provide `hash_output_len/2` bits of commitment security, as with CTX. + /// + /// If the underlying AEAD provides proper confidentiality and integrity + /// protections, we can assume that this new construction also provides + /// proper confidentiality and integrity, since it has the same ciphertext + /// and includes the original tag without exposing cryptographic secrets in + /// a recoverable form. Moreover, HMAC is supposed to be a secure keyed MAC, + /// so an attacker cannot forge a commitment without knowing the key, even + /// with full knowledge of the other input to the HMAC. + /// + /// We use `HMAC_key(nonce || aad || orig_tag)` instead of the original CTX + /// construction of `H(key || nonce || aad || orig_tag)` to mitigate length + /// extension attacks that may become possible when `orig_tag` is sent in + /// the clear (with the result that `H(key || nonce || aad || orig_tag)` + /// decomposes into `H(secret || public)`), even though returning + /// `orig_tag || H(key || nonce || aad || orig_tag)` as the tag would allow + /// for increased interoperability with other CTX implementations. (In fact, + /// revealing `orig_tag` would be fatal for the CTX+ construction which + /// omits `aad` from the `orig_tag` computation by allowing forgery of the + /// hash commitment via length extension on `aad`.) + pub struct CtxishHmacAead { + inner_aead: Aead, + hasher: SimpleHmac, + } + impl CtxishHmacAead { + /// Extracts the inner Aead object. + #[inline] + pub fn into_inner(self) -> Aead { + self.inner_aead + } + } + + impl KeySizeUser + for CtxishHmacAead + { + type KeySize = Aead::KeySize; + } + impl KeyInit + for CtxishHmacAead + { + fn new(key: &aead::Key) -> Self { + CtxishHmacAead { + inner_aead: Aead::new(key), + hasher: as KeyInit>::new_from_slice(key) + .expect("HMAC accepts all key lengths so this always works"), + } + } + } + impl AeadCore + for CtxishHmacAead + where + Aead::TagSize: Add, + >::Output: ArrayLength, + { + type NonceSize = Aead::NonceSize; + + type TagSize = >::Output; + + type CiphertextOverhead = Aead::CiphertextOverhead; + } + // TODO: don't see a way to provide impls for both AeadInPlace + // and AeadMutInPlace, as having both would conflict with the blanket impl + // Choose AeadInPlace because all the current rustcrypto/AEADs do not have + // a mutable state + impl + AeadInPlace for CtxishHmacAead + where + Self: AeadCore, + { + fn encrypt_in_place_detached( + &self, + nonce: &aead::Nonce, + associated_data: &[u8], + buffer: &mut [u8], + ) -> aead::Result> { + // Compiler can't see that Self::NonceSize == Aead::NonceSize + let nonce_recast = aead::Nonce::::from_slice(nonce.as_slice()); + + let tag_inner = + self.inner_aead + .encrypt_in_place_detached(nonce_recast, associated_data, buffer)?; + + let mut tag_computer = self.hasher.clone(); + tag_computer.update(nonce); + tag_computer.update(associated_data); + tag_computer.update(&tag_inner); + let hmac_tag = tag_computer.finalize_fixed(); + + let final_tag_iter = tag_inner.iter().copied().chain(hmac_tag); + + let final_tag = aead::Tag::::from_exact_iter(final_tag_iter) + .expect("Lengths shoud always match"); + Ok(final_tag) + } + + #[allow(unused_variables)] + fn decrypt_in_place_detached( + &self, + nonce: &aead::Nonce, + associated_data: &[u8], + buffer: &mut [u8], + tag: &aead::Tag, + ) -> aead::Result<()> { + // Compiler can't see that Self::NonceSize == Aead::NonceSize + let nonce_recast = aead::Nonce::::from_slice(nonce.as_slice()); + // Get the inner tag + let tag_inner = aead::Tag::::from_slice(&tag[..Aead::TagSize::to_usize()]); + + // Prevent timing side channels by not returning early on inner AEAD + // decryption failure + let tag_inner_is_ok = Choice::from( + match self.inner_aead.decrypt_in_place_detached( + nonce_recast, + associated_data, + buffer, + tag_inner, + ) { + Ok(_) => 1, + Err(_) => 0, + }, + ); + + let mut tag_computer = self.hasher.clone(); + tag_computer.update(nonce); + tag_computer.update(associated_data); + // At this point we know whether tag_inner has the correct value + // If it doesn't then we'll likely get a mismatch here too + // Regardless, we require both `Choice`s to be OK + // So it doesn't matter if we ingest a potentially tainted tag here + tag_computer.update(tag_inner); + + // Get the HMAC tag + let expected_hmac_tag = &tag[Aead::TagSize::to_usize()..]; + + let hmac_tag_is_ok = Choice::from(match tag_computer.verify_slice(expected_hmac_tag) { + Ok(_) => 1, + Err(_) => 0, + }); + + if (tag_inner_is_ok & hmac_tag_is_ok).into() { + Ok(()) + } else { + Err(aead::Error) + } + } + } + impl KeyCommittingAead + for CtxishHmacAead + where + Self: AeadCore, + { + } + impl CommittingAead for CtxishHmacAead where + Self: AeadCore + { + } +} From 5c82faea5b4cddaeda5475e952c84434a24409bf Mon Sep 17 00:00:00 2001 From: rlee287 Date: Mon, 30 Oct 2023 18:59:06 -0700 Subject: [PATCH 2/8] AES-GCM based concrete tests for committing AEAD constructions --- Cargo.lock | 14 +++ committing-aead/Cargo.toml | 6 + committing-aead/tests/aes128gcm_padding.rs | 41 +++++++ committing-aead/tests/aes256gcm_ctx.rs | 58 ++++++++++ committing-aead/tests/aes256gcm_ctxishhmac.rs | 41 +++++++ committing-aead/tests/helpers.rs | 104 ++++++++++++++++++ 6 files changed, 264 insertions(+) create mode 100644 committing-aead/tests/aes128gcm_padding.rs create mode 100644 committing-aead/tests/aes256gcm_ctx.rs create mode 100644 committing-aead/tests/aes256gcm_ctxishhmac.rs create mode 100644 committing-aead/tests/helpers.rs diff --git a/Cargo.lock b/Cargo.lock index 3747a340..78dbf886 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -209,9 +209,12 @@ name = "committing-aead" version = "0.1.0" dependencies = [ "aead", + "aes-gcm", "digest", "generic-array", + "hex-literal 0.3.4", "hmac", + "sha2", "subtle", ] @@ -532,6 +535,17 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "spin" version = "0.9.8" diff --git a/committing-aead/Cargo.toml b/committing-aead/Cargo.toml index 5c647aea..8a37314e 100644 --- a/committing-aead/Cargo.toml +++ b/committing-aead/Cargo.toml @@ -25,6 +25,12 @@ subtle = { version = "2.5", default-features = false } digest = { version = "0.10.7", features = ["mac"] } hmac = { version = "0.12.1" } +[dev-dependencies] +aes-gcm = { path = "../aes-gcm", version = "0.10.3" } +# Don't use hex-literal v0.4 to reduce multiple dependency versiou duplication +hex-literal = "0.3.4" +sha2 = "0.10.8" + [features] default = ["std"] alloc = ["aead/alloc"] diff --git a/committing-aead/tests/aes128gcm_padding.rs b/committing-aead/tests/aes128gcm_padding.rs new file mode 100644 index 00000000..866e4409 --- /dev/null +++ b/committing-aead/tests/aes128gcm_padding.rs @@ -0,0 +1,41 @@ +#[macro_use] +mod helpers; + +use self::helpers::TestVector; + +use aead::{Aead, AeadCore, KeyInit, Payload}; +use aes_gcm::Aes128Gcm; +use committing_aead::PaddedAead; +use generic_array::{typenum::Unsigned, GenericArray}; + +use hex_literal::hex; + +/// Copied a handful of AES-256-GCM test vectors and updated the tags via Python +const PAD_TEST_VECTORS: &[TestVector] = &[ + TestVector { + key: &hex!("11754cd72aec309bf52f7687212e8957"), + nonce: &hex!("3c819d9a9bed087615030b65"), + plaintext: &hex!(""), + aad: &hex!(""), + ciphertext: &hex!("9238D4C2EA6CC96FF204063074E345B7DE28A65EB55E6F0C98C7513A14E4F303CDB2A91A2E93125F2CFBDEDCF00BFF8D"), + tag: &hex!("80FF70A9B8804988D46EA234B3618518"), + }, + TestVector { + key: &hex!("89c949e9c804af014d5604b39459f2c8"), + nonce: &hex!("d1b104c815bf1e94e28c8f16"), + plaintext: &hex!(""), + aad: &hex!("82adcd638d3fa9d9f3e84100d61e0777"), + ciphertext: &hex!("936B6E47964ED8376F9F9988B53520CFC4BF3E4842E75618C9EC82AFA53D67EC482A2FFB50825DD323675C931C84E811"), + tag: &hex!("7AA91F2C56204AD8ECADA9A19BC31685"), + }, + TestVector { + key: &hex!("9b2ddd1af666b91e052d624b04e6b042"), + nonce: &hex!("4ee12e62899c61f9520a13c1"), + plaintext: &hex!("01e5dc87a242782ca3156a27446f386bd9a060ffef1f63c3bc11a93ce305175d"), + aad: &hex!("e591e6ee094981b0e383429a31cceaaa"), + ciphertext: &hex!("865CAACF28820F7C0947F431464635D8B6ED582A2AD3D7D12EA7C50DDA6469E3C93CCB7A22AD9A068634B8E5DB34CBD12E57448BAA46E6DC9A1616D1BB4D3D89525EDDEAA2CB2F953B8D275E6ECAA157"), + tag: &hex!("25304F1836620DBFF689E213033AA17B"), + } +]; + +tests_no_inplace!(PaddedAead, PAD_TEST_VECTORS); diff --git a/committing-aead/tests/aes256gcm_ctx.rs b/committing-aead/tests/aes256gcm_ctx.rs new file mode 100644 index 00000000..50d933e5 --- /dev/null +++ b/committing-aead/tests/aes256gcm_ctx.rs @@ -0,0 +1,58 @@ +mod helpers; + +use self::helpers::TestVector; + +use aead::{Aead, AeadCore, KeyInit, Payload}; +use aes_gcm::Aes256Gcm; +use committing_aead::CtxAead; +use generic_array::{typenum::Unsigned, GenericArray}; +use hex_literal::hex; +use sha2::Sha256; + +/// Copied a handful of AES-256-GCM test vectors and updated the tags via Python +const CTX_TEST_VECTORS: &[TestVector] = &[ + TestVector { + key: &hex!("b52c505a37d78eda5dd34f20c22540ea1b58963cf8e5bf8ffa85f9f2492505b4"), + nonce: &hex!("516c33929df5a3284ff463d7"), + plaintext: b"", + aad: b"", + ciphertext: b"", + tag: &hex!("dd63e6e6c77d5f29975803882becd40b16326b278832b72123d03ea023ad42af"), + }, + TestVector { + key: &hex!("26dc5ce74b4d64d1dc2221cdd6a63d7a9226134708299cd719a68f636b6b5ebd"), + nonce: &hex!("0294c54ff4ed30782222c834"), + plaintext: &hex!("ae4c7f040d3a5ff108e29381e7a0830221d5378b13b87ef0703c327686d30af004902d4ddb59d5787fecea4731eaa8042443d5"), + aad: &hex!("2a9fb326f98bbe2d2cf57bae9ecbeff7"), + ciphertext: &hex!("9601aec6bc6e8a09d054a01e500a4e4cdcc7c2cf83122656be7c26fc7dc1a773a40be7e8a049a6cdf059e93a23ca441ef1ca96"), + tag: &hex!("f6f0e3bcf9b603d9b434bad0cfbee9a4ea90b3e3731e466d4a0d6a3b21bc6641"), + }, + TestVector { + key: &hex!("54c31fb2fb4aab6a82ce188e6afa71a3354811099d1203fe1f991746f7342f90"), + nonce: &hex!("f0fe974bdbe1694dc3b06cc6"), + plaintext: &hex!("fbb7b3730f0cd7b1052a5298ee"), + aad: &hex!("2879e05e0f8dd4402425eabb0dc184dcd07d46d54d775d7c2b76b0f76b3eed5f7ca93c6ae71bf509c270490269ea869ed6603fdf7113aa625648ab8ed88210f8b30ec9c94bca5757ca3d77491f64109101165636b068e3095cb4"), + ciphertext: &hex!("3a5a2a8aa93c462cfb80f1f728"), + tag: &hex!("42a344ebdaa63ff7cb8cf37405cb0efae3b8a2c12113b4cb9647157220565829"), + } +]; + +#[test] +fn ctx_encryptonly() { + for vector in CTX_TEST_VECTORS { + let key = GenericArray::from_slice(vector.key); + let nonce = GenericArray::from_slice(vector.nonce); + let payload = Payload { + msg: vector.plaintext, + aad: vector.aad, + }; + + let cipher = CtxAead::::new(key); + let ciphertext = cipher.encrypt(nonce, payload).unwrap(); + let (ct, tag) = ciphertext.split_at( + ciphertext.len() - as AeadCore>::TagSize::to_usize(), + ); + assert_eq!(vector.ciphertext, ct, "ctxt mismatch"); + assert_eq!(vector.tag, tag, "tag mismatch"); + } +} diff --git a/committing-aead/tests/aes256gcm_ctxishhmac.rs b/committing-aead/tests/aes256gcm_ctxishhmac.rs new file mode 100644 index 00000000..6f736a22 --- /dev/null +++ b/committing-aead/tests/aes256gcm_ctxishhmac.rs @@ -0,0 +1,41 @@ +#[macro_use] +mod helpers; + +use self::helpers::TestVector; + +use aead::{Aead, AeadCore, AeadInPlace, KeyInit, Payload}; +use aes_gcm::Aes256Gcm; +use committing_aead::CtxishHmacAead; +use generic_array::{typenum::Unsigned, GenericArray}; +use hex_literal::hex; +use sha2::Sha256; + +/// Copied a handful of AES-256-GCM test vectors and updated the tags via Python +const CTXISH_TEST_VECTORS: &[TestVector] = &[ + TestVector { + key: &hex!("b52c505a37d78eda5dd34f20c22540ea1b58963cf8e5bf8ffa85f9f2492505b4"), + nonce: &hex!("516c33929df5a3284ff463d7"), + plaintext: b"", + aad: b"", + ciphertext: b"", + tag: &hex!("bdc1ac884d332457a1d2664f168c76f0DD2305070280150ED89E7ED6B1282479FA3A42166153B6BBCFAF0E796843B1A4"), + }, + TestVector { + key: &hex!("26dc5ce74b4d64d1dc2221cdd6a63d7a9226134708299cd719a68f636b6b5ebd"), + nonce: &hex!("0294c54ff4ed30782222c834"), + plaintext: &hex!("ae4c7f040d3a5ff108e29381e7a0830221d5378b13b87ef0703c327686d30af004902d4ddb59d5787fecea4731eaa8042443d5"), + aad: &hex!("2a9fb326f98bbe2d2cf57bae9ecbeff7"), + ciphertext: &hex!("9601aec6bc6e8a09d054a01e500a4e4cdcc7c2cf83122656be7c26fc7dc1a773a40be7e8a049a6cdf059e93a23ca441ef1ca96"), + tag: &hex!("b620a8a0c8fe6117f22735c0ca29434c186379AA20DB53D6A0CD8375FA95665F7CD9677FF690BA437E6D0092A4AF8344"), + }, + TestVector { + key: &hex!("54c31fb2fb4aab6a82ce188e6afa71a3354811099d1203fe1f991746f7342f90"), + nonce: &hex!("f0fe974bdbe1694dc3b06cc6"), + plaintext: &hex!("fbb7b3730f0cd7b1052a5298ee"), + aad: &hex!("2879e05e0f8dd4402425eabb0dc184dcd07d46d54d775d7c2b76b0f76b3eed5f7ca93c6ae71bf509c270490269ea869ed6603fdf7113aa625648ab8ed88210f8b30ec9c94bca5757ca3d77491f64109101165636b068e3095cb4"), + ciphertext: &hex!("3a5a2a8aa93c462cfb80f1f728"), + tag: &hex!("59ef9d54ee01fb6cd54bd0e08f74096fE292175F750A80FD39F7E0F8EAA075DF471481ED2B780DE8BC2EAB0DA3032288"), + } +]; + +tests_with_inplace!(CtxishHmacAead, CTXISH_TEST_VECTORS); diff --git a/committing-aead/tests/helpers.rs b/committing-aead/tests/helpers.rs new file mode 100644 index 00000000..f842c501 --- /dev/null +++ b/committing-aead/tests/helpers.rs @@ -0,0 +1,104 @@ +// Code modified from aes-gcm test helpers + +#[derive(Debug)] +pub struct TestVector { + pub key: &'static [u8], + pub nonce: &'static [u8], + pub aad: &'static [u8], + pub plaintext: &'static [u8], + pub ciphertext: &'static [u8], + pub tag: &'static [u8], +} + +#[macro_export] +macro_rules! tests_no_inplace { + ($aead:ty, $vectors:expr) => { + #[test] + fn encrypt() { + for vector in $vectors { + let key = GenericArray::from_slice(vector.key); + let nonce = GenericArray::from_slice(vector.nonce); + let payload = Payload { + msg: vector.plaintext, + aad: vector.aad, + }; + + let cipher = <$aead>::new(key); + let ciphertext = cipher.encrypt(nonce, payload).unwrap(); + let (ct, tag) = ciphertext + .split_at(ciphertext.len() - <$aead as AeadCore>::TagSize::to_usize()); + assert_eq!(vector.ciphertext, ct, "ctxt mismatch"); + assert_eq!(vector.tag, tag, "tag mismatch"); + } + } + + #[test] + fn decrypt() { + for vector in $vectors { + let key = GenericArray::from_slice(vector.key); + let nonce = GenericArray::from_slice(vector.nonce); + let mut ciphertext = Vec::from(vector.ciphertext); + ciphertext.extend_from_slice(vector.tag); + + let payload = Payload { + msg: &ciphertext, + aad: vector.aad, + }; + + let cipher = <$aead>::new(key); + let plaintext = cipher.decrypt(nonce, payload).unwrap(); + + assert_eq!(vector.plaintext, plaintext.as_slice()); + } + } + + #[test] + fn decrypt_modified() { + let vector = &$vectors[0]; + let key = GenericArray::from_slice(vector.key); + let nonce = GenericArray::from_slice(vector.nonce); + + let mut ciphertext = Vec::from(vector.ciphertext); + ciphertext.extend_from_slice(vector.tag); + + // Tweak the first byte + ciphertext[0] ^= 0xaa; + + let payload = Payload { + msg: &ciphertext, + aad: vector.aad, + }; + + let cipher = <$aead>::new(key); + assert!(cipher.decrypt(nonce, payload).is_err()); + } + }; +} + +#[macro_export] +macro_rules! tests_with_inplace { + ($aead:ty, $vectors:expr) => { + tests_no_inplace!($aead, $vectors); + + #[test] + fn decrypt_in_place_detached_modified() { + let vector = &$vectors.iter().last().unwrap(); + let key = GenericArray::from_slice(vector.key); + let nonce = GenericArray::from_slice(vector.nonce); + + let mut buffer = Vec::from(vector.ciphertext); + assert!(!buffer.is_empty()); + + // Tweak the first byte + let mut tag = GenericArray::clone_from_slice(vector.tag); + tag[0] ^= 0xaa; + + let cipher = <$aead>::new(key); + assert!(cipher + .decrypt_in_place_detached(nonce, &[], &mut buffer, &tag) + .is_err()); + + assert_eq!(vector.ciphertext, buffer); + } + }; +} From 23f6d70fcf096ee748482a2424c04dea4415a1b6 Mon Sep 17 00:00:00 2001 From: rlee287 Date: Mon, 30 Oct 2023 23:11:55 -0700 Subject: [PATCH 3/8] ChaCha20Poly1305 based concrete tests for committing AEAD constructions --- Cargo.lock | 1 + committing-aead/Cargo.toml | 3 +- committing-aead/tests/chacha20poly1305_ctx.rs | 64 +++++++++++++++++++ .../tests/chacha20poly1305_ctxishhmac.rs | 47 ++++++++++++++ .../tests/chacha20poly1305_padding.rs | 39 +++++++++++ 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 committing-aead/tests/chacha20poly1305_ctx.rs create mode 100644 committing-aead/tests/chacha20poly1305_ctxishhmac.rs create mode 100644 committing-aead/tests/chacha20poly1305_padding.rs diff --git a/Cargo.lock b/Cargo.lock index 78dbf886..bf5270e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,7 @@ version = "0.1.0" dependencies = [ "aead", "aes-gcm", + "chacha20poly1305", "digest", "generic-array", "hex-literal 0.3.4", diff --git a/committing-aead/Cargo.toml b/committing-aead/Cargo.toml index 8a37314e..76456859 100644 --- a/committing-aead/Cargo.toml +++ b/committing-aead/Cargo.toml @@ -27,7 +27,8 @@ hmac = { version = "0.12.1" } [dev-dependencies] aes-gcm = { path = "../aes-gcm", version = "0.10.3" } -# Don't use hex-literal v0.4 to reduce multiple dependency versiou duplication +chacha20poly1305 = { path = "../chacha20poly1305", version = "0.10.1" } +# Don't use hex-literal v0.4 to reduce multiple dependency version duplication hex-literal = "0.3.4" sha2 = "0.10.8" diff --git a/committing-aead/tests/chacha20poly1305_ctx.rs b/committing-aead/tests/chacha20poly1305_ctx.rs new file mode 100644 index 00000000..5eb33057 --- /dev/null +++ b/committing-aead/tests/chacha20poly1305_ctx.rs @@ -0,0 +1,64 @@ +#[macro_use] +mod helpers; + +use self::helpers::TestVector; + +use aead::{Aead, AeadCore, KeyInit, Payload}; +use chacha20poly1305::ChaCha20Poly1305; +use committing_aead::CtxAead; +use generic_array::{typenum::Unsigned, GenericArray}; +use hex_literal::hex; +use sha2::Sha256; + +const KEY: &[u8; 32] = &[ + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, +]; + +const AAD: &[u8; 12] = &[ + 0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, +]; + +const PLAINTEXT: &[u8] = b"Ladies and Gentlemen of the class of '99: \ + If I could offer you only one tip for the future, sunscreen would be it."; + +/// Copied non-Wycheproof ChaCha20Poly1305 test vector and updated the tags +const CTX_TEST_VECTORS: &[TestVector] = &[TestVector { + key: KEY, + nonce: &[ + 0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + ], + plaintext: PLAINTEXT, + aad: AAD, + ciphertext: &[ + 0xd3, 0x1a, 0x8d, 0x34, 0x64, 0x8e, 0x60, 0xdb, 0x7b, 0x86, 0xaf, 0xbc, 0x53, 0xef, 0x7e, + 0xc2, 0xa4, 0xad, 0xed, 0x51, 0x29, 0x6e, 0x08, 0xfe, 0xa9, 0xe2, 0xb5, 0xa7, 0x36, 0xee, + 0x62, 0xd6, 0x3d, 0xbe, 0xa4, 0x5e, 0x8c, 0xa9, 0x67, 0x12, 0x82, 0xfa, 0xfb, 0x69, 0xda, + 0x92, 0x72, 0x8b, 0x1a, 0x71, 0xde, 0x0a, 0x9e, 0x06, 0x0b, 0x29, 0x05, 0xd6, 0xa5, 0xb6, + 0x7e, 0xcd, 0x3b, 0x36, 0x92, 0xdd, 0xbd, 0x7f, 0x2d, 0x77, 0x8b, 0x8c, 0x98, 0x03, 0xae, + 0xe3, 0x28, 0x09, 0x1b, 0x58, 0xfa, 0xb3, 0x24, 0xe4, 0xfa, 0xd6, 0x75, 0x94, 0x55, 0x85, + 0x80, 0x8b, 0x48, 0x31, 0xd7, 0xbc, 0x3f, 0xf4, 0xde, 0xf0, 0x8e, 0x4b, 0x7a, 0x9d, 0xe5, + 0x76, 0xd2, 0x65, 0x86, 0xce, 0xc6, 0x4b, 0x61, 0x16, + ], + tag: &hex!("1c294d148a70af27feddc787f08f28466c5f744f813673f749fc7963d5714da4"), +}]; + +#[test] +fn ctx_encryptonly() { + for vector in CTX_TEST_VECTORS { + let key = GenericArray::from_slice(vector.key); + let nonce = GenericArray::from_slice(vector.nonce); + let payload = Payload { + msg: vector.plaintext, + aad: vector.aad, + }; + + let cipher = CtxAead::::new(key); + let ciphertext = cipher.encrypt(nonce, payload).unwrap(); + let (ct, tag) = ciphertext.split_at( + ciphertext.len() - as AeadCore>::TagSize::to_usize(), + ); + assert_eq!(vector.ciphertext, ct, "ctxt mismatch"); + assert_eq!(vector.tag, tag, "tag mismatch"); + } +} diff --git a/committing-aead/tests/chacha20poly1305_ctxishhmac.rs b/committing-aead/tests/chacha20poly1305_ctxishhmac.rs new file mode 100644 index 00000000..66dc4d0a --- /dev/null +++ b/committing-aead/tests/chacha20poly1305_ctxishhmac.rs @@ -0,0 +1,47 @@ +#[macro_use] +mod helpers; + +use self::helpers::TestVector; + +use aead::{Aead, AeadCore, AeadInPlace, KeyInit, Payload}; +use chacha20poly1305::ChaCha20Poly1305; +use committing_aead::CtxishHmacAead; +use generic_array::{typenum::Unsigned, GenericArray}; +use hex_literal::hex; +use sha2::Sha256; + +const KEY: &[u8; 32] = &[ + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, +]; + +const AAD: &[u8; 12] = &[ + 0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, +]; + +const PLAINTEXT: &[u8] = b"Ladies and Gentlemen of the class of '99: \ + If I could offer you only one tip for the future, sunscreen would be it."; + +/// Copied non-Wycheproof ChaCha20Poly1305 test vector and updated the tags +const CTXISH_TEST_VECTORS: &[TestVector] = &[TestVector { + key: KEY, + nonce: &[ + 0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + ], + plaintext: PLAINTEXT, + aad: AAD, + ciphertext: &[ + 0xd3, 0x1a, 0x8d, 0x34, 0x64, 0x8e, 0x60, 0xdb, 0x7b, 0x86, 0xaf, 0xbc, 0x53, 0xef, 0x7e, + 0xc2, 0xa4, 0xad, 0xed, 0x51, 0x29, 0x6e, 0x08, 0xfe, 0xa9, 0xe2, 0xb5, 0xa7, 0x36, 0xee, + 0x62, 0xd6, 0x3d, 0xbe, 0xa4, 0x5e, 0x8c, 0xa9, 0x67, 0x12, 0x82, 0xfa, 0xfb, 0x69, 0xda, + 0x92, 0x72, 0x8b, 0x1a, 0x71, 0xde, 0x0a, 0x9e, 0x06, 0x0b, 0x29, 0x05, 0xd6, 0xa5, 0xb6, + 0x7e, 0xcd, 0x3b, 0x36, 0x92, 0xdd, 0xbd, 0x7f, 0x2d, 0x77, 0x8b, 0x8c, 0x98, 0x03, 0xae, + 0xe3, 0x28, 0x09, 0x1b, 0x58, 0xfa, 0xb3, 0x24, 0xe4, 0xfa, 0xd6, 0x75, 0x94, 0x55, 0x85, + 0x80, 0x8b, 0x48, 0x31, 0xd7, 0xbc, 0x3f, 0xf4, 0xde, 0xf0, 0x8e, 0x4b, 0x7a, 0x9d, 0xe5, + 0x76, 0xd2, 0x65, 0x86, 0xce, 0xc6, 0x4b, 0x61, 0x16, + ], + tag: &hex!("1AE10B594F09E26A7E902ECBD0600691" + "AAB1642E12099EE049E8E0C3B25944D4DE3AA85428DE5E3CBB97D571B9FC7073"), +}]; + +tests_with_inplace!(CtxishHmacAead, CTXISH_TEST_VECTORS); diff --git a/committing-aead/tests/chacha20poly1305_padding.rs b/committing-aead/tests/chacha20poly1305_padding.rs new file mode 100644 index 00000000..9a38daba --- /dev/null +++ b/committing-aead/tests/chacha20poly1305_padding.rs @@ -0,0 +1,39 @@ +#[macro_use] +mod helpers; + +use self::helpers::TestVector; + +use aead::{Aead, AeadCore, KeyInit, Payload}; +use chacha20poly1305::ChaCha20Poly1305; +use committing_aead::PaddedAead; +use generic_array::{typenum::Unsigned, GenericArray}; + +use hex_literal::hex; + +const KEY: &[u8; 32] = &[ + 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, + 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, +]; + +const AAD: &[u8; 12] = &[ + 0x50, 0x51, 0x52, 0x53, 0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, +]; + +const PLAINTEXT: &[u8] = b"Ladies and Gentlemen of the class of '99: \ + If I could offer you only one tip for the future, sunscreen would be it."; + +/// Copied non-Wycheproof ChaCha20Poly1305 test vector and updated the ctxt and tags +const PAD_TEST_VECTORS: &[TestVector] = &[ + TestVector { + key: KEY, + nonce: &[ + 0x07, 0x00, 0x00, 0x00, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + ], + plaintext: PLAINTEXT, + aad: AAD, + ciphertext: &hex!("9F7BE95D01FD40BA15E28FFB36810AAEC1C0883F09016EDEDD8AD087558203A54E9ECB38AC8E5E2BB8DAB20FFADB52E87504B26EBE696D4F60A485CF11B81B59FCB1C45F4219EEACEC6ADEC34E6669788EDB41C49CA301E127E0ACAB3B44B9CF10E7DFFC85182D93FE7E960281C5924E7055696DEE5A0AF7D21E06DBBE839E1706D4103059B9F4FA654C6F39710A5040AFFC78223BB7863C9065F5D756D3DADBE9210BD0B6D45CEB0B3114DB9DB40B5822C93B3B5B66BD7F18CF4AA3A47CE8C5E000AF7697335503FBFB5F43BA21B67AF13F"), + tag: &hex!("CCEC51BFFC65E60810F678FB4F73FA3B"), + }, +]; + +tests_no_inplace!(PaddedAead, PAD_TEST_VECTORS); From b94491bb559096a1d5209f7dab2835f663ef6b62 Mon Sep 17 00:00:00 2001 From: rlee287 Date: Tue, 31 Oct 2023 11:18:14 -0700 Subject: [PATCH 4/8] Add size overhead column to README table listing committing AEAD constructions --- committing-aead/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/committing-aead/README.md b/committing-aead/README.md index d268230b..4fa2e49d 100644 --- a/committing-aead/README.md +++ b/committing-aead/README.md @@ -7,13 +7,13 @@ when they are necessary. ## About -The following constructions are supported: +The following constructions are included: -| Committing AEAD Construction | Encryption? | Decryption? | Commitment Security | -|------------------------------|-------------|-------------|---------------------| -| ["Padding Fix"] | Yes | Yes | Key only | -| [CTX] | Yes | No | All inputs | -| CTXish-HMAC (see code docs) | Yes | Yes | All inputs | +| Committing AEAD Construction | Encryption? | Decryption? | Commitment Security | Overhead | +|------------------------------|-------------|-------------|---------------------|---------| +| ["Padding Fix"] | Yes | Yes | Key only | `ctxt+=3*key_size` | +| [CTX] | Yes | No | All inputs | `tag+=hash_len-orig_tag_len` | +| CTXish-HMAC (see code docs) | Yes | Yes | All inputs | `tag+=hash_len` | CTX decryption is not implemented because verifying the tag at decryption time requires accessing the expected original tag of the wrapped AEAD, which is From c058c9b73c4d9dae8a2a2dd0ee7257baea193beb Mon Sep 17 00:00:00 2001 From: rlee287 Date: Tue, 31 Oct 2023 11:27:59 -0700 Subject: [PATCH 5/8] Add an (untested) Github actions YAML for the new committing-aead crate --- .github/workflows/committing-aead.yml | 69 +++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 .github/workflows/committing-aead.yml diff --git a/.github/workflows/committing-aead.yml b/.github/workflows/committing-aead.yml new file mode 100644 index 00000000..79094127 --- /dev/null +++ b/.github/workflows/committing-aead.yml @@ -0,0 +1,69 @@ +name: committing-aead + +on: + pull_request: + paths: + - "committing-aead/**" + - "aes-gcm/**" + - "chacha20poly1305/**" + - "Cargo.*" + push: + branches: master + +defaults: + run: + working-directory: committing-aead + +env: + CARGO_INCREMENTAL: 0 + RUSTFLAGS: "-Dwarnings" + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + rust: + - 1.56.0 # MSRV + - stable + target: + - armv7a-none-eabi + - thumbv7em-none-eabi + - wasm32-unknown-unknown + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + targets: ${{ matrix.target }} + - run: cargo build --no-default-features --release --target ${{ matrix.target }} + + test: + runs-on: ubuntu-latest + strategy: + matrix: + include: + # 32-bit Linux + - target: i686-unknown-linux-gnu + rust: 1.56.0 # MSRV + deps: sudo apt update && sudo apt install gcc-multilib + - target: i686-unknown-linux-gnu + rust: stable + deps: sudo apt update && sudo apt install gcc-multilib + + # 64-bit Linux + - target: x86_64-unknown-linux-gnu + rust: 1.56.0 # MSRV + - target: x86_64-unknown-linux-gnu + rust: stable + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: ${{ matrix.rust }} + targets: ${{ matrix.target }} + - run: ${{ matrix.deps }} + - run: cargo test --target ${{ matrix.target }} --release + - run: cargo test --target ${{ matrix.target }} --release --features stream,std,zeroize + - run: cargo test --target ${{ matrix.target }} --release --all-features + - run: cargo build --target ${{ matrix.target }} --benches From a76e29f7c9e1d4858213b606c96a5209da023aef Mon Sep 17 00:00:00 2001 From: rlee287 Date: Tue, 31 Oct 2023 12:50:43 -0700 Subject: [PATCH 6/8] Fix Github actions test failures for committing AEADs --- .github/workflows/committing-aead.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/committing-aead.yml b/.github/workflows/committing-aead.yml index 79094127..e8ee14a4 100644 --- a/.github/workflows/committing-aead.yml +++ b/.github/workflows/committing-aead.yml @@ -64,6 +64,4 @@ jobs: targets: ${{ matrix.target }} - run: ${{ matrix.deps }} - run: cargo test --target ${{ matrix.target }} --release - - run: cargo test --target ${{ matrix.target }} --release --features stream,std,zeroize - run: cargo test --target ${{ matrix.target }} --release --all-features - - run: cargo build --target ${{ matrix.target }} --benches From 5e53ed93a30181a59871ca4e27a8e25b4ed8df83 Mon Sep 17 00:00:00 2001 From: rlee287 Date: Tue, 31 Oct 2023 14:19:32 -0700 Subject: [PATCH 7/8] Disable default features of dev-dependencies to fix build errors on no-std targets Annoying consequence of still using Cargo resolver v1 instead of v2 --- committing-aead/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/committing-aead/Cargo.toml b/committing-aead/Cargo.toml index 76456859..eec61bdf 100644 --- a/committing-aead/Cargo.toml +++ b/committing-aead/Cargo.toml @@ -26,11 +26,11 @@ digest = { version = "0.10.7", features = ["mac"] } hmac = { version = "0.12.1" } [dev-dependencies] -aes-gcm = { path = "../aes-gcm", version = "0.10.3" } -chacha20poly1305 = { path = "../chacha20poly1305", version = "0.10.1" } +aes-gcm = { path = "../aes-gcm", version = "0.10.3", default-features = false, features = ["aes"] } +chacha20poly1305 = { path = "../chacha20poly1305", version = "0.10.1", default-features = false } # Don't use hex-literal v0.4 to reduce multiple dependency version duplication hex-literal = "0.3.4" -sha2 = "0.10.8" +sha2 = { version = "0.10.8", default-features = false } [features] default = ["std"] From feb7d3a3d2bdeef0faecad60adfdff395316f202 Mon Sep 17 00:00:00 2001 From: rlee287 Date: Tue, 21 Nov 2023 22:33:12 -0800 Subject: [PATCH 8/8] Use typenum operator_aliases to make the array length summation types more readable --- committing-aead/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/committing-aead/src/lib.rs b/committing-aead/src/lib.rs index 0f13dfc2..5b796ec6 100644 --- a/committing-aead/src/lib.rs +++ b/committing-aead/src/lib.rs @@ -63,6 +63,7 @@ mod padded_aead { use super::KeyCommittingAead; use aead::{AeadCore, AeadInPlace, KeyInit, KeySizeUser}; use core::ops::{Add, Mul}; + use generic_array::typenum::operator_aliases::{Prod, Sum}; use generic_array::typenum::{Unsigned, U3}; use generic_array::ArrayLength; use subtle::{Choice, ConstantTimeEq}; @@ -107,17 +108,15 @@ mod padded_aead { } impl AeadCore for PaddedAead where - Aead::CiphertextOverhead: Add<>::Output>, + Aead::CiphertextOverhead: Add>, Aead::KeySize: Mul, - >::Output>>::Output: - ArrayLength, + Sum>: ArrayLength, { type NonceSize = Aead::NonceSize; type TagSize = Aead::TagSize; - type CiphertextOverhead = - >::Output>>::Output; + type CiphertextOverhead = Sum>; } impl aead::Aead for PaddedAead @@ -288,6 +287,7 @@ mod ctx { use core::ops::Add; use digest::core_api::BlockSizeUser; use digest::{Digest, FixedOutput, Mac}; + use generic_array::typenum::operator_aliases::Sum; use generic_array::typenum::Unsigned; use generic_array::ArrayLength; use hmac::SimpleHmac; @@ -463,11 +463,11 @@ mod ctx { for CtxishHmacAead where Aead::TagSize: Add, - >::Output: ArrayLength, + Sum: ArrayLength, { type NonceSize = Aead::NonceSize; - type TagSize = >::Output; + type TagSize = Sum; type CiphertextOverhead = Aead::CiphertextOverhead; }