├── .clippy.toml ├── .github └── workflows │ ├── ci.yml │ └── security_audit.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── benches.rs ├── examples ├── agility.rs └── client_server.rs ├── src ├── aead.rs ├── aead │ ├── aes_gcm.rs │ ├── chacha20_poly1305.rs │ └── export_only.rs ├── dhkex.rs ├── dhkex │ ├── ecdh_nistp.rs │ └── x25519.rs ├── kat_tests.rs ├── kdf.rs ├── kem.rs ├── kem │ └── dhkem.rs ├── lib.rs ├── op_mode.rs ├── setup.rs ├── single_shot.rs ├── test_util.rs └── util.rs └── test-vectors-5f503c5.json /.clippy.toml: -------------------------------------------------------------------------------- 1 | type-complexity-threshold = 320 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ '**' ] 6 | paths-ignore: ["README.md"] 7 | pull_request: 8 | branches: [ '**' ] 9 | paths-ignore: ["README.md"] 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | platform: 16 | - ubuntu-latest 17 | - macos-latest 18 | - windows-latest 19 | toolchain: 20 | - stable 21 | - beta 22 | - nightly 23 | runs-on: ${{ matrix.platform }} 24 | steps: 25 | - name: Checkout sources 26 | uses: actions/checkout@v1 27 | 28 | - name: Install toolchain 29 | uses: actions-rs/toolchain@v1 30 | with: 31 | toolchain: ${{ matrix.toolchain }} 32 | override: true 33 | 34 | - name: Run cargo test with just X25519 enabled 35 | env: 36 | CARGO_INCREMENTAL: 0 37 | RUSTFLAGS: -D warnings -A dead_code -A unused_imports 38 | run: cargo test --no-default-features --features="x25519" 39 | 40 | - name: Run cargo test with just P256 enabled 41 | env: 42 | CARGO_INCREMENTAL: 0 43 | RUSTFLAGS: -D warnings -A dead_code -A unused_imports 44 | run: cargo test --no-default-features --features="p256" 45 | 46 | - name: Run cargo test with just P384 enabled 47 | env: 48 | CARGO_INCREMENTAL: 0 49 | RUSTFLAGS: -D warnings -A dead_code -A unused_imports 50 | run: cargo test --no-default-features --features="p384" 51 | 52 | - name: Run cargo test with just P521 enabled 53 | env: 54 | CARGO_INCREMENTAL: 0 55 | RUSTFLAGS: -D warnings -A dead_code -A unused_imports 56 | run: cargo test --no-default-features --features="p521" 57 | 58 | - name: Run cargo test with all features enabled 59 | env: 60 | CARGO_INCREMENTAL: 0 61 | RUSTFLAGS: -D warnings -A dead_code -A unused_imports 62 | run: cargo test --all-features 63 | 64 | - name: Run cargo build with all features 65 | env: 66 | CARGO_INCREMENTAL: 0 67 | RUSTFLAGS: -D warnings 68 | run: cargo build --all-features 69 | rustfmt: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - name: Checkout sources 73 | uses: actions/checkout@v1 74 | 75 | - name: Install stable toolchain 76 | uses: actions-rs/toolchain@v1 77 | with: 78 | profile: minimal 79 | toolchain: stable 80 | components: rustfmt 81 | 82 | - name: Run cargo fmt 83 | uses: actions-rs/cargo@v1 84 | with: 85 | command: fmt 86 | args: --all -- --check 87 | 88 | # Enable this once x25519-dalek has another 2.0-pre.X release 89 | #msrv: 90 | # name: Current MSRV is 1.65.0 91 | # runs-on: ubuntu-latest 92 | # steps: 93 | # - uses: actions/checkout@v3 94 | # # First run `cargo +nightly -Z minimal-verisons check` in order to get a 95 | # # Cargo.lock with the oldest possible deps 96 | # - uses: dtolnay/rust-toolchain@nightly 97 | # - run: cargo -Z minimal-versions check --all-features 98 | # # Now check that `cargo build` works with respect to the oldest possible 99 | # # deps and the stated MSRV 100 | # - uses: dtolnay/rust-toolchain@1.65.0 101 | # - run: cargo build --all-features 102 | 103 | clippy: 104 | runs-on: ubuntu-latest 105 | steps: 106 | - uses: actions/checkout@v1 107 | - uses: actions-rs/toolchain@v1 108 | with: 109 | profile: minimal 110 | toolchain: stable 111 | components: clippy 112 | - run: cargo clippy --all-features -- -D warnings 113 | -------------------------------------------------------------------------------- /.github/workflows/security_audit.yml: -------------------------------------------------------------------------------- 1 | name: Security Audit 2 | on: 3 | pull_request: 4 | paths: 5 | - '**/Cargo.toml' 6 | push: 7 | paths: 8 | - '**/Cargo.toml' 9 | jobs: 10 | security_audit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v1 14 | - uses: actions-rs/audit-check@v1 15 | with: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [0.13.0] - 2025-02-19 8 | 9 | ### Changes 10 | 11 | * Made `PskBundle` require an explicit constructor that performs validation on inputs 12 | * Updated `rand` and `rand_core` to 0.9 13 | 14 | ## [0.12.0] - 2024-07-03 15 | 16 | ### Additions 17 | 18 | * Added `Serializable::write_exact` so serialization requires less stack space 19 | * Added support for the P-521 curve 20 | 21 | ### Changes 22 | 23 | * Constrained `Aead::AeadImpl` to be `Send + Sync` 24 | * Bumped `subtle` dependency and removed `byteorder` dependency 25 | 26 | ### Removals 27 | 28 | * Removed all impls of `serde::{Serialize, Deserailize}` from crate. See [wiki](https://github.com/rozbb/rust-hpke/wiki/Migrating-away-from-the-serde_impls-feature) for migration instructions. 29 | 30 | ## [0.11.0] - 2023-10-11 31 | 32 | ### Removals 33 | 34 | * Removed the redundant re-export of the first encapsulated key type as `kem::EncappedKey` 35 | 36 | ### Changes 37 | 38 | * Updated `x25519-dalek` to 2.0 39 | * Updated `subtle` to 2.5 40 | 41 | ## [0.10.0] - 2022-10-01 42 | 43 | ### Additions 44 | * Added `alloc` feature and feature-gated the `open()` and `seal()` methods behind it 45 | 46 | ### Changes 47 | * Bumped MSRV from 1.56.1 (`59eed8a2a` 2021-11-01) to 1.57.0 (`f1edd0429` 2021-11-29) 48 | * Updated dependencies and weakened `zeroize` dependency from `>=1.3` to just `^1` 49 | * Improved documentation for the AEAD `export()` method and the KDF `labeled_expand()` method 50 | 51 | ## [0.9.0] - 2022-05-04 52 | 53 | ### Additions 54 | * Refactored some internals so end users can theoretically define their own KEMs. See PR [#27](https://github.com/rozbb/rust-hpke/pull/27). 55 | 56 | ### Changes 57 | * Bumped MSRV from 1.51.0 (`2fd73fabe` 2021-03-23) to 1.56.1 (`59eed8a2a` 2021-11-01) 58 | * Updated dependencies 59 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "hpke" 3 | repository = "https://github.com/rozbb/rust-hpke" 4 | documentation = "https://docs.rs/hpke" 5 | description = "An implementation of the HPKE hybrid encryption standard (RFC 9180) in pure Rust" 6 | readme = "README.md" 7 | version = "0.13.0" 8 | authors = ["Michael Rosenberg "] 9 | edition = "2021" 10 | license = "MIT/Apache-2.0" 11 | keywords = ["cryptography", "crypto", "key-exchange", "encryption", "aead"] 12 | categories = ["cryptography", "no-std"] 13 | 14 | [features] 15 | # "p256" enables the use of ECDH-NIST-P256 as a KEM 16 | # "p384" enables the use of ECDH-NIST-P384 as a KEM 17 | # "x25519" enables the use of the X25519 as a KEM 18 | default = ["alloc", "p256", "x25519"] 19 | x25519 = ["dep:x25519-dalek"] 20 | p384 = ["dep:p384"] 21 | p256 = ["dep:p256"] 22 | p521 = ["dep:p521"] 23 | # Include allocating methods like open() and seal() 24 | alloc = [] 25 | # Includes an implementation of `std::error::Error` for `HpkeError`. Also does what `alloc` does. 26 | std = [] 27 | 28 | [dependencies] 29 | aead = "0.5" 30 | aes-gcm = "0.10" 31 | chacha20poly1305 = "0.10" 32 | generic-array = { version = "0.14", default-features = false } 33 | digest = "0.10" 34 | hkdf = "0.12" 35 | hmac = "0.12" 36 | rand_core = { version = "0.9", default-features = false } 37 | p256 = { version = "0.13", default-features = false, features = [ 38 | "arithmetic", 39 | "ecdh", 40 | ], optional = true } 41 | p384 = { version = "0.13", default-features = false, features = [ 42 | "arithmetic", 43 | "ecdh", 44 | ], optional = true } 45 | p521 = { version = "0.13", default-features = false, features = [ 46 | "arithmetic", 47 | "ecdh", 48 | ], optional = true } 49 | sha2 = { version = "0.10", default-features = false } 50 | subtle = { version = "2.6", default-features = false } 51 | x25519-dalek = { version = "2", default-features = false, features = [ 52 | "static_secrets", 53 | ], optional = true } 54 | zeroize = { version = "1", default-features = false, features = [ 55 | "zeroize_derive", 56 | ] } 57 | 58 | [dev-dependencies] 59 | criterion = { version = "0.5", features = ["html_reports"] } 60 | hex = "0.4" 61 | hex-literal = "0.4" 62 | serde = { version = "1.0", features = ["derive"] } 63 | serde_json = "1.0" 64 | rand = { version = "0.9", default-features = false, features = [ 65 | "os_rng", 66 | "std_rng", 67 | ] } 68 | 69 | [[example]] 70 | name = "client_server" 71 | required-features = ["x25519"] 72 | 73 | [[example]] 74 | name = "agility" 75 | required-features = ["p256", "p384", "p521", "x25519"] 76 | 77 | # Tell docs.rs to build docs with `--all-features` and `--cfg docsrs` (for nightly docs features) 78 | [package.metadata.docs.rs] 79 | all-features = true 80 | rustdoc-args = ["--cfg", "docsrs"] 81 | 82 | # Criteron benches 83 | [[bench]] 84 | name = "benches" 85 | harness = false 86 | 87 | [lib] 88 | bench = false 89 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Michael Rosenberg 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in 4 | compliance with the License. You may obtain a copy of the License at 5 | 6 | https://www.apache.org/licenses/LICENSE-2.0 7 | 8 | Unless required by applicable law or agreed to in writing, software distributed under the License is 9 | distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | implied. See the License for the specific language governing permissions and limitations under the 11 | License. 12 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Michael Rosenberg 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 6 | associated documentation files (the "Software"), to deal in the Software without restriction, 7 | including without limitation the rights to use, copy, modify, merge, publish, distribute, 8 | sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all copies or substantial 12 | portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 15 | NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 16 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES 17 | OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | rust-hpke 2 | ========= 3 | [![Version](https://img.shields.io/crates/v/hpke.svg)](https://crates.io/crates/hpke) 4 | [![Docs](https://docs.rs/hpke/badge.svg)](https://docs.rs/hpke) 5 | [![CI](https://github.com/rozbb/rust-hpke/workflows/CI/badge.svg)](https://github.com/rozbb/rust-hpke/actions) 6 | 7 | This is an implementation of the [HPKE](https://www.rfc-editor.org/rfc/rfc9180.html) hybrid encryption standard (RFC 9180). 8 | 9 | Warning 10 | ------- 11 | 12 | This crate has not been formally audited. Cloudflare [did a security](https://blog.cloudflare.com/using-hpke-to-encrypt-request-payloads/) review of version 0.8, though: 13 | 14 | > The HPKE implementation we decided on comes with the caveat of not yet being 15 | > formally audited, so we performed our own internal security review. We 16 | > analyzed the cryptography primitives being used and the corresponding 17 | > libraries. Between the composition of said primitives and secure programming 18 | > practices like correctly zeroing memory and safe usage of random number 19 | > generators, we found no security issues. 20 | 21 | What it implements 22 | ------------------ 23 | 24 | This implementation complies with the [HPKE standard](https://www.rfc-editor.org/rfc/rfc9180.html) (RFC 9180). 25 | 26 | Here are all the primitives listed in the spec. The primitives with checked boxes are the ones that are implemented. 27 | 28 | * KEMs 29 | - [X] DHKEM(Curve25519, HKDF-SHA256) 30 | - [ ] DHKEM(Curve448, HKDF-SHA512) 31 | - [X] DHKEM(P-256, HKDF-SHA256) 32 | - [X] DHKEM(P-384, HKDF-SHA384) 33 | - [X] DHKEM(P-521, HKDF-SHA512) 34 | * KDFs 35 | - [X] HKDF-SHA256 36 | - [X] HKDF-SHA384 37 | - [X] HKDF-SHA512 38 | * AEADs 39 | - [X] AES-GCM-128 40 | - [X] AES-GCM-256 41 | - [X] ChaCha20Poly1305 42 | 43 | Crate Features 44 | -------------- 45 | 46 | Default features flags: `alloc`, `x25519`, `p256`. 47 | 48 | Feature flag list: 49 | 50 | * `alloc` - Includes allocating methods like `AeadCtxR::open()` and `AeadCtxS::seal()` 51 | * `x25519` - Enables X25519-based KEMs 52 | * `p256` - Enables NIST P-256-based KEMs 53 | * `p384` - Enables NIST P-384-based KEMs 54 | * `p521` - Enables NIST P-521-based KEMs 55 | * `std` - Includes an implementation of `std::error::Error` for `HpkeError`. Also does what `alloc` does. 56 | 57 | For info on how to omit or include feature flags, see the [cargo docs on features](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#choosing-features). 58 | 59 | Usage Examples 60 | -------------- 61 | 62 | See the [client-server](examples/client_server.rs) example for an idea of how to use HPKE. 63 | 64 | Breaking changes 65 | ---------------- 66 | 67 | ### Breaking changes in v0.13 68 | 69 | * `PskBundle` now has a constructor that validates that the inputs are either both empty or nonempty. 70 | * `rand_core` was updated to v0.9 71 | 72 | ### Breaking changes in v0.12 73 | 74 | The `serde_impls` feature was removed. If you were using this and require backwards compatible serialization/deserialization, see the wiki page [here](https://github.com/rozbb/rust-hpke/wiki/Migrating-away-from-the-serde_impls-feature). 75 | 76 | MSRV 77 | ---- 78 | 79 | The current minimum supported Rust version (MSRV) is 1.65.0 (897e37553 2022-11-02). 80 | 81 | Changelog 82 | --------- 83 | 84 | See [CHANGELOG.md](CHANGELOG.md) for a list of changes made throughout past versions. 85 | 86 | Tests 87 | ----- 88 | 89 | To run all tests, execute `cargo test --all-features`. This includes known-answer tests, which test against `test-vector-COMMIT_ID.json`,where `COMMIT_ID` is the short commit of the version of the [spec](https://github.com/cfrg/draft-irtf-cfrg-hpke) that the test vectors came from. The finalized spec uses commit 5f503c5. See the [reference implementation](https://github.com/cisco/go-hpke) for information on how to generate a test vector. 90 | 91 | Benchmarks 92 | ---------- 93 | 94 | To run all benchmarks, execute `cargo bench --all-features`. If you set your own feature flags, the benchmarks will still work, and run the subset of benches that it is able to. The results of a benchmark can be read as a neat webpage at `target/criterion/report/index.html`. 95 | 96 | Ciphersuites benchmarked: 97 | 98 | * NIST Ciphersuite with 128-bit security: AES-GCM-128, HKDF-SHA256, ECDH-P256 99 | * Non-NIST Ciphersuite with 128-bit security: ChaCha20-Poly1305, HKDF-SHA256, X25519 100 | 101 | Functions benchmarked in each ciphersuite: 102 | 103 | * `Kem::gen_keypair` 104 | * `setup_sender` with OpModes of Base, Auth, Psk, and AuthPsk 105 | * `setup_receiver` with OpModes of Base, Auth, Psk, and AuthPsk 106 | * `AeadCtxS::seal` with plaintext length 64 and AAD length 64 107 | * `AeadCtxR::open` with ciphertext length 64 and AAD length 64 108 | 109 | Agility 110 | ------- 111 | 112 | A definition: *crypto agility* refers to the ability of a cryptosystem or protocol to vary its underlying primitives. For example, TLS has "crypto agility" in that you can run the protocol with many different ciphersuites. 113 | 114 | This crate does not support crypto agility out of the box. This is because the cryptographic primitives are encoded as types satisfying certain constraints, and types need to be determined at compile time (broadly speaking). That said, there is nothing preventing you from implementing agility yourself. There is a [sample implementation](examples/agility.rs) in the examples folder. The sample implementation is messy because agility is messy. 115 | 116 | License 117 | ------- 118 | 119 | Licensed under either of 120 | 121 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE)) 122 | * MIT license ([LICENSE-MIT](LICENSE-MIT)) 123 | 124 | at your option. 125 | -------------------------------------------------------------------------------- /benches/benches.rs: -------------------------------------------------------------------------------- 1 | use hpke::{ 2 | aead::{Aead as AeadTrait, AeadCtxR, AeadTag}, 3 | kdf::Kdf as KdfTrait, 4 | kem::Kem as KemTrait, 5 | setup_receiver, setup_sender, OpModeR, OpModeS, PskBundle, 6 | }; 7 | 8 | use criterion::{black_box, criterion_main, Criterion}; 9 | use rand::{rngs::StdRng, RngCore, SeedableRng}; 10 | use std::time::Instant; 11 | 12 | // Length of AAD for all seal/open benchmarks 13 | const AAD_LEN: usize = 64; 14 | // Length of plaintext and ciphertext for all seal/open benchmarks 15 | const MSG_LEN: usize = 64; 16 | // Length of PSK. Since we're only testing the 128-bit security level, make it 128 bits 17 | const PSK_LEN: usize = 16; 18 | 19 | // Generic function to bench the specified ciphersuite 20 | fn bench_ciphersuite(group_name: &str, c: &mut Criterion) 21 | where 22 | Aead: AeadTrait, 23 | Kdf: KdfTrait, 24 | Kem: KemTrait, 25 | { 26 | let mut csprng = StdRng::from_os_rng(); 27 | 28 | let mut group = c.benchmark_group(group_name); 29 | 30 | // Bench keypair generation 31 | group.bench_function("gen_keypair", |b| b.iter(|| Kem::gen_keypair(&mut csprng))); 32 | 33 | // Make a recipient keypair to encrypt to 34 | let (sk_recip, pk_recip) = Kem::gen_keypair(&mut csprng); 35 | 36 | // Make a PSK bundle for OpModePsk and OpModeAuthPsk 37 | let mut psk = [0u8; PSK_LEN]; 38 | let mut psk_id = [0u8; 8]; 39 | csprng.fill_bytes(&mut psk); 40 | csprng.fill_bytes(&mut psk_id); 41 | let psk_bundle = PskBundle::new(&psk, &psk_id).unwrap(); 42 | 43 | // Make a sender keypair for OpModeAuth and OpModeAuthPsk 44 | let (sk_sender, pk_sender) = Kem::gen_keypair(&mut csprng); 45 | 46 | // Construct all the opmodes we'll use in setup_sender and setup_receiver 47 | let opmodes = ["base", "auth", "psk", "authpsk"]; 48 | let opmodes_s = vec![ 49 | OpModeS::Base, 50 | OpModeS::Auth((sk_sender.clone(), pk_sender.clone())), 51 | OpModeS::Psk(psk_bundle), 52 | OpModeS::AuthPsk((sk_sender, pk_sender.clone()), psk_bundle), 53 | ]; 54 | let opmodes_r = vec![ 55 | OpModeR::Base, 56 | OpModeR::Psk(psk_bundle), 57 | OpModeR::Auth(pk_recip.clone()), 58 | OpModeR::AuthPsk(pk_recip.clone(), psk_bundle), 59 | ]; 60 | 61 | // Bench setup_sender() for each opmode 62 | for (mode, opmode_s) in opmodes.iter().zip(opmodes_s.iter()) { 63 | let bench_name = format!("setup_sender[mode={}]", mode); 64 | group.bench_function(bench_name, |b| { 65 | b.iter(|| { 66 | setup_sender::( 67 | opmode_s, 68 | &pk_recip, 69 | b"bench setup sender", 70 | &mut csprng, 71 | ) 72 | }) 73 | }); 74 | } 75 | 76 | // Collect the encapsulated keys from each setup_sender under each opmode. We will pass these 77 | // to setup_receiver in a moment 78 | let encapped_keys = opmodes_s.iter().map(|opmode_s| { 79 | setup_sender::( 80 | &opmode_s, 81 | &pk_recip, 82 | b"bench setup receiver", 83 | &mut csprng, 84 | ) 85 | .unwrap() 86 | .0 87 | }); 88 | 89 | // Bench setup_receiver for each opmode 90 | for ((mode, opmode_r), encapped_key) in opmodes.iter().zip(opmodes_r).zip(encapped_keys) { 91 | let bench_name = format!("setup_receiver[mode={}]", mode); 92 | group.bench_function(bench_name, |b| { 93 | b.iter(|| { 94 | setup_receiver::( 95 | &opmode_r, 96 | &sk_recip, 97 | &encapped_key, 98 | b"bench setup sender", 99 | ) 100 | .unwrap() 101 | }) 102 | }); 103 | } 104 | 105 | // Make the encryption context so we can benchmark seal() 106 | let (_, mut encryption_ctx) = 107 | setup_sender::(&OpModeS::Base, &pk_recip, b"bench seal", &mut csprng) 108 | .unwrap(); 109 | 110 | // Bench seal_in_place_detached() on a MSG_LEN-byte plaintext and AAD_LEN-byte AAD 111 | let bench_name = format!( 112 | "seal_in_place_detached[msglen={},aadlen={}]", 113 | MSG_LEN, AAD_LEN 114 | ); 115 | group.bench_function(bench_name, |b| { 116 | // Pick random inputs 117 | let mut plaintext = [0u8; MSG_LEN]; 118 | let mut aad = [0u8; AAD_LEN]; 119 | csprng.fill_bytes(&mut plaintext); 120 | csprng.fill_bytes(&mut aad); 121 | 122 | b.iter(|| { 123 | encryption_ctx 124 | .seal_in_place_detached(&mut plaintext, &aad) 125 | .unwrap() 126 | }) 127 | }); 128 | 129 | // Bench open_in_place_detached() on MSG_LEN-bytes ciphertexts with AAD_LEN-byte AADs. This is 130 | // more complicated than the other benchmarks because we need to first construct and store a 131 | // ton of ciphertexts that we can open in sequence. 132 | let bench_name = format!( 133 | "open_in_place_detached[msglen={},aadlen={}]", 134 | MSG_LEN, AAD_LEN 135 | ); 136 | group.bench_function(bench_name, |b| { 137 | b.iter_custom(|iters| { 138 | // Make a decryption context and however many (ciphertexts, aad, tag) tuples the 139 | // bencher tells us we need 140 | let (mut decryption_ctx, ciphertext_aad_tags) = 141 | make_decryption_ctx_with_ciphertexts::(iters as usize); 142 | 143 | // Start the timer, open every ciphertext in quick succession, then stop the timer 144 | let start = Instant::now(); 145 | for (mut ciphertext, aad, tag) in ciphertext_aad_tags.into_iter() { 146 | // black_box makes sure the compiler doesn't optimize away this computation 147 | black_box( 148 | decryption_ctx 149 | .open_in_place_detached(&mut ciphertext, &aad, &tag) 150 | .unwrap(), 151 | ); 152 | } 153 | start.elapsed() 154 | }); 155 | }); 156 | } 157 | 158 | // A tuple of (ciphertext, aad, auth_tag) resulting from a call to seal() 159 | type CiphertextAadTag = ([u8; MSG_LEN], [u8; AAD_LEN], AeadTag); 160 | 161 | // Constructs a decryption context with num_ciphertexts many CiphertextAadTag tuples that are 162 | // decryptable in sequence 163 | fn make_decryption_ctx_with_ciphertexts( 164 | num_ciphertexts: usize, 165 | ) -> (AeadCtxR, Vec>) 166 | where 167 | Aead: AeadTrait, 168 | Kdf: KdfTrait, 169 | Kem: KemTrait, 170 | { 171 | let mut csprng = StdRng::from_os_rng(); 172 | 173 | // Make up the recipient's keypair and setup an encryption context 174 | let (sk_recip, pk_recip) = Kem::gen_keypair(&mut csprng); 175 | let (encapped_key, mut encryption_ctx) = 176 | setup_sender::(&OpModeS::Base, &pk_recip, b"bench seal", &mut csprng) 177 | .unwrap(); 178 | 179 | // Construct num_ciphertext many (plaintext, aad) pairs and pass them through seal() 180 | let mut ciphertext_aad_tags = Vec::with_capacity(num_ciphertexts); 181 | for _ in 0..num_ciphertexts { 182 | // Make the plaintext and AAD random 183 | let mut plaintext = [0u8; MSG_LEN]; 184 | let mut aad = [0u8; AAD_LEN]; 185 | csprng.fill_bytes(&mut plaintext); 186 | csprng.fill_bytes(&mut aad); 187 | 188 | // Seal the random plaintext and AAD 189 | let tag = encryption_ctx 190 | .seal_in_place_detached(&mut plaintext, &aad) 191 | .unwrap(); 192 | // Rename for clarity. Encryption happened in-place 193 | let ciphertext = plaintext; 194 | 195 | // Collect the ciphertext, AAD, and authentication tag 196 | ciphertext_aad_tags.push((ciphertext, aad, tag)); 197 | } 198 | 199 | // Build the recipient's decryption context from the sender's encapsulated key 200 | let decryption_ctx = 201 | setup_receiver::(&OpModeR::Base, &sk_recip, &encapped_key, b"bench seal") 202 | .unwrap(); 203 | 204 | (decryption_ctx, ciphertext_aad_tags) 205 | } 206 | 207 | pub fn benches() { 208 | let mut c = Criterion::default().configure_from_args(); 209 | 210 | // NIST ciphersuite at the 128-bit security level is AES-GCM-128, HKDF-SHA256, and ECDH-P256 211 | #[cfg(feature = "p256")] 212 | bench_ciphersuite::( 213 | "NIST[seclevel=128]", 214 | &mut c, 215 | ); 216 | 217 | // Non-NIST ciphersuite at the 128-bit security level is ChaCha20Poly1305, HKDF-SHA256, and X25519 218 | #[cfg(feature = "x25519")] 219 | bench_ciphersuite::< 220 | hpke::aead::ChaCha20Poly1305, 221 | hpke::kdf::HkdfSha256, 222 | hpke::kem::X25519HkdfSha256, 223 | >("Non-NIST[seclevel=128]", &mut c); 224 | } 225 | 226 | criterion_main!(benches); 227 | -------------------------------------------------------------------------------- /examples/agility.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | //! Here's the gist of this file: Instead of doing things at the type level, you can use zero-sized 3 | //! types and runtime validity checks to do all of HPKE. This file is a rough idea of how one would 4 | //! go about implementing that. There isn't too much repetition. The main part where you have to 5 | //! get clever is in `agile_setup_*`, where you have to have a match statement with up to 3·3·5 = 6 | //! 45 branches for all the different AEAD-KEM-KDF combinations. Practically speaking, though, 7 | //! that's not a big number, so writing that out and using a macro for the actual work (e.g., 8 | //! `do_setup_sender!`) seems to be the way to go. 9 | //! 10 | //! The other point of this file is to demonstrate how messy crypto agility makes things. Many 11 | //! people have different needs when it comes to agility, so I implore you **DO NOT COPY THIS FILE 12 | //! BLINDLY**. Think about what you actually need, make that instead, and make sure to write lots 13 | //! of runtime checks. 14 | 15 | use hpke::{ 16 | aead::{Aead, AeadCtxR, AeadCtxS, AeadTag, AesGcm128, AesGcm256, ChaCha20Poly1305}, 17 | kdf::{HkdfSha256, HkdfSha384, HkdfSha512, Kdf as KdfTrait}, 18 | kem::{ 19 | DhP256HkdfSha256, DhP384HkdfSha384, DhP521HkdfSha512, Kem as KemTrait, X25519HkdfSha256, 20 | }, 21 | setup_receiver, setup_sender, Deserializable, HpkeError, OpModeR, OpModeS, PskBundle, 22 | Serializable, 23 | }; 24 | 25 | use rand::{rngs::StdRng, CryptoRng, RngCore, SeedableRng}; 26 | 27 | trait AgileAeadCtxS { 28 | fn seal_in_place_detached( 29 | &mut self, 30 | plaintext: &mut [u8], 31 | aad: &[u8], 32 | ) -> Result; 33 | fn seal(&mut self, plaintext: &[u8], aad: &[u8]) -> Result, AgileHpkeError>; 34 | } 35 | 36 | trait AgileAeadCtxR { 37 | fn open_in_place_detached( 38 | &mut self, 39 | ciphertext: &mut [u8], 40 | aad: &[u8], 41 | tag_bytes: &[u8], 42 | ) -> Result<(), AgileHpkeError>; 43 | fn open(&mut self, ciphertext: &[u8], aad: &[u8]) -> Result, AgileHpkeError>; 44 | } 45 | 46 | type AgileAeadTag = Vec; 47 | 48 | #[derive(Debug)] 49 | enum AgileHpkeError { 50 | /// When you don't give an algorithm an array of the length it wants. Error is of the form 51 | /// `((alg1, alg1_location) , (alg2, alg2_location))`. 52 | AlgMismatch((&'static str, &'static str), (&'static str, &'static str)), 53 | /// When you get an algorithm identifier you don't recognize. Error is of the form 54 | /// `(alg, given_id)`. 55 | UnknownAlgIdent(&'static str, u16), 56 | /// Represents an error in the `hpke` crate 57 | HpkeError(HpkeError), 58 | } 59 | 60 | // This just wraps the HpkeError 61 | impl From for AgileHpkeError { 62 | fn from(e: HpkeError) -> AgileHpkeError { 63 | AgileHpkeError::HpkeError(e) 64 | } 65 | } 66 | 67 | impl AgileAeadCtxS for AeadCtxS { 68 | fn seal_in_place_detached( 69 | &mut self, 70 | plaintext: &mut [u8], 71 | aad: &[u8], 72 | ) -> Result, AgileHpkeError> { 73 | self.seal_in_place_detached(plaintext, aad) 74 | .map(|tag| tag.to_bytes().to_vec()) 75 | .map_err(Into::into) 76 | } 77 | fn seal(&mut self, plaintext: &[u8], aad: &[u8]) -> Result, AgileHpkeError> { 78 | self.seal(plaintext, aad).map_err(Into::into) 79 | } 80 | } 81 | 82 | impl AgileAeadCtxR for AeadCtxR { 83 | fn open_in_place_detached( 84 | &mut self, 85 | ciphertext: &mut [u8], 86 | aad: &[u8], 87 | tag_bytes: &[u8], 88 | ) -> Result<(), AgileHpkeError> { 89 | let tag = AeadTag::::from_bytes(tag_bytes)?; 90 | self.open_in_place_detached(ciphertext, aad, &tag) 91 | .map_err(Into::into) 92 | } 93 | fn open(&mut self, ciphertext: &[u8], aad: &[u8]) -> Result, AgileHpkeError> { 94 | self.open(ciphertext, aad).map_err(Into::into) 95 | } 96 | } 97 | 98 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 99 | enum AeadAlg { 100 | AesGcm128, 101 | AesGcm256, 102 | ChaCha20Poly1305, 103 | } 104 | 105 | impl AeadAlg { 106 | fn name(&self) -> &'static str { 107 | match self { 108 | AeadAlg::AesGcm128 => "AesGcm128", 109 | AeadAlg::AesGcm256 => "AesGcm256", 110 | AeadAlg::ChaCha20Poly1305 => "ChaCha20Poly1305", 111 | } 112 | } 113 | 114 | fn try_from_u16(id: u16) -> Result { 115 | let res = match id { 116 | 0x01 => AeadAlg::AesGcm128, 117 | 0x02 => AeadAlg::AesGcm256, 118 | 0x03 => AeadAlg::ChaCha20Poly1305, 119 | _ => return Err(AgileHpkeError::UnknownAlgIdent("AeadAlg", id)), 120 | }; 121 | 122 | Ok(res) 123 | } 124 | 125 | fn to_u16(self) -> u16 { 126 | match self { 127 | AeadAlg::AesGcm128 => 0x01, 128 | AeadAlg::AesGcm256 => 0x02, 129 | AeadAlg::ChaCha20Poly1305 => 0x03, 130 | } 131 | } 132 | } 133 | 134 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 135 | enum KdfAlg { 136 | HkdfSha256, 137 | HkdfSha384, 138 | HkdfSha512, 139 | } 140 | 141 | impl KdfAlg { 142 | fn name(&self) -> &'static str { 143 | match self { 144 | KdfAlg::HkdfSha256 => "HkdfSha256", 145 | KdfAlg::HkdfSha384 => "HkdfSha384", 146 | KdfAlg::HkdfSha512 => "HkdfSha512", 147 | } 148 | } 149 | 150 | fn try_from_u16(id: u16) -> Result { 151 | let res = match id { 152 | 0x01 => KdfAlg::HkdfSha256, 153 | 0x02 => KdfAlg::HkdfSha384, 154 | 0x03 => KdfAlg::HkdfSha512, 155 | _ => return Err(AgileHpkeError::UnknownAlgIdent("KdfAlg", id)), 156 | }; 157 | 158 | Ok(res) 159 | } 160 | 161 | fn to_u16(self) -> u16 { 162 | match self { 163 | KdfAlg::HkdfSha256 => 0x01, 164 | KdfAlg::HkdfSha384 => 0x02, 165 | KdfAlg::HkdfSha512 => 0x03, 166 | } 167 | } 168 | 169 | fn get_digest_len(&self) -> usize { 170 | match self { 171 | KdfAlg::HkdfSha256 => 32, 172 | KdfAlg::HkdfSha384 => 48, 173 | KdfAlg::HkdfSha512 => 64, 174 | } 175 | } 176 | } 177 | 178 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 179 | enum KemAlg { 180 | X25519HkdfSha256, 181 | X448HkdfSha512, 182 | DhP256HkdfSha256, 183 | DhP384HkdfSha384, 184 | DhP521HkdfSha512, 185 | } 186 | 187 | impl KemAlg { 188 | fn name(&self) -> &'static str { 189 | match self { 190 | KemAlg::DhP256HkdfSha256 => "DhP256HkdfSha256", 191 | KemAlg::DhP384HkdfSha384 => "DhP384HkdfSha384", 192 | KemAlg::DhP521HkdfSha512 => "DhP521HkdfSha512", 193 | KemAlg::X25519HkdfSha256 => "X25519HkdfSha256", 194 | KemAlg::X448HkdfSha512 => "X448HkdfSha512", 195 | } 196 | } 197 | 198 | fn try_from_u16(id: u16) -> Result { 199 | let res = match id { 200 | 0x10 => KemAlg::DhP256HkdfSha256, 201 | 0x11 => KemAlg::DhP384HkdfSha384, 202 | 0x12 => KemAlg::DhP521HkdfSha512, 203 | 0x20 => KemAlg::X25519HkdfSha256, 204 | 0x21 => KemAlg::X448HkdfSha512, 205 | _ => return Err(AgileHpkeError::UnknownAlgIdent("KemAlg", id)), 206 | }; 207 | 208 | Ok(res) 209 | } 210 | 211 | fn to_u16(self) -> u16 { 212 | match self { 213 | KemAlg::DhP256HkdfSha256 => 0x10, 214 | KemAlg::DhP384HkdfSha384 => 0x11, 215 | KemAlg::DhP521HkdfSha512 => 0x12, 216 | KemAlg::X25519HkdfSha256 => 0x20, 217 | KemAlg::X448HkdfSha512 => 0x21, 218 | } 219 | } 220 | 221 | fn kdf_alg(&self) -> KdfAlg { 222 | match self { 223 | KemAlg::X25519HkdfSha256 => KdfAlg::HkdfSha256, 224 | KemAlg::X448HkdfSha512 => KdfAlg::HkdfSha512, 225 | KemAlg::DhP256HkdfSha256 => KdfAlg::HkdfSha256, 226 | KemAlg::DhP384HkdfSha384 => KdfAlg::HkdfSha384, 227 | KemAlg::DhP521HkdfSha512 => KdfAlg::HkdfSha512, 228 | } 229 | } 230 | } 231 | 232 | #[derive(Clone)] 233 | struct AgilePublicKey { 234 | kem_alg: KemAlg, 235 | pubkey_bytes: Vec, 236 | } 237 | 238 | impl AgilePublicKey { 239 | fn try_lift(&self) -> Result { 240 | Kem::PublicKey::from_bytes(&self.pubkey_bytes).map_err(|e| e.into()) 241 | } 242 | } 243 | 244 | #[derive(Clone)] 245 | struct AgileEncappedKey { 246 | kem_alg: KemAlg, 247 | encapped_key_bytes: Vec, 248 | } 249 | 250 | impl AgileEncappedKey { 251 | fn try_lift(&self) -> Result { 252 | Kem::EncappedKey::from_bytes(&self.encapped_key_bytes).map_err(|e| e.into()) 253 | } 254 | } 255 | 256 | #[derive(Clone)] 257 | struct AgilePrivateKey { 258 | kem_alg: KemAlg, 259 | privkey_bytes: Vec, 260 | } 261 | 262 | impl AgilePrivateKey { 263 | fn try_lift(&self) -> Result { 264 | Kem::PrivateKey::from_bytes(&self.privkey_bytes).map_err(|e| e.into()) 265 | } 266 | } 267 | 268 | #[derive(Clone)] 269 | struct AgileKeypair(AgilePrivateKey, AgilePublicKey); 270 | 271 | impl AgileKeypair { 272 | fn try_lift(&self) -> Result<(Kem::PrivateKey, Kem::PublicKey), AgileHpkeError> { 273 | Ok((self.0.try_lift::()?, self.1.try_lift::()?)) 274 | } 275 | 276 | fn validate(&self) -> Result<(), AgileHpkeError> { 277 | if self.0.kem_alg != self.1.kem_alg { 278 | Err(AgileHpkeError::AlgMismatch( 279 | (self.0.kem_alg.name(), "AgileKeypair::privkey"), 280 | (self.1.kem_alg.name(), "AgileKeypair::pubkey"), 281 | )) 282 | } else { 283 | Ok(()) 284 | } 285 | } 286 | } 287 | 288 | // The leg work of agile_gen_keypair 289 | macro_rules! do_gen_keypair { 290 | ($kem_ty:ty, $kem_alg:ident, $csprng:ident) => {{ 291 | type Kem = $kem_ty; 292 | let kem_alg = $kem_alg; 293 | let csprng = $csprng; 294 | 295 | let (sk, pk) = Kem::gen_keypair(csprng); 296 | let sk = AgilePrivateKey { 297 | kem_alg, 298 | privkey_bytes: sk.to_bytes().to_vec(), 299 | }; 300 | let pk = AgilePublicKey { 301 | kem_alg, 302 | pubkey_bytes: pk.to_bytes().to_vec(), 303 | }; 304 | 305 | AgileKeypair(sk, pk) 306 | }}; 307 | } 308 | 309 | fn agile_gen_keypair(kem_alg: KemAlg, csprng: &mut R) -> AgileKeypair { 310 | match kem_alg { 311 | KemAlg::X25519HkdfSha256 => do_gen_keypair!(X25519HkdfSha256, kem_alg, csprng), 312 | KemAlg::DhP256HkdfSha256 => do_gen_keypair!(DhP256HkdfSha256, kem_alg, csprng), 313 | KemAlg::DhP384HkdfSha384 => do_gen_keypair!(DhP384HkdfSha384, kem_alg, csprng), 314 | KemAlg::DhP521HkdfSha512 => do_gen_keypair!(DhP521HkdfSha512, kem_alg, csprng), 315 | _ => unimplemented!(), 316 | } 317 | } 318 | 319 | #[derive(Clone)] 320 | struct AgileOpModeR<'a> { 321 | kem_alg: KemAlg, 322 | op_mode_ty: AgileOpModeRTy<'a>, 323 | } 324 | 325 | impl<'a> AgileOpModeR<'a> { 326 | fn try_lift(self) -> Result, AgileHpkeError> { 327 | let res = match self.op_mode_ty { 328 | AgileOpModeRTy::Base => OpModeR::Base, 329 | AgileOpModeRTy::Psk(bundle) => OpModeR::Psk(bundle.try_lift::()?), 330 | AgileOpModeRTy::Auth(pk) => OpModeR::Auth(pk.try_lift::()?), 331 | AgileOpModeRTy::AuthPsk(pk, bundle) => { 332 | OpModeR::AuthPsk(pk.try_lift::()?, bundle.try_lift::()?) 333 | } 334 | }; 335 | 336 | Ok(res) 337 | } 338 | 339 | fn validate(&self) -> Result<(), AgileHpkeError> { 340 | match &self.op_mode_ty { 341 | AgileOpModeRTy::Auth(pk) => { 342 | if pk.kem_alg != self.kem_alg { 343 | return Err(AgileHpkeError::AlgMismatch( 344 | (self.kem_alg.name(), "AgileOpModeR::kem_alg"), 345 | ( 346 | pk.kem_alg.name(), 347 | "AgileOpModeR::op_mode_ty::AgilePublicKey::kem_alg", 348 | ), 349 | )); 350 | } 351 | } 352 | AgileOpModeRTy::AuthPsk(pk, _) => { 353 | if pk.kem_alg != self.kem_alg { 354 | return Err(AgileHpkeError::AlgMismatch( 355 | (self.kem_alg.name(), "AgileOpModeR::kem_alg"), 356 | ( 357 | pk.kem_alg.name(), 358 | "AgileOpModeR::op_mode_ty::AgilePublicKey::kem_alg", 359 | ), 360 | )); 361 | } 362 | } 363 | _ => (), 364 | } 365 | 366 | Ok(()) 367 | } 368 | } 369 | 370 | #[derive(Clone)] 371 | enum AgileOpModeRTy<'a> { 372 | Base, 373 | Psk(AgilePskBundle<'a>), 374 | Auth(AgilePublicKey), 375 | AuthPsk(AgilePublicKey, AgilePskBundle<'a>), 376 | } 377 | 378 | #[derive(Clone)] 379 | struct AgileOpModeS<'a> { 380 | kem_alg: KemAlg, 381 | op_mode_ty: AgileOpModeSTy<'a>, 382 | } 383 | 384 | impl<'a> AgileOpModeS<'a> { 385 | fn try_lift(self) -> Result, AgileHpkeError> { 386 | let res = match self.op_mode_ty { 387 | AgileOpModeSTy::Base => OpModeS::Base, 388 | AgileOpModeSTy::Psk(bundle) => OpModeS::Psk(bundle.try_lift::()?), 389 | AgileOpModeSTy::Auth(keypair) => OpModeS::Auth(keypair.try_lift::()?), 390 | AgileOpModeSTy::AuthPsk(keypair, bundle) => { 391 | OpModeS::AuthPsk(keypair.try_lift::()?, bundle.try_lift::()?) 392 | } 393 | }; 394 | 395 | Ok(res) 396 | } 397 | 398 | fn validate(&self) -> Result<(), AgileHpkeError> { 399 | match &self.op_mode_ty { 400 | AgileOpModeSTy::Auth(keypair) => { 401 | keypair.validate()?; 402 | if keypair.0.kem_alg != self.kem_alg { 403 | return Err(AgileHpkeError::AlgMismatch( 404 | (self.kem_alg.name(), "AgileOpModeS::kem_alg"), 405 | ( 406 | keypair.0.kem_alg.name(), 407 | "AgileOpModeS::op_mode_ty::AgilePrivateKey::kem_alg", 408 | ), 409 | )); 410 | } 411 | } 412 | AgileOpModeSTy::AuthPsk(keypair, _) => { 413 | keypair.validate()?; 414 | if keypair.0.kem_alg != self.kem_alg { 415 | return Err(AgileHpkeError::AlgMismatch( 416 | (self.kem_alg.name(), "AgileOpModeS::kem_alg"), 417 | ( 418 | keypair.0.kem_alg.name(), 419 | "AgileOpModeS::op_mode_ty::AgilePrivateKey::kem_alg", 420 | ), 421 | )); 422 | } 423 | } 424 | _ => (), 425 | } 426 | 427 | Ok(()) 428 | } 429 | } 430 | 431 | #[derive(Clone)] 432 | enum AgileOpModeSTy<'a> { 433 | Base, 434 | Psk(AgilePskBundle<'a>), 435 | Auth(AgileKeypair), 436 | AuthPsk(AgileKeypair, AgilePskBundle<'a>), 437 | } 438 | 439 | #[derive(Clone, Copy)] 440 | struct AgilePskBundle<'a>(PskBundle<'a>); 441 | 442 | impl<'a> AgilePskBundle<'a> { 443 | fn try_lift(self) -> Result, AgileHpkeError> { 444 | Ok(self.0) 445 | } 446 | } 447 | 448 | // This macro takes in all the supported AEADs, KDFs, and KEMs, and dispatches the given test 449 | // vector to the test case with the appropriate types 450 | macro_rules! hpke_dispatch { 451 | // Step 1: Roll up the AEAD, KDF, and KEM types into tuples. We'll unroll them later 452 | ($to_set:ident, $to_match:ident, 453 | ($( $aead_ty:ident ),*), ($( $kdf_ty:ident ),*), ($( $kem_ty:ident ),*), $rng_ty:ident, 454 | $callback:ident, $( $callback_args:ident ),* ) => { 455 | hpke_dispatch!(@tup1 456 | $to_set, $to_match, 457 | ($( $aead_ty ),*), ($( $kdf_ty ),*), ($( $kem_ty ),*), $rng_ty, 458 | $callback, ($( $callback_args ),*) 459 | ) 460 | }; 461 | 462 | // Step 2: Expand with respect to every AEAD 463 | (@tup1 464 | $to_set:ident, $to_match:ident, 465 | ($( $aead_ty:ident ),*), $kdf_tup:tt, $kem_tup:tt, $rng_ty:tt, 466 | $callback:ident, $callback_args:tt) => { 467 | $( 468 | hpke_dispatch!(@tup2 469 | $to_set, $to_match, 470 | $aead_ty, $kdf_tup, $kem_tup, $rng_ty, 471 | $callback, $callback_args 472 | ); 473 | )* 474 | }; 475 | 476 | // Step 3: Expand with respect to every KDF 477 | (@tup2 478 | $to_set:ident, $to_match:ident, 479 | $aead_ty:ident, ($( $kdf_ty:ident ),*), $kem_tup:tt, $rng_ty:tt, 480 | $callback:ident, $callback_args:tt) => { 481 | $( 482 | hpke_dispatch!(@tup3 483 | $to_set, $to_match, 484 | $aead_ty, $kdf_ty, $kem_tup, $rng_ty, 485 | $callback, $callback_args 486 | ); 487 | )* 488 | }; 489 | 490 | // Step 4: Expand with respect to every KEM 491 | (@tup3 492 | $to_set:ident, $to_match:ident, 493 | $aead_ty:ident, $kdf_ty:ident, ($( $kem_ty:ident ),*), $rng_ty:tt, 494 | $callback:ident, $callback_args:tt) => { 495 | $( 496 | hpke_dispatch!(@base 497 | $to_set, $to_match, 498 | $aead_ty, $kdf_ty, $kem_ty, $rng_ty, 499 | $callback, $callback_args 500 | ); 501 | )* 502 | }; 503 | 504 | // Step 5: Now that we're only dealing with 1 type of each kind, do the dispatch. If the test 505 | // vector matches the IDs of these types, run the test case. 506 | (@base 507 | $to_set:ident, $to_match:ident, 508 | $aead_ty:ident, $kdf_ty:ident, $kem_ty:ident, $rng_ty:ident, 509 | $callback:ident, ($( $callback_args:ident ),*)) => { 510 | if let (AeadAlg::$aead_ty, KemAlg::$kem_ty, KdfAlg::$kdf_ty) = $to_match 511 | { 512 | $to_set = Some($callback::<$aead_ty, $kdf_ty, $kem_ty, $rng_ty>($( $callback_args ),*)); 513 | } 514 | }; 515 | } 516 | 517 | // The leg work of agile_setup_receiver 518 | fn do_setup_sender( 519 | mode: &AgileOpModeS, 520 | pk_recip: &AgilePublicKey, 521 | info: &[u8], 522 | csprng: &mut R, 523 | ) -> Result<(AgileEncappedKey, Box), AgileHpkeError> 524 | where 525 | A: 'static + Aead, 526 | Kdf: 'static + KdfTrait, 527 | Kem: 'static + KemTrait, 528 | R: CryptoRng + RngCore, 529 | { 530 | let kem_alg = mode.kem_alg; 531 | let mode = mode.clone().try_lift::()?; 532 | let pk_recip = pk_recip.try_lift::()?; 533 | 534 | let (encapped_key, aead_ctx) = setup_sender::(&mode, &pk_recip, info, csprng)?; 535 | let encapped_key = AgileEncappedKey { 536 | kem_alg, 537 | encapped_key_bytes: encapped_key.to_bytes().to_vec(), 538 | }; 539 | 540 | Ok((encapped_key, Box::new(aead_ctx))) 541 | } 542 | 543 | fn agile_setup_sender( 544 | aead_alg: AeadAlg, 545 | kdf_alg: KdfAlg, 546 | kem_alg: KemAlg, 547 | mode: &AgileOpModeS, 548 | pk_recip: &AgilePublicKey, 549 | info: &[u8], 550 | csprng: &mut R, 551 | ) -> Result<(AgileEncappedKey, Box), AgileHpkeError> { 552 | // Do all the necessary validation 553 | mode.validate()?; 554 | if mode.kem_alg != pk_recip.kem_alg { 555 | return Err(AgileHpkeError::AlgMismatch( 556 | (mode.kem_alg.name(), "mode::kem_alg"), 557 | (pk_recip.kem_alg.name(), "pk_recip::kem_alg"), 558 | )); 559 | } 560 | if kem_alg != mode.kem_alg { 561 | return Err(AgileHpkeError::AlgMismatch( 562 | (kem_alg.name(), "kem_alg::kem_alg"), 563 | (mode.kem_alg.name(), "mode::kem_alg"), 564 | )); 565 | } 566 | 567 | // The triple we dispatch on 568 | let to_match = (aead_alg, kem_alg, kdf_alg); 569 | 570 | // This gets overwritten by the below macro call. It's None iff dispatch failed. 571 | let mut res: Option), AgileHpkeError>> = None; 572 | 573 | #[rustfmt::skip] 574 | hpke_dispatch!( 575 | res, to_match, 576 | (ChaCha20Poly1305, AesGcm128, AesGcm256), 577 | (HkdfSha256, HkdfSha384, HkdfSha512), 578 | (X25519HkdfSha256, DhP256HkdfSha256, DhP384HkdfSha384, DhP521HkdfSha512), 579 | R, 580 | do_setup_sender, 581 | mode, 582 | pk_recip, 583 | info, 584 | csprng 585 | ); 586 | 587 | if res.is_none() { 588 | panic!("DHKEM({}) isn't impelmented yet!", kem_alg.name()); 589 | } 590 | 591 | res.unwrap() 592 | } 593 | 594 | // The leg work of agile_setup_receiver. The Dummy type parameter is so that it can be used with 595 | // the hpke_dispatch! macro. The macro expects its callback function to have 4 type parameters 596 | fn do_setup_receiver( 597 | mode: &AgileOpModeR, 598 | recip_keypair: &AgileKeypair, 599 | encapped_key: &AgileEncappedKey, 600 | info: &[u8], 601 | ) -> Result, AgileHpkeError> 602 | where 603 | A: 'static + Aead, 604 | Kdf: 'static + KdfTrait, 605 | Kem: 'static + KemTrait, 606 | { 607 | let mode = mode.clone().try_lift::()?; 608 | let (sk_recip, _) = recip_keypair.try_lift::()?; 609 | let encapped_key = encapped_key.try_lift::()?; 610 | 611 | let aead_ctx = setup_receiver::(&mode, &sk_recip, &encapped_key, info)?; 612 | Ok(Box::new(aead_ctx)) 613 | } 614 | 615 | fn agile_setup_receiver( 616 | aead_alg: AeadAlg, 617 | kdf_alg: KdfAlg, 618 | kem_alg: KemAlg, 619 | mode: &AgileOpModeR, 620 | recip_keypair: &AgileKeypair, 621 | encapped_key: &AgileEncappedKey, 622 | info: &[u8], 623 | ) -> Result, AgileHpkeError> { 624 | // Do all the necessary validation 625 | recip_keypair.validate()?; 626 | mode.validate()?; 627 | if mode.kem_alg != recip_keypair.0.kem_alg { 628 | return Err(AgileHpkeError::AlgMismatch( 629 | (mode.kem_alg.name(), "mode::kem_alg"), 630 | (recip_keypair.0.kem_alg.name(), "recip_keypair::kem_alg"), 631 | )); 632 | } 633 | if kem_alg != mode.kem_alg { 634 | return Err(AgileHpkeError::AlgMismatch( 635 | (kem_alg.name(), "kem_alg::kem_alg"), 636 | (mode.kem_alg.name(), "mode::kem_alg"), 637 | )); 638 | } 639 | if recip_keypair.0.kem_alg != encapped_key.kem_alg { 640 | return Err(AgileHpkeError::AlgMismatch( 641 | (recip_keypair.0.kem_alg.name(), "recip_keypair::kem_alg"), 642 | (encapped_key.kem_alg.name(), "encapped_key::kem_alg"), 643 | )); 644 | } 645 | 646 | // The triple we dispatch on 647 | let to_match = (aead_alg, kem_alg, kdf_alg); 648 | 649 | // This gets overwritten by the below macro call. It's None iff dispatch failed. 650 | let mut res: Option, AgileHpkeError>> = None; 651 | 652 | // Dummy type to give to the macro. do_setup_receiver doesn't use an RNG, so it doesn't need a 653 | // concrete RNG type. We give it the unit type to make it happy. 654 | type Unit = (); 655 | 656 | #[rustfmt::skip] 657 | hpke_dispatch!( 658 | res, to_match, 659 | (ChaCha20Poly1305, AesGcm128, AesGcm256), 660 | (HkdfSha256, HkdfSha384, HkdfSha512), 661 | (X25519HkdfSha256, DhP256HkdfSha256, DhP384HkdfSha384, DhP521HkdfSha512), 662 | Unit, 663 | do_setup_receiver, 664 | mode, 665 | recip_keypair, 666 | encapped_key, 667 | info 668 | ); 669 | 670 | if res.is_none() { 671 | panic!("DHKEM({}) isn't impelmented yet!", kem_alg.name()); 672 | } 673 | 674 | res.unwrap() 675 | } 676 | 677 | fn main() { 678 | let mut csprng = StdRng::from_os_rng(); 679 | 680 | let supported_aead_algs = &[ 681 | AeadAlg::AesGcm128, 682 | AeadAlg::AesGcm256, 683 | AeadAlg::ChaCha20Poly1305, 684 | ]; 685 | let supported_kem_algs = &[ 686 | KemAlg::X25519HkdfSha256, 687 | KemAlg::DhP256HkdfSha256, 688 | KemAlg::DhP384HkdfSha384, 689 | KemAlg::DhP521HkdfSha512, 690 | ]; 691 | let supported_kdf_algs = &[KdfAlg::HkdfSha256, KdfAlg::HkdfSha384, KdfAlg::HkdfSha512]; 692 | 693 | // For every combination of supported algorithms, test an encryption-decryption round trip 694 | for &aead_alg in supported_aead_algs { 695 | for &kem_alg in supported_kem_algs { 696 | for &kdf_alg in supported_kdf_algs { 697 | let info = b"we're gonna agile him in his clavicle"; 698 | 699 | // Make a random sender keypair and PSK bundle 700 | let sender_keypair = agile_gen_keypair(kem_alg, &mut csprng); 701 | let mut psk_bytes = vec![0u8; kdf_alg.get_digest_len()]; 702 | let psk_id = b"preshared key attempt #5, take 2. action"; 703 | let psk_bundle = { 704 | csprng.fill_bytes(&mut psk_bytes); 705 | AgilePskBundle(PskBundle::new(&psk_bytes, psk_id).unwrap()) 706 | }; 707 | 708 | // Make two agreeing OpModes (AuthPsk is the most complicated, so we're just using 709 | // that). 710 | let op_mode_s_ty = AgileOpModeSTy::AuthPsk(sender_keypair.clone(), psk_bundle); 711 | let op_mode_s = AgileOpModeS { 712 | kem_alg, 713 | op_mode_ty: op_mode_s_ty, 714 | }; 715 | let op_mode_r_ty = AgileOpModeRTy::AuthPsk(sender_keypair.1, psk_bundle); 716 | let op_mode_r = AgileOpModeR { 717 | kem_alg, 718 | op_mode_ty: op_mode_r_ty, 719 | }; 720 | 721 | // Set up the sender's encryption context 722 | let recip_keypair = agile_gen_keypair(kem_alg, &mut csprng); 723 | let (encapped_key, mut aead_ctx1) = agile_setup_sender( 724 | aead_alg, 725 | kdf_alg, 726 | kem_alg, 727 | &op_mode_s, 728 | &recip_keypair.1, 729 | &info[..], 730 | &mut csprng, 731 | ) 732 | .unwrap(); 733 | 734 | // Set up the receivers's encryption context 735 | let mut aead_ctx2 = agile_setup_receiver( 736 | aead_alg, 737 | kdf_alg, 738 | kem_alg, 739 | &op_mode_r, 740 | &recip_keypair, 741 | &encapped_key, 742 | &info[..], 743 | ) 744 | .unwrap(); 745 | 746 | // Test an encryption-decryption round trip 747 | let msg = b"paper boy paper boy"; 748 | let aad = b"all about that paper, boy"; 749 | let ciphertext = aead_ctx1.seal(msg, aad).unwrap(); 750 | let roundtrip_plaintext = aead_ctx2.open(&ciphertext, aad).unwrap(); 751 | 752 | // Assert that the derived plaintext equals the original message 753 | assert_eq!(&roundtrip_plaintext, msg); 754 | } 755 | } 756 | } 757 | 758 | println!("PEAK AGILITY ACHIEVED"); 759 | } 760 | -------------------------------------------------------------------------------- /examples/client_server.rs: -------------------------------------------------------------------------------- 1 | // This file describes a simple interaction between a client and a server. Here's the flow: 2 | // 1. The server initializes itself with a new public-private keypair. 3 | // 2. The client generates and encapsulates a symmetric key, and uses that key to derive an 4 | // encryption context. With the encryption context, it encrypts ("seals") a message and gets an 5 | // authenticated ciphertext. It then sends the server the encapsulated key and the 6 | // authenticated ciphertext. 7 | // 3. The server uses the received encapsulated key to derive the decryption context, and decrypts 8 | // the ciphertext. 9 | // 10 | // Concepts not covered in this example: 11 | // * Different operation modes (Auth, Psk, AuthPsk). See the docs on `OpModeR` and `OpModeS` 12 | // types for more info 13 | // * The single-shot API. See the methods exposed in the `single_shot` module for more info. The 14 | // single-shot methods are basically just `setup` followed by `seal/open`. 15 | // * Proper error handling. Everything here just panics when an error is encountered. It is up to 16 | // the user of this library to do the appropriate thing when a function returns an error. 17 | 18 | use hpke::{ 19 | aead::{AeadTag, ChaCha20Poly1305}, 20 | kdf::HkdfSha384, 21 | kem::X25519HkdfSha256, 22 | Deserializable, Kem as KemTrait, OpModeR, OpModeS, Serializable, 23 | }; 24 | 25 | use rand::{rngs::StdRng, SeedableRng}; 26 | 27 | const INFO_STR: &[u8] = b"example session"; 28 | 29 | // These are the only algorithms we're gonna use for this example 30 | type Kem = X25519HkdfSha256; 31 | type Aead = ChaCha20Poly1305; 32 | type Kdf = HkdfSha384; 33 | 34 | // Initializes the server with a fresh keypair 35 | fn server_init() -> (::PrivateKey, ::PublicKey) { 36 | let mut csprng = StdRng::from_os_rng(); 37 | Kem::gen_keypair(&mut csprng) 38 | } 39 | 40 | // Given a message and associated data, returns an encapsulated key, ciphertext, and tag. The 41 | // ciphertext is encrypted with the shared AEAD context 42 | fn client_encrypt_msg( 43 | msg: &[u8], 44 | associated_data: &[u8], 45 | server_pk: &::PublicKey, 46 | ) -> (::EncappedKey, Vec, AeadTag) { 47 | let mut csprng = StdRng::from_os_rng(); 48 | 49 | // Encapsulate a key and use the resulting shared secret to encrypt a message. The AEAD context 50 | // is what you use to encrypt. 51 | let (encapped_key, mut sender_ctx) = 52 | hpke::setup_sender::(&OpModeS::Base, server_pk, INFO_STR, &mut csprng) 53 | .expect("invalid server pubkey!"); 54 | 55 | // On success, seal_in_place_detached() will encrypt the plaintext in place 56 | let mut msg_copy = msg.to_vec(); 57 | let tag = sender_ctx 58 | .seal_in_place_detached(&mut msg_copy, associated_data) 59 | .expect("encryption failed!"); 60 | 61 | // Rename for clarity 62 | let ciphertext = msg_copy; 63 | 64 | (encapped_key, ciphertext, tag) 65 | } 66 | 67 | // Returns the decrypted client message 68 | fn server_decrypt_msg( 69 | server_sk_bytes: &[u8], 70 | encapped_key_bytes: &[u8], 71 | ciphertext: &[u8], 72 | associated_data: &[u8], 73 | tag_bytes: &[u8], 74 | ) -> Vec { 75 | // We have to derialize the secret key, AEAD tag, and encapsulated pubkey. These fail if the 76 | // bytestrings are the wrong length. 77 | let server_sk = ::PrivateKey::from_bytes(server_sk_bytes) 78 | .expect("could not deserialize server privkey!"); 79 | let tag = AeadTag::::from_bytes(tag_bytes).expect("could not deserialize AEAD tag!"); 80 | let encapped_key = ::EncappedKey::from_bytes(encapped_key_bytes) 81 | .expect("could not deserialize the encapsulated pubkey!"); 82 | 83 | // Decapsulate and derive the shared secret. This creates a shared AEAD context. 84 | let mut receiver_ctx = 85 | hpke::setup_receiver::(&OpModeR::Base, &server_sk, &encapped_key, INFO_STR) 86 | .expect("failed to set up receiver!"); 87 | 88 | // On success, open_in_place_detached() will decrypt the ciphertext in place 89 | let mut ciphertext_copy = ciphertext.to_vec(); 90 | receiver_ctx 91 | .open_in_place_detached(&mut ciphertext_copy, associated_data, &tag) 92 | .expect("invalid ciphertext!"); 93 | 94 | // Rename for clarity. Cargo clippy thinks it's unnecessary, but I disagree 95 | #[allow(clippy::let_and_return)] 96 | let plaintext = ciphertext_copy; 97 | 98 | plaintext 99 | } 100 | 101 | fn main() { 102 | // Set up the server 103 | let (server_privkey, server_pubkey) = server_init(); 104 | 105 | // The message to be encrypted 106 | let msg = b"Kat Branchman"; 107 | // Associated data that's authenticated but left unencrypted 108 | let associated_data = b"Mr. Meow"; 109 | 110 | // Let the client send a message to the server using the server's pubkey 111 | let (encapped_key, ciphertext, tag) = client_encrypt_msg(msg, associated_data, &server_pubkey); 112 | 113 | // Now imagine we send everything over the wire, so we have to serialize it 114 | let encapped_key_bytes = encapped_key.to_bytes(); 115 | let tag_bytes = tag.to_bytes(); 116 | 117 | // Now imagine the server had to reboot so it saved its private key in byte format 118 | let server_privkey_bytes = server_privkey.to_bytes(); 119 | 120 | // Now let the server decrypt the message. The to_bytes() calls returned a GenericArray, so we 121 | // have to convert them to slices before sending them 122 | let decrypted_msg = server_decrypt_msg( 123 | server_privkey_bytes.as_slice(), 124 | encapped_key_bytes.as_slice(), 125 | &ciphertext, 126 | associated_data, 127 | tag_bytes.as_slice(), 128 | ); 129 | 130 | // Make sure everything decrypted correctly 131 | assert_eq!(decrypted_msg, msg); 132 | 133 | println!("MESSAGE SUCCESSFULLY SENT AND RECEIVED"); 134 | } 135 | -------------------------------------------------------------------------------- /src/aead/aes_gcm.rs: -------------------------------------------------------------------------------- 1 | use crate::aead::Aead; 2 | 3 | /// The implementation of AES-128-GCM 4 | pub struct AesGcm128; 5 | 6 | impl Aead for AesGcm128 { 7 | type AeadImpl = aes_gcm::Aes128Gcm; 8 | 9 | // RFC 9180 §7.3: AES-128-GCM 10 | const AEAD_ID: u16 = 0x0001; 11 | } 12 | 13 | /// The implementation of AES-256-GCM 14 | pub struct AesGcm256 {} 15 | 16 | impl Aead for AesGcm256 { 17 | type AeadImpl = aes_gcm::Aes256Gcm; 18 | 19 | // RFC 9180 §7.3: AES-256-GCM 20 | const AEAD_ID: u16 = 0x0002; 21 | } 22 | -------------------------------------------------------------------------------- /src/aead/chacha20_poly1305.rs: -------------------------------------------------------------------------------- 1 | use crate::aead::Aead; 2 | 3 | /// The implementation of ChaCha20-Poly1305 4 | pub struct ChaCha20Poly1305; 5 | 6 | impl Aead for ChaCha20Poly1305 { 7 | type AeadImpl = chacha20poly1305::ChaCha20Poly1305; 8 | 9 | // RFC 9180 §7.3: ChaCha20Poly1305 10 | const AEAD_ID: u16 = 0x0003; 11 | } 12 | -------------------------------------------------------------------------------- /src/aead/export_only.rs: -------------------------------------------------------------------------------- 1 | use crate::aead::Aead; 2 | 3 | use aead::{ 4 | AeadCore as BaseAeadCore, AeadInPlace as BaseAeadInPlace, KeyInit as BaseKeyInit, 5 | KeySizeUser as BaseKeySizeUser, 6 | }; 7 | use generic_array::typenum; 8 | 9 | /// An inert underlying Aead implementation. The open/seal routines panic. The `new()` function 10 | /// returns an `EmptyAeadImpl`, and that is all of the functionality this struct has. 11 | #[doc(hidden)] 12 | #[derive(Clone)] 13 | pub struct EmptyAeadImpl; 14 | 15 | impl BaseAeadCore for EmptyAeadImpl { 16 | // The nonce size has to be bigger than the sequence size (currently u64), otherwise we get an 17 | // underflow error on seal()/open() before we can even panic 18 | type NonceSize = typenum::U128; 19 | type TagSize = typenum::U0; 20 | type CiphertextOverhead = typenum::U0; 21 | } 22 | 23 | impl BaseAeadInPlace for EmptyAeadImpl { 24 | fn encrypt_in_place_detached( 25 | &self, 26 | _: &aead::Nonce, 27 | _: &[u8], 28 | _: &mut [u8], 29 | ) -> Result, aead::Error> { 30 | panic!("Cannot encrypt with an export-only encryption context!"); 31 | } 32 | 33 | fn decrypt_in_place_detached( 34 | &self, 35 | _: &aead::Nonce, 36 | _: &[u8], 37 | _: &mut [u8], 38 | _: &aead::Tag, 39 | ) -> Result<(), aead::Error> { 40 | panic!("Cannot decrypt with an export-only encryption context!"); 41 | } 42 | } 43 | 44 | impl BaseKeySizeUser for EmptyAeadImpl { 45 | type KeySize = typenum::U0; 46 | } 47 | 48 | impl BaseKeyInit for EmptyAeadImpl { 49 | // Ignore the key, since we can't encrypt or decrypt anything anyway. Just return the object 50 | fn new(_: &aead::Key) -> Self { 51 | EmptyAeadImpl 52 | } 53 | } 54 | 55 | /// An AEAD which can **only** be used for its `export()` function. The `open()` and `seal()` 56 | /// methods on an `AeadCtxR` or `AeadCtxS` which uses this AEAD underlyingly **will panic** if you 57 | /// call them 58 | pub struct ExportOnlyAead; 59 | 60 | impl Aead for ExportOnlyAead { 61 | type AeadImpl = EmptyAeadImpl; 62 | 63 | // RFC 9180 §7.3: Export-only 64 | const AEAD_ID: u16 = 0xFFFF; 65 | } 66 | -------------------------------------------------------------------------------- /src/dhkex.rs: -------------------------------------------------------------------------------- 1 | use crate::{kdf::Kdf as KdfTrait, util::KemSuiteId, Deserializable, Serializable}; 2 | 3 | use core::fmt::Debug; 4 | 5 | // This is the maximum value of all of Npk, Ndh, and Nenc. It's achieved by P-521 in RFC 9180 §7.1 6 | // Table 2. 7 | pub(crate) const MAX_PUBKEY_SIZE: usize = 133; 8 | 9 | #[doc(hidden)] 10 | /// Internal error type used to represent `DhKeyExchange::dh()` failing 11 | #[derive(Debug)] 12 | pub struct DhError; 13 | 14 | /// This trait captures the requirements of a Diffie-Hellman key exchange mechanism. It must have a 15 | /// way to generate keypairs, perform the Diffie-Hellman operation, and serialize/deserialize 16 | /// pubkeys. This is built into a KEM in `kem/dhkem.rs`. 17 | pub trait DhKeyExchange { 18 | /// The key exchange's public key type. If you want to generate a keypair, see 19 | /// `Kem::gen_keypair` or `Kem::derive_keypair` 20 | type PublicKey: Clone + Debug + PartialEq + Eq + Serializable + Deserializable; 21 | 22 | /// The key exchange's private key type. If you want to generate a keypair, see 23 | /// `Kem::gen_keypair` or `Kem::derive_keypair` 24 | type PrivateKey: Clone + Serializable + Deserializable; 25 | 26 | /// The result of a DH operation 27 | #[doc(hidden)] 28 | type KexResult: Serializable; 29 | 30 | /// Computes the public key of a given private key 31 | #[doc(hidden)] 32 | fn sk_to_pk(sk: &Self::PrivateKey) -> Self::PublicKey; 33 | 34 | /// Does the Diffie-Hellman operation 35 | #[doc(hidden)] 36 | fn dh(sk: &Self::PrivateKey, pk: &Self::PublicKey) -> Result; 37 | 38 | /// Computes a keypair given key material `ikm` of sufficient entropy. See 39 | /// [`crate::kem::Kem::derive_keypair`] for discussion of entropy. 40 | #[doc(hidden)] 41 | fn derive_keypair( 42 | suite_id: &KemSuiteId, 43 | ikm: &[u8], 44 | ) -> (Self::PrivateKey, Self::PublicKey); 45 | } 46 | 47 | #[cfg(any(feature = "p256", feature = "p384", feature = "p521"))] 48 | pub(crate) mod ecdh_nistp; 49 | 50 | #[cfg(feature = "x25519")] 51 | pub(crate) mod x25519; 52 | -------------------------------------------------------------------------------- /src/dhkex/ecdh_nistp.rs: -------------------------------------------------------------------------------- 1 | // We define all the NIST P- curve ECDH functionalities in one macro 2 | macro_rules! nistp_dhkex { 3 | ( 4 | $curve_name:expr, 5 | $dh_name:ident, 6 | $curve:ident, 7 | $pubkey_size:ty, 8 | $privkey_size:ty, 9 | $ss_size:ty, 10 | $keygen_bitmask:expr 11 | ) => { 12 | pub(crate) mod $curve { 13 | use super::*; 14 | 15 | use crate::{ 16 | dhkex::{DhError, DhKeyExchange}, 17 | kdf::{labeled_extract, Kdf as KdfTrait, LabeledExpand}, 18 | util::{enforce_equal_len, enforce_outbuf_len, KemSuiteId}, 19 | Deserializable, HpkeError, Serializable, 20 | }; 21 | 22 | use ::$curve as curve_crate; 23 | use curve_crate::elliptic_curve::{ecdh::diffie_hellman, sec1::ToEncodedPoint}; 24 | use generic_array::{typenum::Unsigned, GenericArray}; 25 | use subtle::{Choice, ConstantTimeEq}; 26 | 27 | #[doc = concat!( 28 | "An ECDH ", 29 | $curve_name, 30 | " public key. This is never the point at infinity." 31 | )] 32 | #[derive(Clone, Debug, Eq, PartialEq)] 33 | pub struct PublicKey(curve_crate::PublicKey); 34 | 35 | // This is only ever constructed via its Deserializable::from_bytes, which checks for 36 | // the 0 value. Also, the underlying type is zeroize-on-drop. 37 | #[doc = concat!( 38 | "An ECDH ", 39 | $curve_name, 40 | " private key. This is a scalar in the range `[1,p)` where `p` is the group order." 41 | )] 42 | #[derive(Clone, Eq, PartialEq)] 43 | pub struct PrivateKey(curve_crate::SecretKey); 44 | 45 | impl ConstantTimeEq for PrivateKey { 46 | fn ct_eq(&self, other: &Self) -> Choice { 47 | self.0.ct_eq(&other.0) 48 | } 49 | } 50 | 51 | // The underlying type is zeroize-on-drop 52 | /// A bare DH computation result 53 | pub struct KexResult(curve_crate::ecdh::SharedSecret); 54 | 55 | // Everything is serialized and deserialized in uncompressed form 56 | impl Serializable for PublicKey { 57 | type OutputSize = $pubkey_size; 58 | 59 | fn write_exact(&self, buf: &mut [u8]) { 60 | // Check the length is correct and panic if not 61 | enforce_outbuf_len::(buf); 62 | 63 | // Get the uncompressed pubkey encoding 64 | let encoded = self.0.as_affine().to_encoded_point(false); 65 | // Serialize it 66 | buf.copy_from_slice(encoded.as_bytes()); 67 | } 68 | 69 | } 70 | 71 | // Everything is serialized and deserialized in uncompressed form 72 | impl Deserializable for PublicKey { 73 | fn from_bytes(encoded: &[u8]) -> Result { 74 | // In order to parse as an uncompressed curve point, we first make sure the 75 | // input length is correct. This ensures we're receiving the uncompressed 76 | // representation. 77 | enforce_equal_len(Self::OutputSize::to_usize(), encoded.len())?; 78 | 79 | // Now just deserialize. The non-identity invariant is preserved because 80 | // PublicKey::from_sec1_bytes() will error if it receives the point at 81 | // infinity. This is because its submethod, PublicKey::from_encoded_point(), 82 | // does this check explicitly. 83 | let parsed = curve_crate::PublicKey::from_sec1_bytes(encoded) 84 | .map_err(|_| HpkeError::ValidationError)?; 85 | Ok(PublicKey(parsed)) 86 | } 87 | } 88 | 89 | impl Serializable for PrivateKey { 90 | type OutputSize = $privkey_size; 91 | 92 | fn write_exact(&self, buf: &mut [u8]) { 93 | // Check the length is correct and panic if not 94 | enforce_outbuf_len::(buf); 95 | 96 | // SecretKeys already know how to convert to bytes 97 | buf.copy_from_slice(&self.0.to_bytes()); 98 | } 99 | } 100 | 101 | impl Deserializable for PrivateKey { 102 | fn from_bytes(encoded: &[u8]) -> Result { 103 | // Check the length 104 | enforce_equal_len(Self::OutputSize::to_usize(), encoded.len())?; 105 | 106 | // * Invariant: PrivateKey is in [1,p). This is preserved here. 107 | // * SecretKey::from_be_bytes() directly checks that the value isn't zero. And 108 | // its submethod, 109 | // * ScalarCore::from_be_bytes() checks that the value doesn't exceed the 110 | // modulus. 111 | let sk = curve_crate::SecretKey::from_bytes(encoded.into()) 112 | .map_err(|_| HpkeError::ValidationError)?; 113 | 114 | Ok(PrivateKey(sk)) 115 | } 116 | } 117 | 118 | // DH results are serialized in the same way as public keys 119 | impl Serializable for KexResult { 120 | // RFC 9180 §4.1 121 | // For P-256, P-384, and P-521, the size Ndh of the Diffie-Hellman shared secret is 122 | // equal to 32, 48, and 66, respectively, corresponding to the x-coordinate of the 123 | // resulting elliptic curve point. 124 | type OutputSize = $ss_size; 125 | 126 | fn write_exact(&self, buf: &mut [u8]) { 127 | // Check the length is correct and panic if not 128 | enforce_outbuf_len::(buf); 129 | 130 | // elliptic_curve::ecdh::SharedSecret::raw_secret_bytes returns the serialized 131 | // x-coordinate 132 | buf.copy_from_slice(self.0.raw_secret_bytes()) 133 | } 134 | } 135 | 136 | #[doc = concat!("Represents ECDH functionality over NIST curve ", $curve_name, ".")] 137 | pub struct $dh_name {} 138 | 139 | impl DhKeyExchange for $dh_name { 140 | #[doc(hidden)] 141 | type PublicKey = PublicKey; 142 | #[doc(hidden)] 143 | type PrivateKey = PrivateKey; 144 | #[doc(hidden)] 145 | type KexResult = KexResult; 146 | 147 | /// Converts a private key to a public key 148 | #[doc(hidden)] 149 | fn sk_to_pk(sk: &PrivateKey) -> PublicKey { 150 | // pk = sk·G where G is the generator. This maintains the invariant of the 151 | // public key not being the point at infinity, since ord(G) = p, and sk is not 152 | // 0 mod p (by the invariant we keep on PrivateKeys) 153 | PublicKey(sk.0.public_key()) 154 | } 155 | 156 | /// Does the DH operation. This function is infallible, thanks to invariants on its 157 | /// inputs. 158 | #[doc(hidden)] 159 | fn dh(sk: &PrivateKey, pk: &PublicKey) -> Result { 160 | // Do the DH operation 161 | let dh_res = diffie_hellman(sk.0.to_nonzero_scalar(), pk.0.as_affine()); 162 | 163 | // RFC 9180 §7.1.4: Senders and recipients MUST ensure that dh_res is not the 164 | // point at infinity 165 | // 166 | // This is already true, since: 167 | // 1. pk is not the point at infinity (due to the invariant we keep on 168 | // PublicKeys) 169 | // 2. sk is not 0 mod p (due to the invariant we keep on PrivateKeys) 170 | // 3. Exponentiating a non-identity element of a prime-order group by something 171 | // less than the order yields a non-identity value 172 | // Therefore, dh_res cannot be the point at infinity 173 | Ok(KexResult(dh_res)) 174 | } 175 | 176 | // RFC 9180 §7.1.3: 177 | // def DeriveKeyPair(ikm): 178 | // dkp_prk = LabeledExtract("", "dkp_prk", ikm) 179 | // sk = 0 180 | // counter = 0 181 | // while sk == 0 or sk >= order: 182 | // if counter > 255: 183 | // raise DeriveKeyPairError 184 | // bytes = LabeledExpand(dkp_prk, "candidate", 185 | // I2OSP(counter, 1), Nsk) 186 | // bytes[0] = bytes[0] & bitmask 187 | // sk = OS2IP(bytes) 188 | // counter = counter + 1 189 | // return (sk, pk(sk)) 190 | // where `bitmask` is defined to be 0xFF for P-256 and P-384, and 0x01 for P-521 191 | 192 | /// Deterministically derives a keypair from the given input keying material and 193 | /// ciphersuite ID. The keying material SHOULD have as many bits of entropy as the 194 | /// bit length of a secret key 195 | #[doc(hidden)] 196 | fn derive_keypair( 197 | suite_id: &KemSuiteId, 198 | ikm: &[u8], 199 | ) -> (PrivateKey, PublicKey) { 200 | // Write the label into a byte buffer and extract from the IKM 201 | let (_, hkdf_ctx) = labeled_extract::(&[], suite_id, b"dkp_prk", ikm); 202 | 203 | // The buffer we hold the candidate scalar bytes in. This is the size of a 204 | // private key. 205 | let mut buf = 206 | GenericArray::::OutputSize>::default(); 207 | 208 | // Try to generate a key 256 times. Practically, this will succeed and return 209 | // early on the first iteration. 210 | for counter in 0u8..=255 { 211 | // This unwrap is fine. It only triggers if buf is way too big. It's only 212 | // 32 bytes. 213 | hkdf_ctx 214 | .labeled_expand(suite_id, b"candidate", &[counter], &mut buf) 215 | .unwrap(); 216 | 217 | // Apply the bitmask 218 | buf[0] &= $keygen_bitmask; 219 | 220 | // Try to convert to a valid secret key. If the conversion succeeded, 221 | // return the keypair. Recall the invariant of PrivateKey: it is a value in 222 | // the range [1,p). 223 | if let Ok(sk) = PrivateKey::from_bytes(&buf) { 224 | let pk = Self::sk_to_pk(&sk); 225 | return (sk, pk); 226 | } 227 | } 228 | 229 | // The code should never ever get here. The likelihood that we get 256 bad 230 | // samples in a row for P-256 is 2^-8192. For P-384 it's (2^-256)^256. 231 | panic!("DeriveKeyPair failed all attempts"); 232 | } 233 | } 234 | } 235 | }; 236 | } 237 | 238 | use generic_array::typenum; 239 | 240 | #[cfg(feature = "p256")] 241 | nistp_dhkex!( 242 | "P-256", 243 | DhP256, 244 | p256, 245 | typenum::U65, // RFC 9180 §7.1: Npk of DHKEM(P-256, HKDF-SHA256) is 65 246 | typenum::U32, // RFC 9180 §7.1: Nsk of DHKEM(P-256, HKDF-SHA256) is 32 247 | typenum::U32, // RFC 9180 §4.1: Ndh of P-256 is equal to 32 248 | 0xFF // RFC 9180 §7.1.3: The `bitmask` in DeriveKeyPair to be 0xFF for P-256 249 | ); 250 | 251 | #[cfg(feature = "p384")] 252 | nistp_dhkex!( 253 | "P-384", 254 | DhP384, 255 | p384, 256 | typenum::U97, // RFC 9180 §7.1: Npk of DHKEM(P-384, HKDF-SHA384) is 97 257 | typenum::U48, // RFC 9180 §7.1: Nsk of DHKEM(P-384, HKDF-SHA384) is 48 258 | typenum::U48, // RFC 9180 §4.1: Ndh of P-384 is equal to 48 259 | 0xFF // RFC 9180 §7.1.3: The `bitmask` in DeriveKeyPair to be 0xFF for P-384 260 | ); 261 | 262 | #[cfg(feature = "p521")] 263 | nistp_dhkex!( 264 | "P-521", 265 | DhP521, 266 | p521, 267 | typenum::U133, // RFC 9180 §7.1: Npk of DHKEM(P-521, HKDF-SHA512) is 133 268 | typenum::U66, // RFC 9180 §7.1: Nsk of DHKEM(P-521, HKDF-SHA512) is 66 269 | typenum::U66, // RFC 9180 §4.1: Ndh of P-521 is equal to 66 270 | 0x01 // RFC 9180 §7.1.3: The `bitmask` in DeriveKeyPair to be 0x01 for P-521 271 | ); 272 | 273 | #[cfg(test)] 274 | mod tests { 275 | use crate::{dhkex::DhKeyExchange, test_util::dhkex_gen_keypair, Deserializable, Serializable}; 276 | 277 | #[cfg(feature = "p256")] 278 | use super::p256::DhP256; 279 | #[cfg(feature = "p384")] 280 | use super::p384::DhP384; 281 | #[cfg(feature = "p521")] 282 | use super::p521::DhP521; 283 | 284 | use hex_literal::hex; 285 | use rand::{rngs::StdRng, SeedableRng}; 286 | 287 | // 288 | // Test vectors come from RFC 5903 §8.1, §8.2 and §8.3 289 | // https://tools.ietf.org/html/rfc5903 290 | // 291 | 292 | #[cfg(feature = "p256")] 293 | const P256_PRIVKEYS: &[&[u8]] = &[ 294 | &hex!("C88F01F5 10D9AC3F 70A292DA A2316DE5 44E9AAB8 AFE84049 C62A9C57 862D1433"), 295 | &hex!("C6EF9C5D 78AE012A 011164AC B397CE20 88685D8F 06BF9BE0 B283AB46 476BEE53"), 296 | ]; 297 | 298 | // The public keys corresponding to the above private keys, in order 299 | #[cfg(feature = "p256")] 300 | const P256_PUBKEYS: &[&[u8]] = &[ 301 | &hex!( 302 | "04" // Uncompressed 303 | "DAD0B653 94221CF9 B051E1FE CA5787D0 98DFE637 FC90B9EF 945D0C37 72581180" // x-coordinate 304 | "5271A046 1CDB8252 D61F1C45 6FA3E59A B1F45B33 ACCF5F58 389E0577 B8990BB3" // y-coordinate 305 | ), 306 | &hex!( 307 | "04" // Uncompressed 308 | "D12DFB52 89C8D4F8 1208B702 70398C34 2296970A 0BCCB74C 736FC755 4494BF63" // x-coordinate 309 | "56FBF3CA 366CC23E 8157854C 13C58D6A AC23F046 ADA30F83 53E74F33 039872AB" // y-coordinate 310 | ), 311 | ]; 312 | 313 | // The result of DH(privkey0, pubkey1) or equivalently, DH(privkey1, pubkey0) 314 | #[cfg(feature = "p256")] 315 | const P256_DH_RES_XCOORD: &[u8] = 316 | &hex!("D6840F6B 42F6EDAF D13116E0 E1256520 2FEF8E9E CE7DCE03 812464D0 4B9442DE"); 317 | 318 | #[cfg(feature = "p384")] 319 | const P384_PRIVKEYS: &[&[u8]] = &[ 320 | &hex!( 321 | "099F3C70 34D4A2C6 99884D73 A375A67F 7624EF7C 6B3C0F16 0647B674 14DCE655 E35B5380" 322 | "41E649EE 3FAEF896 783AB194" 323 | ), 324 | &hex!( 325 | "41CB0779 B4BDB85D 47846725 FBEC3C94 30FAB46C C8DC5060 855CC9BD A0AA2942 E0308312" 326 | "916B8ED2 960E4BD5 5A7448FC" 327 | ), 328 | ]; 329 | 330 | // The public keys corresponding to the above private keys, in order 331 | #[cfg(feature = "p384")] 332 | const P384_PUBKEYS: &[&[u8]] = &[ 333 | &hex!( 334 | "04" // Uncompressed 335 | "667842D7 D180AC2C DE6F74F3 7551F557 55C7645C 20EF73E3 1634FE72" // x-coordinate 336 | "B4C55EE6 DE3AC808 ACB4BDB4 C88732AE E95F41AA" // ...cont 337 | "9482ED1F C0EEB9CA FC498462 5CCFC23F 65032149 E0E144AD A0241815" // y-coordinate 338 | "35A0F38E EB9FCFF3 C2C947DA E69B4C63 4573A81C" // ...cont 339 | ), 340 | &hex!( 341 | "04" // Uncompressed 342 | "E558DBEF 53EECDE3 D3FCCFC1 AEA08A89 A987475D 12FD950D 83CFA417" // x-coordinate 343 | "32BC509D 0D1AC43A 0336DEF9 6FDA41D0 774A3571" // ...cont 344 | "DCFBEC7A ACF31964 72169E83 8430367F 66EEBE3C 6E70C416 DD5F0C68" // y-coordinate 345 | "759DD1FF F83FA401 42209DFF 5EAAD96D B9E6386C" // ...cont 346 | ), 347 | ]; 348 | 349 | // The result of DH(privkey0, pubkey1) or equivalently, DH(privkey1, pubkey0) 350 | #[cfg(feature = "p384")] 351 | const P384_DH_RES_XCOORD: &[u8] = &hex!( 352 | "11187331 C279962D 93D60424 3FD592CB 9D0A926F 422E4718 7521287E 7156C5C4 D6031355" 353 | "69B9E9D0 9CF5D4A2 70F59746" 354 | ); 355 | 356 | #[cfg(feature = "p521")] 357 | const P521_PRIVKEYS: &[&[u8]] = &[ 358 | &hex!( 359 | "0037ADE9 319A89F4 DABDB3EF 411AACCC A5123C61 ACAB57B5 393DCE47 608172A0" 360 | "95AA85A3 0FE1C295 2C6771D9 37BA9777 F5957B26 39BAB072 462F68C2 7A57382D" 361 | "4A52" 362 | ), 363 | &hex!( 364 | "0145BA99 A847AF43 793FDD0E 872E7CDF A16BE30F DC780F97 BCCC3F07 8380201E" 365 | "9C677D60 0B343757 A3BDBF2A 3163E4C2 F869CCA7 458AA4A4 EFFC311F 5CB15168" 366 | "5EB9" 367 | ), 368 | ]; 369 | 370 | // The public keys corresponding to the above private keys, in order 371 | #[cfg(feature = "p521")] 372 | const P521_PUBKEYS: &[&[u8]] = &[ 373 | &hex!( 374 | "04" // Uncompressed 375 | "0015417E 84DBF28C 0AD3C278 713349DC 7DF153C8 97A1891B D98BAB43 57C9ECBE" // x-coordinate 376 | "E1E3BF42 E00B8E38 0AEAE57C 2D107564 94188594 2AF5A7F4 601723C4 195D176C" // ...cont 377 | "ED3E" // ...cont 378 | "017CAE20 B6641D2E EB695786 D8C94614 6239D099 E18E1D5A 514C739D 7CB4A10A" // y-coordinate 379 | "D8A78801 5AC405D7 799DC75E 7B7D5B6C F2261A6A 7F150743 8BF01BEB 6CA3926F" // ...cont 380 | "9582" // ...cont 381 | ), 382 | &hex!( 383 | "04" // Uncompressed 384 | "00D0B397 5AC4B799 F5BEA16D 5E13E9AF 971D5E9B 984C9F39 728B5E57 39735A21" // x-coordinate 385 | "9B97C356 436ADC6E 95BB0352 F6BE64A6 C2912D4E F2D0433C ED2B6171 640012D9" // ...cont 386 | "460F" // ...cont 387 | "015C6822 6383956E 3BD066E7 97B623C2 7CE0EAC2 F551A10C 2C724D98 52077B87" // y-coordinate 388 | "220B6536 C5C408A1 D2AEBB8E 86D678AE 49CB5709 1F473229 6579AB44 FCD17F0F" // ...cont 389 | "C56A" // ...cont 390 | ), 391 | ]; 392 | 393 | // The result of DH(privkey0, pubkey1) or equivalently, DH(privkey1, pubkey0) 394 | #[cfg(feature = "p521")] 395 | const P521_DH_RES_XCOORD: &[u8] = &hex!( 396 | "01144C7D 79AE6956 BC8EDB8E 7C787C45 21CB086F A64407F9 7894E5E6 B2D79B04" 397 | "D1427E73 CA4BAA24 0A347868 59810C06 B3C715A3 A8CC3151 F2BEE417 996D19F3" 398 | "DDEA" 399 | ); 400 | 401 | // 402 | // Some helper functions for tests 403 | // 404 | 405 | /// Tests the ECDH op against a known answer 406 | #[allow(dead_code)] 407 | fn test_vector_ecdh( 408 | sk_recip_bytes: &[u8], 409 | pk_sender_bytes: &[u8], 410 | dh_res_xcoord_bytes: &[u8], 411 | ) { 412 | // Deserialize the pubkey and privkey and do a DH operation 413 | let sk_recip = Kex::PrivateKey::from_bytes(&sk_recip_bytes).unwrap(); 414 | let pk_sender = Kex::PublicKey::from_bytes(&pk_sender_bytes).unwrap(); 415 | let derived_dh = Kex::dh(&sk_recip, &pk_sender).unwrap(); 416 | 417 | // Assert that the derived DH result matches the test vector. Recall that the HPKE DH 418 | // result is just the x-coordinate, so that's all we can compare 419 | assert_eq!(derived_dh.to_bytes().as_slice(), dh_res_xcoord_bytes,); 420 | } 421 | 422 | /// Tests that an deserialize-serialize round-trip ends up at the same pubkey 423 | #[allow(dead_code)] 424 | fn test_pubkey_serialize_correctness() { 425 | let mut csprng = StdRng::from_os_rng(); 426 | 427 | // We can't do the same thing as in the X25519 tests, since a completely random point 428 | // is not likely to lie on the curve. Instead, we just generate a random point, 429 | // serialize it, deserialize it, and test whether it's the same using impl Eq for 430 | // AffinePoint 431 | 432 | let (_, pubkey) = dhkex_gen_keypair::(&mut csprng); 433 | let pubkey_bytes = pubkey.to_bytes(); 434 | let rederived_pubkey = 435 | ::PublicKey::from_bytes(&pubkey_bytes).unwrap(); 436 | 437 | // See if the re-serialized bytes are the same as the input 438 | assert_eq!(pubkey, rederived_pubkey); 439 | } 440 | 441 | /// Tests the `sk_to_pk` function against known answers 442 | #[allow(dead_code)] 443 | fn test_vector_corresponding_pubkey(sks: &[&[u8]], pks: &[&[u8]]) { 444 | for (sk_bytes, pk_bytes) in sks.iter().zip(pks.iter()) { 445 | // Deserialize the hex values 446 | let sk = Kex::PrivateKey::from_bytes(sk_bytes).unwrap(); 447 | let pk = Kex::PublicKey::from_bytes(pk_bytes).unwrap(); 448 | 449 | // Derive the secret key's corresponding pubkey and check that it matches the given 450 | // pubkey 451 | let derived_pk = Kex::sk_to_pk(&sk); 452 | assert_eq!(derived_pk, pk); 453 | } 454 | } 455 | 456 | /// Tests that an deserialize-serialize round-trip on a DH keypair ends up at the same values 457 | #[allow(dead_code)] 458 | fn test_dh_serialize_correctness() 459 | where 460 | Kex::PrivateKey: PartialEq, 461 | { 462 | let mut csprng = StdRng::from_os_rng(); 463 | 464 | // Make a random keypair and serialize it 465 | let (sk, pk) = dhkex_gen_keypair::(&mut csprng); 466 | let (sk_bytes, pk_bytes) = (sk.to_bytes(), pk.to_bytes()); 467 | 468 | // Now deserialize those bytes 469 | let new_sk = Kex::PrivateKey::from_bytes(&sk_bytes).unwrap(); 470 | let new_pk = Kex::PublicKey::from_bytes(&pk_bytes).unwrap(); 471 | 472 | // See if the deserialized values are the same as the initial ones 473 | assert!(new_sk == sk, "private key doesn't serialize correctly"); 474 | assert!(new_pk == pk, "public key doesn't serialize correctly"); 475 | } 476 | 477 | #[cfg(feature = "p256")] 478 | #[test] 479 | fn test_vector_ecdh_p256() { 480 | test_vector_ecdh::(&P256_PRIVKEYS[0], &P256_PUBKEYS[1], &P256_DH_RES_XCOORD); 481 | } 482 | 483 | #[cfg(feature = "p384")] 484 | #[test] 485 | fn test_vector_ecdh_p384() { 486 | test_vector_ecdh::(&P384_PRIVKEYS[0], &P384_PUBKEYS[1], &P384_DH_RES_XCOORD); 487 | } 488 | 489 | #[cfg(feature = "p521")] 490 | #[test] 491 | fn test_vector_ecdh_p521() { 492 | test_vector_ecdh::(&P521_PRIVKEYS[0], &P521_PUBKEYS[1], &P521_DH_RES_XCOORD); 493 | } 494 | 495 | #[cfg(feature = "p256")] 496 | #[test] 497 | fn test_vector_corresponding_pubkey_p256() { 498 | test_vector_corresponding_pubkey::(P256_PRIVKEYS, P256_PUBKEYS); 499 | } 500 | 501 | #[cfg(feature = "p384")] 502 | #[test] 503 | fn test_vector_corresponding_pubkey_p384() { 504 | test_vector_corresponding_pubkey::(P384_PRIVKEYS, P384_PUBKEYS); 505 | } 506 | 507 | #[cfg(feature = "p521")] 508 | #[test] 509 | fn test_vector_corresponding_pubkey_p521() { 510 | test_vector_corresponding_pubkey::(P521_PRIVKEYS, P521_PUBKEYS); 511 | } 512 | 513 | #[cfg(feature = "p256")] 514 | #[test] 515 | fn test_pubkey_serialize_correctness_p256() { 516 | test_pubkey_serialize_correctness::(); 517 | } 518 | 519 | #[cfg(feature = "p384")] 520 | #[test] 521 | fn test_pubkey_serialize_correctness_p384() { 522 | test_pubkey_serialize_correctness::(); 523 | } 524 | 525 | #[cfg(feature = "p521")] 526 | #[test] 527 | fn test_pubkey_serialize_correctness_p521() { 528 | test_pubkey_serialize_correctness::(); 529 | } 530 | 531 | #[cfg(feature = "p256")] 532 | #[test] 533 | fn test_dh_serialize_correctness_p256() { 534 | test_dh_serialize_correctness::(); 535 | } 536 | 537 | #[cfg(feature = "p384")] 538 | #[test] 539 | fn test_dh_serialize_correctness_p384() { 540 | test_dh_serialize_correctness::(); 541 | } 542 | 543 | #[cfg(feature = "p521")] 544 | #[test] 545 | fn test_dh_serialize_correctness_p521() { 546 | test_dh_serialize_correctness::(); 547 | } 548 | } 549 | -------------------------------------------------------------------------------- /src/dhkex/x25519.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | dhkex::{DhError, DhKeyExchange}, 3 | kdf::{labeled_extract, Kdf as KdfTrait, LabeledExpand}, 4 | util::{enforce_equal_len, enforce_outbuf_len, KemSuiteId}, 5 | Deserializable, HpkeError, Serializable, 6 | }; 7 | 8 | use generic_array::typenum::{self, Unsigned}; 9 | use subtle::{Choice, ConstantTimeEq}; 10 | 11 | // We wrap the types in order to abstract away the dalek dep 12 | 13 | /// An X25519 public key 14 | #[derive(Clone, Debug, Eq, PartialEq)] 15 | pub struct PublicKey(x25519_dalek::PublicKey); 16 | 17 | // The underlying type is zeroize-on-drop 18 | /// An X25519 private key 19 | #[derive(Clone)] 20 | pub struct PrivateKey(x25519_dalek::StaticSecret); 21 | 22 | impl ConstantTimeEq for PrivateKey { 23 | fn ct_eq(&self, other: &Self) -> Choice { 24 | // We can use to_bytes because StaticSecret is only ever constructed from a clamped scalar 25 | self.0.to_bytes().ct_eq(&other.0.to_bytes()) 26 | } 27 | } 28 | 29 | impl PartialEq for PrivateKey { 30 | fn eq(&self, other: &Self) -> bool { 31 | self.ct_eq(other).into() 32 | } 33 | } 34 | impl Eq for PrivateKey {} 35 | 36 | // The underlying type is zeroize-on-drop 37 | /// A bare DH computation result 38 | pub struct KexResult(x25519_dalek::SharedSecret); 39 | 40 | // Oh I love an excuse to break out type-level integers 41 | impl Serializable for PublicKey { 42 | // RFC 9180 §7.1 Table 2: Npk of DHKEM(X25519, HKDF-SHA256) is 32 43 | type OutputSize = typenum::U32; 44 | 45 | // Dalek lets us convert pubkeys to [u8; 32] 46 | fn write_exact(&self, buf: &mut [u8]) { 47 | // Check the length is correct and panic if not 48 | enforce_outbuf_len::(buf); 49 | 50 | buf.copy_from_slice(self.0.as_bytes()); 51 | } 52 | } 53 | 54 | impl Deserializable for PublicKey { 55 | // Dalek lets us convert [u8; 32] to pubkeys. Assuming the input length is correct, this 56 | // conversion is infallible, so no ValidationErrors are raised. 57 | fn from_bytes(encoded: &[u8]) -> Result { 58 | // Pubkeys must be 32 bytes 59 | enforce_equal_len(Self::OutputSize::to_usize(), encoded.len())?; 60 | 61 | // Copy to a fixed-size array 62 | let mut arr = [0u8; 32]; 63 | arr.copy_from_slice(encoded); 64 | Ok(PublicKey(x25519_dalek::PublicKey::from(arr))) 65 | } 66 | } 67 | 68 | impl Serializable for PrivateKey { 69 | // RFC 9180 §7.1 Table 2: Nsk of DHKEM(X25519, HKDF-SHA256) is 32 70 | type OutputSize = typenum::U32; 71 | 72 | // Dalek lets us convert scalars to [u8; 32] 73 | fn write_exact(&self, buf: &mut [u8]) { 74 | // Check the length is correct and panic if not 75 | enforce_outbuf_len::(buf); 76 | 77 | buf.copy_from_slice(self.0.as_bytes()); 78 | } 79 | } 80 | impl Deserializable for PrivateKey { 81 | // Dalek lets us convert [u8; 32] to scalars. Assuming the input length is correct, this 82 | // conversion is infallible, so no ValidationErrors are raised. 83 | fn from_bytes(encoded: &[u8]) -> Result { 84 | // Privkeys must be 32 bytes 85 | enforce_equal_len(Self::OutputSize::to_usize(), encoded.len())?; 86 | 87 | // Copy to a fixed-size array 88 | let mut arr = [0u8; 32]; 89 | arr.copy_from_slice(encoded); 90 | // We don't have to do a zero-check for X25519 private keys. We clamp all private keys upon 91 | // deserialization, and clamped private keys cannot ever be 0 mod curve_order. In fact, 92 | // they can't even be 0 mod q where q is the order of the prime subgroup generated by the 93 | // canonical generator. 94 | // Why? 95 | // A clamped key k is of the form 2^254 + 8j where j is in [0, 2^251-1]. If k = 0 (mod q) 96 | // then k = nq for some n > 0. And since k is a multiple of 8 and q is prime, n must be a 97 | // multiple of 8. However, 8q > 2^257 which is already out of representable range! So k 98 | // cannot be 0 (mod q). 99 | Ok(PrivateKey(x25519_dalek::StaticSecret::from(arr))) 100 | } 101 | } 102 | 103 | impl Serializable for KexResult { 104 | // RFC 9180 §4.1: For X25519 and X448, the size Ndh is equal to 32 and 56, respectively 105 | type OutputSize = typenum::U32; 106 | 107 | // curve25519's point representation is our DH result. We don't have to do anything special. 108 | fn write_exact(&self, buf: &mut [u8]) { 109 | // Check the length is correct and panic if not 110 | enforce_outbuf_len::(buf); 111 | 112 | // Dalek lets us convert shared secrets to to [u8; 32] 113 | buf.copy_from_slice(self.0.as_bytes()); 114 | } 115 | } 116 | 117 | /// Represents ECDH functionality over the X25519 group 118 | pub struct X25519 {} 119 | 120 | impl DhKeyExchange for X25519 { 121 | #[doc(hidden)] 122 | type PublicKey = PublicKey; 123 | #[doc(hidden)] 124 | type PrivateKey = PrivateKey; 125 | #[doc(hidden)] 126 | type KexResult = KexResult; 127 | 128 | /// Converts an X25519 private key to a public key 129 | #[doc(hidden)] 130 | fn sk_to_pk(sk: &PrivateKey) -> PublicKey { 131 | PublicKey(x25519_dalek::PublicKey::from(&sk.0)) 132 | } 133 | 134 | /// Does the DH operation. Returns an error if and only if the DH result was all zeros. This is 135 | /// required by the HPKE spec. The error is converted into the appropriate higher-level error 136 | /// by the caller, i.e., `HpkeError::EncapError` or `HpkeError::DecapError`. 137 | #[doc(hidden)] 138 | fn dh(sk: &PrivateKey, pk: &PublicKey) -> Result { 139 | let res = sk.0.diffie_hellman(&pk.0); 140 | // "Senders and recipients MUST check whether the shared secret is the all-zero value 141 | // and abort if so" 142 | if res.as_bytes().ct_eq(&[0u8; 32]).into() { 143 | Err(DhError) 144 | } else { 145 | Ok(KexResult(res)) 146 | } 147 | } 148 | 149 | // RFC 9180 §7.1.3 150 | // def DeriveKeyPair(ikm): 151 | // dkp_prk = LabeledExtract("", "dkp_prk", ikm) 152 | // sk = LabeledExpand(dkp_prk, "sk", "", Nsk) 153 | // return (sk, pk(sk)) 154 | 155 | /// Deterministically derives a keypair from the given input keying material and ciphersuite 156 | /// ID. The keying material SHOULD have as many bits of entropy as the bit length of a secret 157 | /// key, i.e., 256. 158 | #[doc(hidden)] 159 | fn derive_keypair(suite_id: &KemSuiteId, ikm: &[u8]) -> (PrivateKey, PublicKey) { 160 | // Write the label into a byte buffer and extract from the IKM 161 | let (_, hkdf_ctx) = labeled_extract::(&[], suite_id, b"dkp_prk", ikm); 162 | // The buffer we hold the candidate scalar bytes in. This is the size of a private key. 163 | let mut buf = [0u8; 32]; 164 | hkdf_ctx 165 | .labeled_expand(suite_id, b"sk", &[], &mut buf) 166 | .unwrap(); 167 | 168 | let sk = x25519_dalek::StaticSecret::from(buf); 169 | let pk = x25519_dalek::PublicKey::from(&sk); 170 | 171 | (PrivateKey(sk), PublicKey(pk)) 172 | } 173 | } 174 | 175 | #[cfg(test)] 176 | mod tests { 177 | use crate::{ 178 | dhkex::{x25519::X25519, Deserializable, DhKeyExchange, Serializable}, 179 | test_util::dhkex_gen_keypair, 180 | }; 181 | use generic_array::typenum::Unsigned; 182 | use rand::{rngs::StdRng, RngCore, SeedableRng}; 183 | 184 | /// Tests that an serialize-deserialize round-trip ends up at the same pubkey 185 | #[test] 186 | fn test_pubkey_serialize_correctness() { 187 | type Kex = X25519; 188 | 189 | let mut csprng = StdRng::from_os_rng(); 190 | 191 | // Fill a buffer with randomness 192 | let orig_bytes = { 193 | let mut buf = 194 | [0u8; <::PublicKey as Serializable>::OutputSize::USIZE]; 195 | csprng.fill_bytes(buf.as_mut_slice()); 196 | buf 197 | }; 198 | 199 | // Make a pubkey with those random bytes. Note, that from_bytes() does not clamp the input 200 | // bytes. This is why this test passes. 201 | let pk = ::PublicKey::from_bytes(&orig_bytes).unwrap(); 202 | let pk_bytes = pk.to_bytes(); 203 | 204 | // See if the re-serialized bytes are the same as the input 205 | assert_eq!(orig_bytes.as_slice(), pk_bytes.as_slice()); 206 | } 207 | 208 | /// Tests that an deserialize-serialize round trip on a DH keypair ends up at the same values 209 | #[test] 210 | fn test_dh_serialize_correctness() { 211 | type Kex = X25519; 212 | 213 | let mut csprng = StdRng::from_os_rng(); 214 | 215 | // Make a random keypair and serialize it 216 | let (sk, pk) = dhkex_gen_keypair::(&mut csprng); 217 | let (sk_bytes, pk_bytes) = (sk.to_bytes(), pk.to_bytes()); 218 | 219 | // Now deserialize those bytes 220 | let new_sk = ::PrivateKey::from_bytes(&sk_bytes).unwrap(); 221 | let new_pk = ::PublicKey::from_bytes(&pk_bytes).unwrap(); 222 | 223 | // See if the deserialized values are the same as the initial ones 224 | assert!(new_sk == sk, "private key doesn't serialize correctly"); 225 | assert!(new_pk == pk, "public key doesn't serialize correctly"); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/kat_tests.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | aead::{Aead, AesGcm128, AesGcm256, ChaCha20Poly1305, ExportOnlyAead}, 3 | kdf::{HkdfSha256, HkdfSha384, HkdfSha512, Kdf as KdfTrait}, 4 | kem::{ 5 | self, DhP256HkdfSha256, DhP384HkdfSha384, DhP521HkdfSha512, Kem as KemTrait, SharedSecret, 6 | X25519HkdfSha256, 7 | }, 8 | op_mode::{OpModeR, PskBundle}, 9 | setup::setup_receiver, 10 | Deserializable, HpkeError, Serializable, 11 | }; 12 | 13 | extern crate std; 14 | use std::{fs::File, string::String, vec::Vec}; 15 | 16 | use hex; 17 | use serde::{de::Error as SError, Deserialize, Deserializer}; 18 | use serde_json; 19 | 20 | // For known-answer tests we need to be able to encap with fixed randomness. This allows that. 21 | trait TestableKem: KemTrait { 22 | /// The ephemeral key used in encapsulation. This is the same thing as a private key in the 23 | /// case of DHKEM, but this is not always true 24 | type EphemeralKey: Deserializable; 25 | 26 | // Encap with fixed randomness 27 | #[doc(hidden)] 28 | fn encap_with_eph( 29 | pk_recip: &Self::PublicKey, 30 | sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>, 31 | sk_eph: Self::EphemeralKey, 32 | ) -> Result<(SharedSecret, Self::EncappedKey), HpkeError>; 33 | } 34 | 35 | // Now implement TestableKem for all the KEMs in the KAT 36 | impl TestableKem for X25519HkdfSha256 { 37 | // In DHKEM, ephemeral keys and private keys are both scalars 38 | type EphemeralKey = ::PrivateKey; 39 | 40 | // Call the x25519 deterministic encap function we defined in dhkem.rs 41 | fn encap_with_eph( 42 | pk_recip: &Self::PublicKey, 43 | sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>, 44 | sk_eph: Self::EphemeralKey, 45 | ) -> Result<(SharedSecret, Self::EncappedKey), HpkeError> { 46 | kem::x25519_hkdfsha256::encap_with_eph(pk_recip, sender_id_keypair, sk_eph) 47 | } 48 | } 49 | impl TestableKem for DhP256HkdfSha256 { 50 | // In DHKEM, ephemeral keys and private keys are both scalars 51 | type EphemeralKey = ::PrivateKey; 52 | 53 | // Call the p256 deterministic encap function we defined in dhkem.rs 54 | fn encap_with_eph( 55 | pk_recip: &Self::PublicKey, 56 | sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>, 57 | sk_eph: Self::EphemeralKey, 58 | ) -> Result<(SharedSecret, Self::EncappedKey), HpkeError> { 59 | kem::dhp256_hkdfsha256::encap_with_eph(pk_recip, sender_id_keypair, sk_eph) 60 | } 61 | } 62 | 63 | impl TestableKem for DhP384HkdfSha384 { 64 | // In DHKEM, ephemeral keys and private keys are both scalars 65 | type EphemeralKey = ::PrivateKey; 66 | 67 | // Call the p384 deterministic encap function we defined in dhkem.rs 68 | fn encap_with_eph( 69 | pk_recip: &Self::PublicKey, 70 | sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>, 71 | sk_eph: Self::EphemeralKey, 72 | ) -> Result<(SharedSecret, Self::EncappedKey), HpkeError> { 73 | kem::dhp384_hkdfsha384::encap_with_eph(pk_recip, sender_id_keypair, sk_eph) 74 | } 75 | } 76 | 77 | impl TestableKem for DhP521HkdfSha512 { 78 | // In DHKEM, ephemeral keys and private keys are both scalars 79 | type EphemeralKey = ::PrivateKey; 80 | 81 | // Call the p521 deterministic encap function we defined in dhkem.rs 82 | fn encap_with_eph( 83 | pk_recip: &Self::PublicKey, 84 | sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>, 85 | sk_eph: Self::EphemeralKey, 86 | ) -> Result<(SharedSecret, Self::EncappedKey), HpkeError> { 87 | kem::dhp521_hkdfsha512::encap_with_eph(pk_recip, sender_id_keypair, sk_eph) 88 | } 89 | } 90 | 91 | /// Asserts that the given serializable values are equal 92 | macro_rules! assert_serializable_eq { 93 | ($a:expr, $b:expr, $args:tt) => { 94 | assert_eq!($a.to_bytes(), $b.to_bytes(), $args) 95 | }; 96 | } 97 | 98 | // Tells serde how to deserialize bytes from the hex representation 99 | fn bytes_from_hex<'de, D>(deserializer: D) -> Result, D::Error> 100 | where 101 | D: Deserializer<'de>, 102 | { 103 | let mut hex_str = String::deserialize(deserializer)?; 104 | // Prepend a 0 if it's not even length 105 | if hex_str.len() % 2 == 1 { 106 | hex_str.insert(0, '0'); 107 | } 108 | hex::decode(hex_str).map_err(|e| SError::custom(format!("{:?}", e))) 109 | } 110 | 111 | // Tells serde how to deserialize bytes from an optional field with hex encoding 112 | fn bytes_from_hex_opt<'de, D>(deserializer: D) -> Result>, D::Error> 113 | where 114 | D: Deserializer<'de>, 115 | { 116 | bytes_from_hex(deserializer).map(|v| Some(v)) 117 | } 118 | 119 | // Each individual test case looks like this 120 | #[derive(Clone, serde::Deserialize, Debug)] 121 | struct MainTestVector { 122 | // Parameters 123 | mode: u8, 124 | kem_id: u16, 125 | kdf_id: u16, 126 | aead_id: u16, 127 | #[serde(deserialize_with = "bytes_from_hex")] 128 | info: Vec, 129 | 130 | // Keying material 131 | #[serde(rename = "ikmR", deserialize_with = "bytes_from_hex")] 132 | ikm_recip: Vec, 133 | #[serde(default, rename = "ikmS", deserialize_with = "bytes_from_hex_opt")] 134 | ikm_sender: Option>, 135 | #[serde(rename = "ikmE", deserialize_with = "bytes_from_hex")] 136 | _ikm_eph: Vec, 137 | 138 | // Private keys 139 | #[serde(rename = "skRm", deserialize_with = "bytes_from_hex")] 140 | sk_recip: Vec, 141 | #[serde(default, rename = "skSm", deserialize_with = "bytes_from_hex_opt")] 142 | sk_sender: Option>, 143 | #[serde(rename = "skEm", deserialize_with = "bytes_from_hex")] 144 | sk_eph: Vec, 145 | 146 | // Preshared Key Bundle 147 | #[serde(default, deserialize_with = "bytes_from_hex_opt")] 148 | psk: Option>, 149 | #[serde(default, rename = "psk_id", deserialize_with = "bytes_from_hex_opt")] 150 | psk_id: Option>, 151 | 152 | // Public Keys 153 | #[serde(rename = "pkRm", deserialize_with = "bytes_from_hex")] 154 | pk_recip: Vec, 155 | #[serde(default, rename = "pkSm", deserialize_with = "bytes_from_hex_opt")] 156 | pk_sender: Option>, 157 | #[serde(rename = "pkEm", deserialize_with = "bytes_from_hex")] 158 | _pk_eph: Vec, 159 | 160 | // Key schedule inputs and computations 161 | #[serde(rename = "enc", deserialize_with = "bytes_from_hex")] 162 | encapped_key: Vec, 163 | #[serde(deserialize_with = "bytes_from_hex")] 164 | shared_secret: Vec, 165 | #[serde(rename = "key_schedule_context", deserialize_with = "bytes_from_hex")] 166 | _hpke_context: Vec, 167 | #[serde(rename = "secret", deserialize_with = "bytes_from_hex")] 168 | _key_schedule_secret: Vec, 169 | #[serde(rename = "key", deserialize_with = "bytes_from_hex")] 170 | _aead_key: Vec, 171 | #[serde(rename = "base_nonce", deserialize_with = "bytes_from_hex")] 172 | _aead_base_nonce: Vec, 173 | #[serde(rename = "exporter_secret", deserialize_with = "bytes_from_hex")] 174 | _exporter_secret: Vec, 175 | 176 | encryptions: Vec, 177 | exports: Vec, 178 | } 179 | 180 | #[derive(Clone, serde::Deserialize, Debug)] 181 | struct EncryptionTestVector { 182 | #[serde(rename = "pt", deserialize_with = "bytes_from_hex")] 183 | plaintext: Vec, 184 | #[serde(deserialize_with = "bytes_from_hex")] 185 | aad: Vec, 186 | #[serde(rename = "nonce", deserialize_with = "bytes_from_hex")] 187 | _nonce: Vec, 188 | #[serde(rename = "ct", deserialize_with = "bytes_from_hex")] 189 | ciphertext: Vec, 190 | } 191 | 192 | #[derive(Clone, serde::Deserialize, Debug)] 193 | struct ExporterTestVector { 194 | #[serde(rename = "exporter_context", deserialize_with = "bytes_from_hex")] 195 | export_ctx: Vec, 196 | #[serde(rename = "L")] 197 | export_len: usize, 198 | #[serde(rename = "exported_value", deserialize_with = "bytes_from_hex")] 199 | export_val: Vec, 200 | } 201 | 202 | /// Returns a keypair given the secret bytes and pubkey bytes 203 | fn deser_keypair( 204 | sk_bytes: &[u8], 205 | pk_bytes: &[u8], 206 | ) -> (Kem::PrivateKey, Kem::PublicKey) { 207 | // Deserialize the secret key 208 | let sk = ::PrivateKey::from_bytes(sk_bytes).unwrap(); 209 | // Deserialize the pubkey 210 | let pk = ::PublicKey::from_bytes(pk_bytes).unwrap(); 211 | 212 | (sk, pk) 213 | } 214 | 215 | /// Constructs an `OpModeR` from the given components. The variant constructed is determined solely 216 | /// by `mode_id`. This will panic if there is insufficient data to construct the variants specified 217 | /// by `mode_id`. 218 | fn make_op_mode_r<'a, Kem: KemTrait>( 219 | mode_id: u8, 220 | pk: Option, 221 | psk: Option<&'a [u8]>, 222 | psk_id: Option<&'a [u8]>, 223 | ) -> OpModeR<'a, Kem> { 224 | // Deserialize the optional bundle 225 | let bundle = psk.map(|bytes| PskBundle::new(bytes, psk_id.unwrap()).unwrap()); 226 | 227 | // These better be set if the mode ID calls for them 228 | match mode_id { 229 | 0 => OpModeR::Base, 230 | 1 => OpModeR::Psk(bundle.unwrap()), 231 | 2 => OpModeR::Auth(pk.unwrap()), 232 | 3 => OpModeR::AuthPsk(pk.unwrap(), bundle.unwrap()), 233 | _ => panic!("Invalid mode ID: {}", mode_id), 234 | } 235 | } 236 | 237 | // This does all the legwork 238 | fn test_case(tv: MainTestVector) { 239 | // First, deserialize all the relevant keys so we can reconstruct the encapped key 240 | let recip_keypair = deser_keypair::(&tv.sk_recip, &tv.pk_recip); 241 | let sk_eph = ::EphemeralKey::from_bytes(&tv.sk_eph).unwrap(); 242 | let sender_keypair = { 243 | let pk_sender = &tv.pk_sender.as_ref(); 244 | tv.sk_sender 245 | .as_ref() 246 | .map(|sk| deser_keypair::(sk, pk_sender.unwrap())) 247 | }; 248 | 249 | // Make sure the keys match what we would've gotten had we used DeriveKeyPair 250 | { 251 | let derived_kp = Kem::derive_keypair(&tv.ikm_recip); 252 | assert_serializable_eq!(recip_keypair.0, derived_kp.0, "sk recip doesn't match"); 253 | assert_serializable_eq!(recip_keypair.1, derived_kp.1, "pk recip doesn't match"); 254 | } 255 | if let Some(sks) = sender_keypair.as_ref() { 256 | let derived_kp = Kem::derive_keypair(&tv.ikm_sender.unwrap()); 257 | assert_serializable_eq!(sks.0, derived_kp.0, "sk sender doesn't match"); 258 | assert_serializable_eq!(sks.1, derived_kp.1, "pk sender doesn't match"); 259 | } 260 | 261 | let (sk_recip, pk_recip) = recip_keypair; 262 | 263 | // Now derive the encapped key with the deterministic encap function, using all the inputs 264 | // above 265 | let (shared_secret, encapped_key) = { 266 | let sender_keypair_ref = sender_keypair.as_ref().map(|&(ref sk, ref pk)| (sk, pk)); 267 | Kem::encap_with_eph(&pk_recip, sender_keypair_ref, sk_eph).expect("encap failed") 268 | }; 269 | 270 | // Assert that the derived shared secret key is identical to the one provided 271 | assert_eq!( 272 | shared_secret.0.as_slice(), 273 | tv.shared_secret.as_slice(), 274 | "shared_secret doesn't match" 275 | ); 276 | 277 | // Assert that the derived encapped key is identical to the one provided 278 | { 279 | let provided_encapped_key = 280 | ::EncappedKey::from_bytes(&tv.encapped_key).unwrap(); 281 | assert_serializable_eq!( 282 | encapped_key, 283 | provided_encapped_key, 284 | "encapped keys don't match" 285 | ); 286 | } 287 | 288 | // We're going to test the encryption contexts. First, construct the appropriate OpMode. 289 | let mode = make_op_mode_r( 290 | tv.mode, 291 | sender_keypair.map(|(_, pk)| pk), 292 | tv.psk.as_ref().map(Vec::as_slice), 293 | tv.psk_id.as_ref().map(Vec::as_slice), 294 | ); 295 | let mut aead_ctx = setup_receiver::(&mode, &sk_recip, &encapped_key, &tv.info) 296 | .expect("setup_receiver failed"); 297 | 298 | // Go through all the plaintext-ciphertext pairs of this test vector and assert the 299 | // ciphertext decrypts to the corresponding plaintext 300 | for enc_packet in tv.encryptions { 301 | // Descructure the vector 302 | let EncryptionTestVector { 303 | aad, 304 | ciphertext, 305 | plaintext, 306 | .. 307 | } = enc_packet; 308 | 309 | // Open the ciphertext and assert that it succeeds 310 | let decrypted = aead_ctx.open(&ciphertext, &aad).expect("open failed"); 311 | 312 | // Assert the decrypted payload equals the expected plaintext 313 | assert_eq!(decrypted, plaintext, "plaintexts don't match"); 314 | } 315 | 316 | // Now check that AeadCtx::export returns the expected values 317 | for export in tv.exports { 318 | let mut exported_val = vec![0u8; export.export_len]; 319 | aead_ctx 320 | .export(&export.export_ctx, &mut exported_val) 321 | .unwrap(); 322 | assert_eq!(exported_val, export.export_val, "export values don't match"); 323 | } 324 | } 325 | 326 | // This macro takes in all the supported AEADs, KDFs, and KEMs, and dispatches the given test 327 | // vector to the test case with the appropriate types 328 | macro_rules! dispatch_testcase { 329 | // Step 1: Roll up the AEAD, KDF, and KEM types into tuples. We'll unroll them later 330 | ($tv:ident, ($( $aead_ty:ty ),*), ($( $kdf_ty:ty ),*), ($( $kem_ty:ty ),*)) => { 331 | dispatch_testcase!(@tup1 $tv, ($( $aead_ty ),*), ($( $kdf_ty ),*), ($( $kem_ty ),*)) 332 | }; 333 | // Step 2: Expand with respect to every AEAD 334 | (@tup1 $tv:ident, ($( $aead_ty:ty ),*), $kdf_tup:tt, $kem_tup:tt) => { 335 | $( 336 | dispatch_testcase!(@tup2 $tv, $aead_ty, $kdf_tup, $kem_tup); 337 | )* 338 | }; 339 | // Step 3: Expand with respect to every KDF 340 | (@tup2 $tv:ident, $aead_ty:ty, ($( $kdf_ty:ty ),*), $kem_tup:tt) => { 341 | $( 342 | dispatch_testcase!(@tup3 $tv, $aead_ty, $kdf_ty, $kem_tup); 343 | )* 344 | }; 345 | // Step 4: Expand with respect to every KEM 346 | (@tup3 $tv:ident, $aead_ty:ty, $kdf_ty:ty, ($( $kem_ty:ty ),*)) => { 347 | $( 348 | dispatch_testcase!(@base $tv, $aead_ty, $kdf_ty, $kem_ty); 349 | )* 350 | }; 351 | // Step 5: Now that we're only dealing with 1 type of each kind, do the dispatch. If the test 352 | // vector matches the IDs of these types, run the test case. 353 | (@base $tv:ident, $aead_ty:ty, $kdf_ty:ty, $kem_ty:ty) => { 354 | if let (<$aead_ty>::AEAD_ID, <$kdf_ty>::KDF_ID, <$kem_ty>::KEM_ID) = 355 | ($tv.aead_id, $tv.kdf_id, $tv.kem_id) 356 | { 357 | println!( 358 | "Running test case on {}, {}, {}", 359 | stringify!($aead_ty), 360 | stringify!($kdf_ty), 361 | stringify!($kem_ty) 362 | ); 363 | 364 | let tv = $tv.clone(); 365 | test_case::<$aead_ty, $kdf_ty, $kem_ty>(tv); 366 | 367 | // This is so that code that comes after a dispatch_testcase! invocation will know that 368 | // the test vector matched no known ciphersuites 369 | continue; 370 | } 371 | }; 372 | } 373 | 374 | #[test] 375 | fn kat_test() { 376 | let file = File::open("test-vectors-5f503c5.json").unwrap(); 377 | let tvs: Vec = serde_json::from_reader(file).unwrap(); 378 | 379 | for tv in tvs.into_iter() { 380 | // Ignore everything that doesn't use X25519, P256, P384 or P521, since that's all we support 381 | // right now 382 | if tv.kem_id != X25519HkdfSha256::KEM_ID 383 | && tv.kem_id != DhP256HkdfSha256::KEM_ID 384 | && tv.kem_id != DhP384HkdfSha384::KEM_ID 385 | && tv.kem_id != DhP521HkdfSha512::KEM_ID 386 | { 387 | continue; 388 | } 389 | 390 | // This unrolls into 36 `if let` statements 391 | dispatch_testcase!( 392 | tv, 393 | (AesGcm128, AesGcm256, ChaCha20Poly1305, ExportOnlyAead), 394 | (HkdfSha256, HkdfSha384, HkdfSha512), 395 | ( 396 | X25519HkdfSha256, 397 | DhP256HkdfSha256, 398 | DhP384HkdfSha384, 399 | DhP521HkdfSha512 400 | ) 401 | ); 402 | 403 | // The above macro has a `continue` in every branch. We only get to this line if it failed 404 | // to match every combination of the above primitives. 405 | panic!( 406 | "Unrecognized (AEAD ID, KDF ID, KEM ID) combo: ({}, {}, {})", 407 | tv.aead_id, tv.kdf_id, tv.kem_id 408 | ); 409 | } 410 | } 411 | -------------------------------------------------------------------------------- /src/kdf.rs: -------------------------------------------------------------------------------- 1 | //! Traits and structs for key derivation functions 2 | 3 | use crate::util::write_u16_be; 4 | 5 | use digest::{core_api::BlockSizeUser, Digest, OutputSizeUser}; 6 | use generic_array::GenericArray; 7 | use hmac::SimpleHmac; 8 | use sha2::{Sha256, Sha384, Sha512}; 9 | 10 | const VERSION_LABEL: &[u8] = b"HPKE-v1"; 11 | 12 | // This is the maximum value of Nh. It is achieved by HKDF-SHA512 in RFC 9180 §7.2. 13 | pub(crate) const MAX_DIGEST_SIZE: usize = 64; 14 | 15 | // Pretty much all the KDF functionality is covered by the hkdf crate 16 | 17 | /// Represents key derivation functionality 18 | pub trait Kdf { 19 | /// The underlying hash function 20 | #[doc(hidden)] 21 | type HashImpl: Clone + Digest + OutputSizeUser + BlockSizeUser; 22 | 23 | /// The algorithm identifier for a KDF implementation 24 | const KDF_ID: u16; 25 | } 26 | 27 | // We use Kdf as a type parameter, so this is to avoid ambiguity. 28 | use Kdf as KdfTrait; 29 | 30 | // Convenience types for the functions below 31 | pub(crate) type DigestArray = 32 | GenericArray::HashImpl as OutputSizeUser>::OutputSize>; 33 | pub(crate) type SimpleHkdf = 34 | hkdf::Hkdf<::HashImpl, SimpleHmac<::HashImpl>>; 35 | type SimpleHkdfExtract = 36 | hkdf::HkdfExtract<::HashImpl, SimpleHmac<::HashImpl>>; 37 | 38 | /// The implementation of HKDF-SHA256 39 | pub struct HkdfSha256 {} 40 | 41 | impl KdfTrait for HkdfSha256 { 42 | #[doc(hidden)] 43 | type HashImpl = Sha256; 44 | 45 | // RFC 9180 §7.2: HKDF-SHA256 46 | const KDF_ID: u16 = 0x0001; 47 | } 48 | 49 | /// The implementation of HKDF-SHA384 50 | pub struct HkdfSha384 {} 51 | 52 | impl KdfTrait for HkdfSha384 { 53 | #[doc(hidden)] 54 | type HashImpl = Sha384; 55 | 56 | // RFC 9180 §7.2: HKDF-SHA384 57 | const KDF_ID: u16 = 0x0002; 58 | } 59 | 60 | /// The implementation of HKDF-SHA512 61 | pub struct HkdfSha512 {} 62 | 63 | impl KdfTrait for HkdfSha512 { 64 | #[doc(hidden)] 65 | type HashImpl = Sha512; 66 | 67 | // RFC 9180 §7.2: HKDF-SHA512 68 | const KDF_ID: u16 = 0x0003; 69 | } 70 | 71 | // RFC 9180 §4.1 72 | // def ExtractAndExpand(dh, kem_context): 73 | // eae_prk = LabeledExtract("", "eae_prk", dh) 74 | // shared_secret = LabeledExpand(eae_prk, "shared_secret", 75 | // kem_context, Nsecret) 76 | // return shared_secret 77 | 78 | /// Uses the given IKM to extract a secret, and then uses that secret, plus the given suite ID and 79 | /// info string, to expand to the output buffer 80 | #[doc(hidden)] 81 | pub fn extract_and_expand( 82 | ikm: &[u8], 83 | suite_id: &[u8], 84 | info: &[u8], 85 | out: &mut [u8], 86 | ) -> Result<(), hkdf::InvalidLength> { 87 | // Extract using given IKM 88 | let (_, hkdf_ctx) = labeled_extract::(&[], suite_id, b"eae_prk", ikm); 89 | // Expand using given info string 90 | hkdf_ctx.labeled_expand(suite_id, b"shared_secret", info, out) 91 | } 92 | 93 | // RFC 9180 §4 94 | // def LabeledExtract(salt, label, ikm): 95 | // labeled_ikm = concat("HPKE-v1", suite_id, label, ikm) 96 | // return Extract(salt, labeled_ikm) 97 | 98 | /// Returns the HKDF context derived from `(salt=salt, ikm="HPKE-v1"||suite_id||label||ikm)` 99 | #[doc(hidden)] 100 | pub fn labeled_extract( 101 | salt: &[u8], 102 | suite_id: &[u8], 103 | label: &[u8], 104 | ikm: &[u8], 105 | ) -> (DigestArray, SimpleHkdf) { 106 | // Call HKDF-Extract with the IKM being the concatenation of all of the above 107 | let mut extract_ctx = SimpleHkdfExtract::::new(Some(salt)); 108 | extract_ctx.input_ikm(VERSION_LABEL); 109 | extract_ctx.input_ikm(suite_id); 110 | extract_ctx.input_ikm(label); 111 | extract_ctx.input_ikm(ikm); 112 | extract_ctx.finalize() 113 | } 114 | 115 | // This trait only exists so I can implement it for hkdf::Hkdf 116 | #[doc(hidden)] 117 | pub trait LabeledExpand { 118 | /// Does a `LabeledExpand` key derivation function using HKDF. If `out.len()` is more than 255x 119 | /// the digest size (in bytes) of the underlying hash function, returns an 120 | /// `Err(hkdf::InvalidLength)`. 121 | fn labeled_expand( 122 | &self, 123 | suite_id: &[u8], 124 | label: &[u8], 125 | info: &[u8], 126 | out: &mut [u8], 127 | ) -> Result<(), hkdf::InvalidLength>; 128 | } 129 | 130 | impl LabeledExpand for hkdf::Hkdf> 131 | where 132 | D: Clone + OutputSizeUser + Digest + BlockSizeUser, 133 | { 134 | // RFC 9180 §4 135 | // def LabeledExpand(prk, label, info, L): 136 | // labeled_info = concat(I2OSP(L, 2), "HPKE-v1", suite_id, 137 | // label, info) 138 | // return Expand(prk, labeled_info, L) 139 | 140 | /// Does a `LabeledExpand` key derivation function using HKDF. If `out.len()` is more than 255x 141 | /// the digest size (in bytes) of the underlying hash function, returns an 142 | /// `Err(hkdf::InvalidLength)`. 143 | fn labeled_expand( 144 | &self, 145 | suite_id: &[u8], 146 | label: &[u8], 147 | info: &[u8], 148 | out: &mut [u8], 149 | ) -> Result<(), hkdf::InvalidLength> { 150 | // We need to write the length as a u16, so that's the de-facto upper bound on length 151 | if out.len() > u16::MAX as usize { 152 | // The error condition is met, since 2^16 is way bigger than 255 * digest_bytelen 153 | return Err(hkdf::InvalidLength); 154 | } 155 | 156 | // Encode the output length in the info string 157 | let mut len_buf = [0u8; 2]; 158 | write_u16_be(&mut len_buf, out.len() as u16); 159 | 160 | // Call HKDF-Expand() with the info string set to the concatenation of all of the above 161 | let labeled_info = [&len_buf, VERSION_LABEL, suite_id, label, info]; 162 | self.expand_multi_info(&labeled_info, out) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /src/kem.rs: -------------------------------------------------------------------------------- 1 | //! Traits and structs for key encapsulation mechanisms 2 | 3 | use crate::{Deserializable, HpkeError, Serializable}; 4 | 5 | use core::fmt::Debug; 6 | 7 | use generic_array::{ArrayLength, GenericArray}; 8 | use rand_core::{CryptoRng, RngCore}; 9 | use zeroize::Zeroize; 10 | 11 | mod dhkem; 12 | pub use dhkem::*; 13 | 14 | /// Represents authenticated encryption functionality 15 | pub trait Kem: Sized { 16 | /// The key exchange's public key type. If you want to generate a keypair, see 17 | /// `Kem::gen_keypair` or `Kem::derive_keypair` 18 | type PublicKey: Clone + Debug + PartialEq + Eq + Serializable + Deserializable; 19 | 20 | /// The key exchange's private key type. If you want to generate a keypair, see 21 | /// `Kem::gen_keypair` or `Kem::derive_keypair` 22 | type PrivateKey: Clone + PartialEq + Eq + Serializable + Deserializable; 23 | 24 | /// Computes the public key of a given private key 25 | fn sk_to_pk(sk: &Self::PrivateKey) -> Self::PublicKey; 26 | /// The encapsulated key for this KEM. This is used by the recipient to derive the shared 27 | /// secret. 28 | type EncappedKey: Clone + Serializable + Deserializable; 29 | 30 | /// The size of a shared secret in this KEM 31 | #[doc(hidden)] 32 | type NSecret: ArrayLength; 33 | 34 | /// The algorithm identifier for a KEM implementation 35 | const KEM_ID: u16; 36 | 37 | /// Deterministically derives a keypair from the given input keying material 38 | /// 39 | /// Requirements 40 | /// ============ 41 | /// This keying material SHOULD have as many bits of entropy as the bit length of a secret key, 42 | /// i.e., `8 * Self::PrivateKey::size()`. For X25519 and P-256, this is 256 bits of 43 | /// entropy. 44 | fn derive_keypair(ikm: &[u8]) -> (Self::PrivateKey, Self::PublicKey); 45 | 46 | /// Generates a random keypair using the given RNG 47 | fn gen_keypair(csprng: &mut R) -> (Self::PrivateKey, Self::PublicKey) { 48 | // Make some keying material that's the size of a private key 49 | let mut ikm: GenericArray::OutputSize> = 50 | GenericArray::default(); 51 | // Fill it with randomness 52 | csprng.fill_bytes(&mut ikm); 53 | // Run derive_keypair using the KEM's KDF 54 | Self::derive_keypair(&ikm) 55 | } 56 | 57 | /// Derives a shared secret given the encapsulated key and the recipients secret key. If 58 | /// `pk_sender_id` is given, the sender's identity will be tied to the shared secret. 59 | /// 60 | /// Return Value 61 | /// ============ 62 | /// Returns a shared secret on success. If an error happened during key exchange, returns 63 | /// `Err(HpkeError::DecapError)`. 64 | #[doc(hidden)] 65 | fn decap( 66 | sk_recip: &Self::PrivateKey, 67 | pk_sender_id: Option<&Self::PublicKey>, 68 | encapped_key: &Self::EncappedKey, 69 | ) -> Result, HpkeError>; 70 | 71 | /// Derives a shared secret and an ephemeral pubkey that the owner of the reciepint's pubkey 72 | /// can use to derive the same shared secret. If `sk_sender_id` is given, the sender's identity 73 | /// will be tied to the shared secret. All this does is generate an ephemeral keypair and pass 74 | /// to `encap_with_eph`. 75 | /// 76 | /// Return Value 77 | /// ============ 78 | /// Returns a shared secret and encapped key on success. If an error happened during key 79 | /// exchange, returns `Err(HpkeError::EncapError)`. 80 | #[doc(hidden)] 81 | fn encap( 82 | pk_recip: &Self::PublicKey, 83 | sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>, 84 | csprng: &mut R, 85 | ) -> Result<(SharedSecret, Self::EncappedKey), HpkeError>; 86 | } 87 | 88 | // Kem is used as a type parameter everywhere. To avoid confusion, alias it 89 | use Kem as KemTrait; 90 | 91 | /// A convenience type for `[u8; NSecret]` for any given KEM 92 | #[doc(hidden)] 93 | pub struct SharedSecret(pub GenericArray); 94 | 95 | impl Default for SharedSecret { 96 | fn default() -> SharedSecret { 97 | SharedSecret(GenericArray::::default()) 98 | } 99 | } 100 | 101 | // SharedSecrets should zeroize on drop 102 | impl Zeroize for SharedSecret { 103 | fn zeroize(&mut self) { 104 | self.0.zeroize() 105 | } 106 | } 107 | impl Drop for SharedSecret { 108 | fn drop(&mut self) { 109 | self.zeroize(); 110 | } 111 | } 112 | 113 | #[cfg(test)] 114 | mod tests { 115 | use crate::{kem::Kem as KemTrait, Deserializable, Serializable}; 116 | 117 | use rand::{rngs::StdRng, SeedableRng}; 118 | 119 | macro_rules! test_encap_correctness { 120 | ($test_name:ident, $kem_ty:ty) => { 121 | /// Tests that encap and decap produce the same shared secret when composed 122 | #[test] 123 | fn $test_name() { 124 | type Kem = $kem_ty; 125 | 126 | let mut csprng = StdRng::from_os_rng(); 127 | let (sk_recip, pk_recip) = Kem::gen_keypair(&mut csprng); 128 | 129 | // Encapsulate a random shared secret 130 | let (auth_shared_secret, encapped_key) = 131 | Kem::encap(&pk_recip, None, &mut csprng).unwrap(); 132 | 133 | // Decap it 134 | let decapped_auth_shared_secret = 135 | Kem::decap(&sk_recip, None, &encapped_key).unwrap(); 136 | 137 | // Ensure that the encapsulated secret is what decap() derives 138 | assert_eq!(auth_shared_secret.0, decapped_auth_shared_secret.0); 139 | 140 | // 141 | // Now do it with the auth, i.e., using the sender's identity keys 142 | // 143 | 144 | // Make a sender identity keypair 145 | let (sk_sender_id, pk_sender_id) = Kem::gen_keypair(&mut csprng); 146 | 147 | // Encapsulate a random shared secret 148 | let (auth_shared_secret, encapped_key) = Kem::encap( 149 | &pk_recip, 150 | Some((&sk_sender_id, &pk_sender_id.clone())), 151 | &mut csprng, 152 | ) 153 | .unwrap(); 154 | 155 | // Decap it 156 | let decapped_auth_shared_secret = 157 | Kem::decap(&sk_recip, Some(&pk_sender_id), &encapped_key).unwrap(); 158 | 159 | // Ensure that the encapsulated secret is what decap() derives 160 | assert_eq!(auth_shared_secret.0, decapped_auth_shared_secret.0); 161 | } 162 | }; 163 | } 164 | 165 | /// Tests that an deserialize-serialize round trip on an encapped key ends up at the same value 166 | macro_rules! test_encapped_serialize { 167 | ($test_name:ident, $kem_ty:ty) => { 168 | #[test] 169 | fn $test_name() { 170 | type Kem = $kem_ty; 171 | 172 | // Encapsulate a random shared secret 173 | let encapped_key = { 174 | let mut csprng = StdRng::from_os_rng(); 175 | let (_, pk_recip) = Kem::gen_keypair(&mut csprng); 176 | Kem::encap(&pk_recip, None, &mut csprng).unwrap().1 177 | }; 178 | // Serialize it 179 | let encapped_key_bytes = encapped_key.to_bytes(); 180 | // Deserialize it 181 | let new_encapped_key = 182 | <::EncappedKey as Deserializable>::from_bytes( 183 | &encapped_key_bytes, 184 | ) 185 | .unwrap(); 186 | 187 | assert_eq!( 188 | new_encapped_key.0, encapped_key.0, 189 | "encapped key doesn't serialize correctly" 190 | ); 191 | } 192 | }; 193 | } 194 | 195 | #[cfg(feature = "x25519")] 196 | mod x25519_tests { 197 | use super::*; 198 | 199 | test_encap_correctness!(test_encap_correctness_x25519, crate::kem::X25519HkdfSha256); 200 | test_encapped_serialize!(test_encapped_serialize_x25519, crate::kem::X25519HkdfSha256); 201 | } 202 | 203 | #[cfg(feature = "p256")] 204 | mod p256_tests { 205 | use super::*; 206 | 207 | test_encap_correctness!(test_encap_correctness_p256, crate::kem::DhP256HkdfSha256); 208 | test_encapped_serialize!(test_encapped_serialize_p256, crate::kem::DhP256HkdfSha256); 209 | } 210 | 211 | #[cfg(feature = "p384")] 212 | mod p384_tests { 213 | use super::*; 214 | 215 | test_encap_correctness!(test_encap_correctness_p384, crate::kem::DhP384HkdfSha384); 216 | test_encapped_serialize!(test_encapped_serialize_p384, crate::kem::DhP384HkdfSha384); 217 | } 218 | 219 | #[cfg(feature = "p521")] 220 | mod p521_tests { 221 | use super::*; 222 | 223 | test_encap_correctness!(test_encap_correctness_p521, crate::kem::DhP521HkdfSha512); 224 | test_encapped_serialize!(test_encapped_serialize_p521, crate::kem::DhP521HkdfSha512); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/kem/dhkem.rs: -------------------------------------------------------------------------------- 1 | /// Defines DHKEM(G, K) given a Diffie-Hellman group G and KDF K 2 | macro_rules! impl_dhkem { 3 | ( 4 | $mod_name:ident, 5 | $kem_name:ident, 6 | $dhkex:ty, 7 | $kdf:ty, 8 | $kem_id:literal, 9 | $doc_str:expr 10 | ) => { 11 | pub use $mod_name::$kem_name; 12 | 13 | pub(crate) mod $mod_name { 14 | use crate::{ 15 | dhkex::{DhKeyExchange, MAX_PUBKEY_SIZE}, 16 | kdf::{extract_and_expand, Kdf as KdfTrait}, 17 | kem::{Kem as KemTrait, SharedSecret}, 18 | util::{enforce_outbuf_len, kem_suite_id}, 19 | Deserializable, HpkeError, Serializable, 20 | }; 21 | 22 | use digest::OutputSizeUser; 23 | use rand_core::{CryptoRng, RngCore}; 24 | 25 | // Define convenience types 26 | type PublicKey = <$dhkex as DhKeyExchange>::PublicKey; 27 | type PrivateKey = <$dhkex as DhKeyExchange>::PrivateKey; 28 | 29 | // RFC 9180 §4.1 30 | // The function parameters pkR and pkS are deserialized public keys, and enc is a 31 | // serialized public key. Since encapsulated keys are Diffie-Hellman public keys in 32 | // this KEM algorithm, we use SerializePublicKey() and DeserializePublicKey() to 33 | // encode and decode them, respectively. Npk equals Nenc. 34 | 35 | /// Holds the content of an encapsulated secret. This is what the receiver uses to 36 | /// derive the shared secret. This just wraps a pubkey, because that's all an 37 | /// encapsulated key is in a DHKEM. 38 | #[doc(hidden)] 39 | #[derive(Clone)] 40 | pub struct EncappedKey(pub(crate) <$dhkex as DhKeyExchange>::PublicKey); 41 | 42 | // EncappedKeys need to be serializable, since they're gonna be sent over the wire. 43 | // Underlyingly, they're just DH pubkeys, so we just serialize them the same way 44 | impl Serializable for EncappedKey { 45 | type OutputSize = <<$dhkex as DhKeyExchange>::PublicKey as Serializable>::OutputSize; 46 | 47 | // Pass to underlying to_bytes() impl 48 | fn write_exact(&self, buf: &mut [u8]) { 49 | // Check the length is correct and panic if not 50 | enforce_outbuf_len::(buf); 51 | 52 | buf.copy_from_slice(&self.0.to_bytes()); 53 | } 54 | } 55 | 56 | impl Deserializable for EncappedKey { 57 | // Pass to underlying from_bytes() impl 58 | fn from_bytes(encoded: &[u8]) -> Result { 59 | let pubkey = 60 | <<$dhkex as DhKeyExchange>::PublicKey as Deserializable>::from_bytes(encoded)?; 61 | Ok(EncappedKey(pubkey)) 62 | } 63 | } 64 | 65 | // Define the KEM struct 66 | #[doc = $doc_str] 67 | pub struct $kem_name; 68 | 69 | // RFC 9180 §4.1 70 | // def Encap(pkR): 71 | // skE, pkE = GenerateKeyPair() 72 | // dh = DH(skE, pkR) 73 | // enc = SerializePublicKey(pkE) 74 | // 75 | // pkRm = SerializePublicKey(pkR) 76 | // kem_context = concat(enc, pkRm) 77 | // 78 | // def AuthEncap(pkR, skS): 79 | // skE, pkE = GenerateKeyPair() 80 | // dh = concat(DH(skE, pkR), DH(skS, pkR)) 81 | // enc = SerializePublicKey(pkE) 82 | // 83 | // pkRm = SerializePublicKey(pkR) 84 | // pkSm = SerializePublicKey(pk(skS)) 85 | // kem_context = concat(enc, pkRm, pkSm) 86 | // 87 | // shared_secret = ExtractAndExpand(dh, kem_context) 88 | // return shared_secret, enc 89 | 90 | // The reason we define encap_with_eph() rather than just encap() is because we need to 91 | // use deterministic ephemeral keys in the known-answer tests. So we define a function 92 | // here, then use it to impl kem::Kem and kat_tests::TestableKem. 93 | 94 | /// Derives a shared secret that the owner of the recipient's pubkey can use to derive 95 | /// the same shared secret. If `sk_sender_id` is given, the sender's identity will be 96 | /// tied to the shared secret. 97 | /// 98 | /// Return Value 99 | /// ============ 100 | /// Returns a shared secret and encapped key on success. If an error happened during 101 | /// key exchange, returns `Err(HpkeError::EncapError)`. 102 | #[doc(hidden)] 103 | pub(crate) fn encap_with_eph( 104 | pk_recip: &PublicKey, 105 | sender_id_keypair: Option<(&PrivateKey, &PublicKey)>, 106 | sk_eph: PrivateKey, 107 | ) -> Result<(SharedSecret<$kem_name>, EncappedKey), HpkeError> { 108 | // Put together the binding context used for all KDF operations 109 | let suite_id = kem_suite_id::<$kem_name>(); 110 | 111 | // Compute the shared secret from the ephemeral inputs 112 | let kex_res_eph = <$dhkex as DhKeyExchange>::dh(&sk_eph, pk_recip) 113 | .map_err(|_| HpkeError::EncapError)?; 114 | 115 | // The encapped key is the ephemeral pubkey 116 | let encapped_key = { 117 | let pk_eph = <$kem_name as KemTrait>::sk_to_pk(&sk_eph); 118 | EncappedKey(pk_eph) 119 | }; 120 | 121 | // The shared secret is either gonna be kex_res_eph, or that along with another 122 | // shared secret that's tied to the sender's identity. 123 | let shared_secret = if let Some((sk_sender_id, pk_sender_id)) = sender_id_keypair { 124 | // kem_context = encapped_key || pk_recip || pk_sender_id 125 | // We concat without allocation by making a buffer of the maximum possible 126 | // size, then taking the appropriately sized slice. 127 | let (kem_context_buf, kem_context_size) = concat_with_known_maxlen!( 128 | MAX_PUBKEY_SIZE, 129 | &encapped_key.to_bytes(), 130 | &pk_recip.to_bytes(), 131 | &pk_sender_id.to_bytes() 132 | ); 133 | let kem_context = &kem_context_buf[..kem_context_size]; 134 | 135 | // We want to do an authed encap. Do a DH exchange between the sender identity 136 | // secret key and the recipient's pubkey 137 | let kex_res_identity = <$dhkex as DhKeyExchange>::dh(sk_sender_id, pk_recip) 138 | .map_err(|_| HpkeError::EncapError)?; 139 | 140 | // concatted_secrets = kex_res_eph || kex_res_identity 141 | // Same no-alloc concat trick as above 142 | let (concatted_secrets_buf, concatted_secret_size) = concat_with_known_maxlen!( 143 | MAX_PUBKEY_SIZE, 144 | &kex_res_eph.to_bytes(), 145 | &kex_res_identity.to_bytes() 146 | ); 147 | let concatted_secrets = &concatted_secrets_buf[..concatted_secret_size]; 148 | 149 | // The "authed shared secret" is derived from the KEX of the ephemeral input 150 | // with the recipient pubkey, and the KEX of the identity input with the 151 | // recipient pubkey. The HKDF-Expand call only errors if the output values are 152 | // 255x the digest size of the hash function. Since these values are fixed at 153 | // compile time, we don't worry about it. 154 | let mut buf = as Default>::default(); 155 | extract_and_expand::<$kdf>(concatted_secrets, &suite_id, kem_context, &mut buf.0) 156 | .expect("shared secret is way too big"); 157 | buf 158 | } else { 159 | // kem_context = encapped_key || pk_recip 160 | // We concat without allocation by making a buffer of the maximum possible 161 | // size, then taking the appropriately sized slice. 162 | let (kem_context_buf, kem_context_size) = concat_with_known_maxlen!( 163 | MAX_PUBKEY_SIZE, 164 | &encapped_key.to_bytes(), 165 | &pk_recip.to_bytes() 166 | ); 167 | let kem_context = &kem_context_buf[..kem_context_size]; 168 | 169 | // The "unauthed shared secret" is derived from just the KEX of the ephemeral 170 | // input with the recipient pubkey. The HKDF-Expand call only errors if the 171 | // output values are 255x the digest size of the hash function. Since these 172 | // values are fixed at compile time, we don't worry about it. 173 | let mut buf = as Default>::default(); 174 | extract_and_expand::<$kdf>( 175 | &kex_res_eph.to_bytes(), 176 | &suite_id, 177 | kem_context, 178 | &mut buf.0, 179 | ) 180 | .expect("shared secret is way too big"); 181 | buf 182 | }; 183 | 184 | Ok((shared_secret, encapped_key)) 185 | } 186 | 187 | impl KemTrait for $kem_name { 188 | // RFC 9180 §4.1 189 | // For the variants of DHKEM defined in this document, the size Nsecret of the 190 | // KEM shared secret is equal to the output length of the hash function underlying 191 | // the KDF. 192 | 193 | /// The size of the shared secret at the end of the key exchange process 194 | #[doc(hidden)] 195 | type NSecret = <<$kdf as KdfTrait>::HashImpl as OutputSizeUser>::OutputSize; 196 | 197 | type PublicKey = PublicKey; 198 | type PrivateKey = PrivateKey; 199 | type EncappedKey = EncappedKey; 200 | 201 | const KEM_ID: u16 = $kem_id; 202 | 203 | /// Deterministically derives a keypair from the given input keying material 204 | /// 205 | /// Requirements 206 | /// ============ 207 | /// This keying material SHOULD have as many bits of entropy as the bit length of a 208 | /// secret key, i.e., `8 * Self::PrivateKey::size()`. For X25519 and P-256, this is 209 | /// 256 bits of entropy. 210 | fn derive_keypair(ikm: &[u8]) -> (Self::PrivateKey, Self::PublicKey) { 211 | let suite_id = kem_suite_id::(); 212 | <$dhkex as DhKeyExchange>::derive_keypair::<$kdf>(&suite_id, ikm) 213 | } 214 | 215 | /// Computes the public key of a given private key 216 | fn sk_to_pk(sk: &PrivateKey) -> PublicKey { 217 | <$dhkex as DhKeyExchange>::sk_to_pk(sk) 218 | } 219 | 220 | // Runs encap_with_eph using a random ephemeral key 221 | fn encap( 222 | pk_recip: &Self::PublicKey, 223 | sender_id_keypair: Option<(&Self::PrivateKey, &Self::PublicKey)>, 224 | csprng: &mut R, 225 | ) -> Result<(SharedSecret, Self::EncappedKey), HpkeError> { 226 | // Generate a new ephemeral key 227 | let (sk_eph, _) = Self::gen_keypair(csprng); 228 | // Now pass to encap_with_eph() 229 | encap_with_eph(pk_recip, sender_id_keypair, sk_eph) 230 | } 231 | 232 | // RFC 9180 §4.1 233 | // def Decap(enc, skR): 234 | // pkE = DeserializePublicKey(enc) 235 | // dh = DH(skR, pkE) 236 | // 237 | // pkRm = SerializePublicKey(pk(skR)) 238 | // kem_context = concat(enc, pkRm) 239 | // 240 | // shared_secret = ExtractAndExpand(dh, kem_context) 241 | // return shared_secret 242 | // 243 | // def AuthDecap(enc, skR, pkS): 244 | // pkE = DeserializePublicKey(enc) 245 | // dh = concat(DH(skR, pkE), DH(skR, pkS)) 246 | // 247 | // pkRm = SerializePublicKey(pk(skR)) 248 | // pkSm = SerializePublicKey(pkS) 249 | // kem_context = concat(enc, pkRm, pkSm) 250 | // 251 | // shared_secret = ExtractAndExpand(dh, kem_context) 252 | // return shared_secret 253 | 254 | /// Derives a shared secret given the encapsulated key and the recipients secret key. 255 | /// If `pk_sender_id` is given, the sender's identity will be tied to the shared 256 | /// secret. 257 | /// 258 | /// Return Value 259 | /// ============ 260 | /// Returns a shared secret on success. If an error happened during key exchange, 261 | /// returns `Err(HpkeError::DecapError)`. 262 | #[doc(hidden)] 263 | fn decap( 264 | sk_recip: &Self::PrivateKey, 265 | pk_sender_id: Option<&Self::PublicKey>, 266 | encapped_key: &Self::EncappedKey, 267 | ) -> Result, HpkeError> { 268 | // Put together the binding context used for all KDF operations 269 | let suite_id = kem_suite_id::(); 270 | 271 | // Compute the shared secret from the ephemeral inputs 272 | let kex_res_eph = <$dhkex as DhKeyExchange>::dh(sk_recip, &encapped_key.0) 273 | .map_err(|_| HpkeError::DecapError)?; 274 | 275 | // Compute the sender's pubkey from their privkey 276 | let pk_recip = <$dhkex as DhKeyExchange>::sk_to_pk(sk_recip); 277 | 278 | // The shared secret is either gonna be kex_res_eph, or that along with another 279 | // shared secret that's tied to the sender's identity. 280 | if let Some(pk_sender_id) = pk_sender_id { 281 | // kem_context = encapped_key || pk_recip || pk_sender_id We concat without 282 | // allocation by making a buffer of the maximum possible size, then taking the 283 | // appropriately sized slice. 284 | let (kem_context_buf, kem_context_size) = concat_with_known_maxlen!( 285 | MAX_PUBKEY_SIZE, 286 | &encapped_key.to_bytes(), 287 | &pk_recip.to_bytes(), 288 | &pk_sender_id.to_bytes() 289 | ); 290 | let kem_context = &kem_context_buf[..kem_context_size]; 291 | 292 | // We want to do an authed encap. Do a DH exchange between the sender identity 293 | // secret key and the recipient's pubkey 294 | let kex_res_identity = <$dhkex as DhKeyExchange>::dh(sk_recip, pk_sender_id) 295 | .map_err(|_| HpkeError::DecapError)?; 296 | 297 | // concatted_secrets = kex_res_eph || kex_res_identity 298 | // Same no-alloc concat trick as above 299 | let (concatted_secrets_buf, concatted_secret_size) = concat_with_known_maxlen!( 300 | MAX_PUBKEY_SIZE, 301 | &kex_res_eph.to_bytes(), 302 | &kex_res_identity.to_bytes() 303 | ); 304 | let concatted_secrets = &concatted_secrets_buf[..concatted_secret_size]; 305 | 306 | // The "authed shared secret" is derived from the KEX of the ephemeral input 307 | // with the recipient pubkey, and the kex of the identity input with the 308 | // recipient pubkey. The HKDF-Expand call only errors if the output values are 309 | // 255x the digest size of the hash function. Since these values are fixed at 310 | // compile time, we don't worry about it. 311 | let mut shared_secret = as Default>::default(); 312 | extract_and_expand::<$kdf>( 313 | concatted_secrets, 314 | &suite_id, 315 | kem_context, 316 | &mut shared_secret.0, 317 | ) 318 | .expect("shared secret is way too big"); 319 | Ok(shared_secret) 320 | } else { 321 | // kem_context = encapped_key || pk_recip || pk_sender_id 322 | // We concat without allocation by making a buffer of the maximum possible 323 | // size, then taking the appropriately sized slice. 324 | let (kem_context_buf, kem_context_size) = concat_with_known_maxlen!( 325 | MAX_PUBKEY_SIZE, 326 | &encapped_key.to_bytes(), 327 | &pk_recip.to_bytes() 328 | ); 329 | let kem_context = &kem_context_buf[..kem_context_size]; 330 | 331 | // The "unauthed shared secret" is derived from just the KEX of the ephemeral 332 | // input with the recipient pubkey. The HKDF-Expand call only errors if the 333 | // output values are 255x the digest size of the hash function. Since these 334 | // values are fixed at compile time, we don't worry about it. 335 | let mut shared_secret = as Default>::default(); 336 | extract_and_expand::<$kdf>( 337 | &kex_res_eph.to_bytes(), 338 | &suite_id, 339 | kem_context, 340 | &mut shared_secret.0, 341 | ) 342 | .expect("shared secret is way too big"); 343 | Ok(shared_secret) 344 | } 345 | } 346 | } 347 | } 348 | }; 349 | } 350 | 351 | // Implement DHKEM(X25519, HKDF-SHA256) 352 | #[cfg(feature = "x25519")] 353 | impl_dhkem!( 354 | x25519_hkdfsha256, 355 | X25519HkdfSha256, 356 | crate::dhkex::x25519::X25519, 357 | crate::kdf::HkdfSha256, 358 | 0x0020, 359 | "Represents DHKEM(X25519, HKDF-SHA256)" 360 | ); 361 | 362 | // Implement DHKEM(P-256, HKDF-SHA256) 363 | #[cfg(feature = "p256")] 364 | impl_dhkem!( 365 | dhp256_hkdfsha256, 366 | DhP256HkdfSha256, 367 | crate::dhkex::ecdh_nistp::p256::DhP256, 368 | crate::kdf::HkdfSha256, 369 | 0x0010, 370 | "Represents DHKEM(P-256, HKDF-SHA256)" 371 | ); 372 | 373 | // Implement DHKEM(P-384, HKDF-SHA384) 374 | #[cfg(feature = "p384")] 375 | impl_dhkem!( 376 | dhp384_hkdfsha384, 377 | DhP384HkdfSha384, 378 | crate::dhkex::ecdh_nistp::p384::DhP384, 379 | crate::kdf::HkdfSha384, 380 | 0x0011, 381 | "Represents DHKEM(P-384, HKDF-SHA384)" 382 | ); 383 | 384 | // Implement DHKEM(P-521, HKDF-SHA512) 385 | #[cfg(feature = "p521")] 386 | impl_dhkem!( 387 | dhp521_hkdfsha512, 388 | DhP521HkdfSha512, 389 | crate::dhkex::ecdh_nistp::p521::DhP521, 390 | crate::kdf::HkdfSha512, 391 | 0x0012, 392 | "Represents DHKEM(P-521, HKDF-SHA512)" 393 | ); 394 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # hpke 2 | //! **WARNING:** This code has not been audited. Use at your own discretion. 3 | //! 4 | //! This is a pure Rust implementation of the 5 | //! [HPKE](https://datatracker.ietf.org/doc/rfc9180/) hybrid encryption scheme (RFC 9180). The 6 | //! purpose of hybrid encryption is to use allow someone to send secure messages to an entity whose 7 | //! public key they know. Here's an example of Alice and Bob, where Alice knows Bob's public key: 8 | //! 9 | //! ``` 10 | //! # #[cfg(any(feature = "alloc", feature = "std"))] { 11 | //! # #[cfg(feature = "x25519")] 12 | //! # { 13 | //! # use rand::{rngs::StdRng, SeedableRng}; 14 | //! # use hpke::{ 15 | //! # aead::ChaCha20Poly1305, 16 | //! # kdf::HkdfSha384, 17 | //! # kem::X25519HkdfSha256, 18 | //! # Kem as KemTrait, OpModeR, OpModeS, setup_receiver, setup_sender, 19 | //! # }; 20 | //! // These types define the ciphersuite Alice and Bob will be using 21 | //! type Kem = X25519HkdfSha256; 22 | //! type Aead = ChaCha20Poly1305; 23 | //! type Kdf = HkdfSha384; 24 | //! 25 | //! let mut csprng = StdRng::from_os_rng(); 26 | //! # let (bob_sk, bob_pk) = Kem::gen_keypair(&mut csprng); 27 | //! 28 | //! // This is a description string for the session. Both Alice and Bob need to know this value. 29 | //! // It's not secret. 30 | //! let info_str = b"Alice and Bob's weekly chat"; 31 | //! 32 | //! // Alice initiates a session with Bob. OpModeS::Base means that Alice is not authenticating 33 | //! // herself at all. If she had a public key herself, or a pre-shared secret that Bob also 34 | //! // knew, she'd be able to authenticate herself. See the OpModeS and OpModeR types for more 35 | //! // detail. 36 | //! let (encapsulated_key, mut encryption_context) = 37 | //! hpke::setup_sender::(&OpModeS::Base, &bob_pk, info_str, &mut csprng) 38 | //! .expect("invalid server pubkey!"); 39 | //! 40 | //! // Alice encrypts a message to Bob. `aad` is authenticated associated data that is not 41 | //! // encrypted. 42 | //! let msg = b"fronthand or backhand?"; 43 | //! let aad = b"a gentleman's game"; 44 | //! // To seal without allocating: 45 | //! // let auth_tag = encryption_context.seal_in_place_detached(&mut msg, aad)?; 46 | //! // To seal with allocating: 47 | //! let ciphertext = encryption_context.seal(msg, aad).expect("encryption failed!"); 48 | //! 49 | //! // ~~~ 50 | //! // Alice sends the encapsulated key, message ciphertext, AAD, and auth tag to Bob over the 51 | //! // internet. Alice doesn't care if it's an insecure connection, because only Bob can read 52 | //! // her ciphertext. 53 | //! // ~~~ 54 | //! 55 | //! // Somewhere far away, Bob receives the data and makes a decryption session 56 | //! let mut decryption_context = 57 | //! hpke::setup_receiver::( 58 | //! &OpModeR::Base, 59 | //! &bob_sk, 60 | //! &encapsulated_key, 61 | //! info_str, 62 | //! ).expect("failed to set up receiver!"); 63 | //! // To open without allocating: 64 | //! // decryption_context.open_in_place_detached(&mut ciphertext, aad, &auth_tag) 65 | //! // To open with allocating: 66 | //! let plaintext = decryption_context.open(&ciphertext, aad).expect("invalid ciphertext!"); 67 | //! 68 | //! assert_eq!(&plaintext, b"fronthand or backhand?"); 69 | //! # } 70 | //! # } 71 | //! ``` 72 | 73 | // The doc_cfg feature is only available in nightly. It lets us mark items in documentation as 74 | // dependent on specific features. 75 | #![cfg_attr(docsrs, feature(doc_cfg))] 76 | //-------- no_std stuff --------// 77 | #![no_std] 78 | 79 | #[cfg(feature = "std")] 80 | #[allow(unused_imports)] 81 | #[macro_use] 82 | extern crate std; 83 | 84 | #[cfg(feature = "std")] 85 | pub(crate) use std::vec::Vec; 86 | 87 | #[cfg(all(feature = "alloc", not(feature = "std")))] 88 | #[allow(unused_imports)] 89 | #[macro_use] 90 | extern crate alloc; 91 | 92 | #[cfg(all(feature = "alloc", not(feature = "std")))] 93 | pub(crate) use alloc::vec::Vec; 94 | 95 | //-------- Testing stuff --------// 96 | 97 | // kat_tests tests all the implemented ciphersuites, and thus needs all the dependencies. It also 98 | // needs std for file IO. 99 | #[cfg(all( 100 | test, 101 | feature = "std", 102 | feature = "x25519", 103 | feature = "p256", 104 | feature = "p384", 105 | feature = "p521" 106 | ))] 107 | mod kat_tests; 108 | 109 | #[cfg(test)] 110 | mod test_util; 111 | 112 | //-------- Modules and exports--------// 113 | 114 | // Re-export our versions of generic_array and rand_core, since their traits and types are exposed 115 | // in this crate 116 | pub use generic_array; 117 | pub use rand_core; 118 | 119 | #[macro_use] 120 | mod util; 121 | 122 | pub mod aead; 123 | mod dhkex; 124 | pub mod kdf; 125 | pub mod kem; 126 | mod op_mode; 127 | mod setup; 128 | mod single_shot; 129 | 130 | #[doc(inline)] 131 | pub use kem::Kem; 132 | #[doc(inline)] 133 | pub use op_mode::{OpModeR, OpModeS, PskBundle}; 134 | #[doc(inline)] 135 | pub use setup::{setup_receiver, setup_sender}; 136 | #[doc(inline)] 137 | pub use single_shot::{single_shot_open_in_place_detached, single_shot_seal_in_place_detached}; 138 | 139 | #[doc(inline)] 140 | #[cfg(any(feature = "alloc", feature = "std"))] 141 | pub use single_shot::{single_shot_open, single_shot_seal}; 142 | 143 | //-------- Top-level types --------// 144 | 145 | use generic_array::{typenum::marker_traits::Unsigned, ArrayLength, GenericArray}; 146 | 147 | /// Describes things that can go wrong in the HPKE protocol 148 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] 149 | pub enum HpkeError { 150 | /// The allowed number of message encryptions has been reached 151 | MessageLimitReached, 152 | /// An error occurred while opening a ciphertext 153 | OpenError, 154 | /// An error occured while sealing a plaintext 155 | SealError, 156 | /// The KDF was asked to output too many bytes 157 | KdfOutputTooLong, 158 | /// An invalid input value was encountered 159 | ValidationError, 160 | /// Encapsulation failed 161 | EncapError, 162 | /// Decapsulation failed 163 | DecapError, 164 | /// An input isn't the right length. First value is the expected length, second is the given 165 | /// length. 166 | IncorrectInputLength(usize, usize), 167 | /// A preshared key bundle was constructed incorrectly 168 | InvalidPskBundle, 169 | } 170 | 171 | impl core::fmt::Display for HpkeError { 172 | fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 173 | match self { 174 | HpkeError::MessageLimitReached => write!(f, "Message limit reached"), 175 | HpkeError::OpenError => write!(f, "Failed to open ciphertext"), 176 | HpkeError::SealError => write!(f, "Failed to seal plaintext"), 177 | HpkeError::KdfOutputTooLong => write!(f, "Too many bytes requested from KDF"), 178 | HpkeError::ValidationError => write!(f, "Input value is invalid"), 179 | HpkeError::EncapError => write!(f, "Encapsulation failed"), 180 | HpkeError::DecapError => write!(f, "Decapsulation failed"), 181 | HpkeError::IncorrectInputLength(expected, given) => write!( 182 | f, 183 | "Incorrect input length. Expected {} bytes. Got {}.", 184 | expected, given 185 | ), 186 | HpkeError::InvalidPskBundle => { 187 | write!(f, "Preshared key bundle is missing a key or key ID") 188 | } 189 | } 190 | } 191 | } 192 | 193 | /// Implemented by types that have a fixed-length byte representation 194 | pub trait Serializable { 195 | /// Serialized size in bytes 196 | type OutputSize: ArrayLength; 197 | 198 | /// Serializes `self` to the given slice. `buf` MUST have length equal to `Self::size()`. 199 | /// 200 | /// Panics 201 | /// ====== 202 | /// Panics if `buf.len() != Self::size()`. 203 | fn write_exact(&self, buf: &mut [u8]); 204 | 205 | /// Serializes `self` to a new array 206 | fn to_bytes(&self) -> GenericArray { 207 | // Make a buffer of the correct size and write to it 208 | let mut buf = GenericArray::default(); 209 | self.write_exact(&mut buf); 210 | // Return the buffer 211 | buf 212 | } 213 | 214 | /// Returns the size (in bytes) of this type when serialized 215 | fn size() -> usize { 216 | Self::OutputSize::to_usize() 217 | } 218 | } 219 | 220 | /// Implemented by types that can be deserialized from byte representation 221 | pub trait Deserializable: Serializable + Sized { 222 | fn from_bytes(encoded: &[u8]) -> Result; 223 | } 224 | 225 | // An Error type is just something that's Debug and Display 226 | #[cfg(feature = "std")] 227 | impl std::error::Error for HpkeError {} 228 | -------------------------------------------------------------------------------- /src/op_mode.rs: -------------------------------------------------------------------------------- 1 | use crate::{kem::Kem as KemTrait, HpkeError}; 2 | 3 | /// Contains preshared key bytes and an identifier. This is intended to go inside an `OpModeR` or 4 | /// `OpModeS` struct. 5 | #[derive(Clone, Copy)] 6 | pub struct PskBundle<'a> { 7 | /// The preshared key 8 | psk: &'a [u8], 9 | /// A bytestring that uniquely identifies this PSK 10 | psk_id: &'a [u8], 11 | } 12 | 13 | impl<'a> PskBundle<'a> { 14 | /// Creates a new preshared key bundle from the given preshared key and its ID 15 | /// 16 | /// Errors 17 | /// ====== 18 | /// `psk` and `psk_id` must either both be empty or both be nonempty. If one is empty while 19 | /// the other is not, then this returns [`HpkeError::InvalidPskBundle`]. 20 | /// 21 | /// Other requirements 22 | /// ================== 23 | /// Other requirements from the HPKE spec: `psk` MUST contain at least 32 bytes of entropy. 24 | /// Further, `psk.len()` SHOULD be at least as long as an extracted key from the KDF you use 25 | /// with `setup_sender`/`setup_receiver`, i.e., at least `Kdf::extracted_key_size()`. 26 | pub fn new(psk: &'a [u8], psk_id: &'a [u8]) -> Result { 27 | // RFC 9180 §5.1: The psk and psk_id fields MUST appear together or not at all 28 | if (psk.is_empty() && psk_id.is_empty()) || (!psk.is_empty() && !psk_id.is_empty()) { 29 | Ok(PskBundle { psk, psk_id }) 30 | } else { 31 | Err(HpkeError::InvalidPskBundle) 32 | } 33 | } 34 | } 35 | 36 | /// The operation mode of the HPKE session (receiver's view). This is how the sender authenticates 37 | /// their identity to the receiver. This authentication information can include a preshared key, 38 | /// the identity key of the sender, both, or neither. `Base` is the only mode that does not provide 39 | /// any kind of sender identity authentication. 40 | pub enum OpModeR<'a, Kem: KemTrait> { 41 | /// No extra information included 42 | Base, 43 | /// A preshared key known to the sender and receiver. If the bundle contents is empty strings, 44 | /// then this is equivalent to `Base`. 45 | Psk(PskBundle<'a>), 46 | /// The identity public key of the sender 47 | Auth(Kem::PublicKey), 48 | /// Both of the above 49 | AuthPsk(Kem::PublicKey, PskBundle<'a>), 50 | } 51 | 52 | // Helper function for setup_receiver 53 | impl OpModeR<'_, Kem> { 54 | /// Returns the sender's identity pubkey if it's specified 55 | pub(crate) fn get_pk_sender_id(&self) -> Option<&Kem::PublicKey> { 56 | match self { 57 | OpModeR::Auth(pk) => Some(pk), 58 | OpModeR::AuthPsk(pk, _) => Some(pk), 59 | _ => None, 60 | } 61 | } 62 | } 63 | 64 | /// The operation mode of the HPKE session (sender's view). This is how the sender authenticates 65 | /// their identity to the receiver. This authentication information can include a preshared key, 66 | /// the identity key of the sender, both, or neither. `Base` is the only mode that does not provide 67 | /// any kind of sender identity authentication. 68 | pub enum OpModeS<'a, Kem: KemTrait> { 69 | /// No extra information included 70 | Base, 71 | /// A preshared key known to the sender and receiver. If the bundle contents is empty strings, 72 | /// then this is equivalent to `Base`. 73 | Psk(PskBundle<'a>), 74 | /// The identity keypair of the sender 75 | Auth((Kem::PrivateKey, Kem::PublicKey)), 76 | /// Both of the above 77 | AuthPsk((Kem::PrivateKey, Kem::PublicKey), PskBundle<'a>), 78 | } 79 | 80 | // Helpers functions for setup_sender and testing 81 | impl OpModeS<'_, Kem> { 82 | /// Returns the sender's identity pubkey if it's specified 83 | pub(crate) fn get_sender_id_keypair(&self) -> Option<(&Kem::PrivateKey, &Kem::PublicKey)> { 84 | match self { 85 | OpModeS::Auth(keypair) => Some((&keypair.0, &keypair.1)), 86 | OpModeS::AuthPsk(keypair, _) => Some((&keypair.0, &keypair.1)), 87 | _ => None, 88 | } 89 | } 90 | } 91 | 92 | /// Represents the convenience methods necessary for getting default values out of the operation 93 | /// mode 94 | pub(crate) trait OpMode { 95 | /// Gets the mode ID (hardcoded based on variant) 96 | fn mode_id(&self) -> u8; 97 | /// If this is a PSK mode, returns the PSK. Otherwise returns the empty string. 98 | fn get_psk_bytes(&self) -> &[u8]; 99 | /// If this is a PSK mode, returns the PSK ID. Otherwise returns the empty string. 100 | fn get_psk_id(&self) -> &[u8]; 101 | } 102 | 103 | impl OpMode for OpModeR<'_, Kem> { 104 | // Defined in RFC 9180 §5 Table 1 105 | fn mode_id(&self) -> u8 { 106 | match self { 107 | OpModeR::Base => 0x00, 108 | OpModeR::Psk(..) => 0x01, 109 | OpModeR::Auth(..) => 0x02, 110 | OpModeR::AuthPsk(..) => 0x03, 111 | } 112 | } 113 | 114 | // Returns the preshared key bytes if it's set in the mode, otherwise returns 115 | // [0u8; Kdf::HashImpl::OutputSize] 116 | fn get_psk_bytes(&self) -> &[u8] { 117 | // RFC 9180 §5.1: default_psk = "" 118 | match self { 119 | OpModeR::Psk(bundle) => bundle.psk, 120 | OpModeR::AuthPsk(_, bundle) => bundle.psk, 121 | _ => &[], 122 | } 123 | } 124 | 125 | // Returns the preshared key ID if it's set in the mode, otherwise returns the emtpy string 126 | fn get_psk_id(&self) -> &[u8] { 127 | // RFC 9180 §5.1: default_psk_id = "" 128 | match self { 129 | OpModeR::Psk(p) => p.psk_id, 130 | OpModeR::AuthPsk(_, p) => p.psk_id, 131 | _ => &[], 132 | } 133 | } 134 | } 135 | 136 | // I know there's a bunch of code reuse here, but it's not so much that I feel the need to abstract 137 | // something away 138 | impl OpMode for OpModeS<'_, Kem> { 139 | // Defined in RFC 9180 §5 Table 1 140 | fn mode_id(&self) -> u8 { 141 | match self { 142 | OpModeS::Base => 0x00, 143 | OpModeS::Psk(..) => 0x01, 144 | OpModeS::Auth(..) => 0x02, 145 | OpModeS::AuthPsk(..) => 0x03, 146 | } 147 | } 148 | 149 | // Returns the preshared key bytes if it's set in the mode, otherwise returns 150 | // [0u8; Kdf::Hashfunction::OutputSize] 151 | fn get_psk_bytes(&self) -> &[u8] { 152 | // RFC 9180 §5.1: default_psk = "" 153 | match self { 154 | OpModeS::Psk(bundle) => bundle.psk, 155 | OpModeS::AuthPsk(_, bundle) => bundle.psk, 156 | _ => &[], 157 | } 158 | } 159 | 160 | // Returns the preshared key ID if it's set in the mode, otherwise returns the emtpy string 161 | fn get_psk_id(&self) -> &[u8] { 162 | // RFC 9180 §5.1: default_psk_id = "" 163 | match self { 164 | OpModeS::Psk(p) => p.psk_id, 165 | OpModeS::AuthPsk(_, p) => p.psk_id, 166 | _ => &[], 167 | } 168 | } 169 | } 170 | 171 | // Test that you can only make a PskBundle if both fields are empty or both fields are nonempty 172 | #[test] 173 | fn psk_bundle_validation() { 174 | assert!(PskBundle::new(b"hello", b"world").is_ok()); 175 | assert!(PskBundle::new(b"", b"").is_ok()); 176 | assert!(PskBundle::new(b"hello", b"").is_err()); 177 | assert!(PskBundle::new(b"", b"world").is_err()); 178 | } 179 | -------------------------------------------------------------------------------- /src/setup.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | aead::{Aead, AeadCtx, AeadCtxR, AeadCtxS}, 3 | kdf::{labeled_extract, DigestArray, Kdf as KdfTrait, LabeledExpand, MAX_DIGEST_SIZE}, 4 | kem::{Kem as KemTrait, SharedSecret}, 5 | op_mode::{OpMode, OpModeR, OpModeS}, 6 | util::full_suite_id, 7 | HpkeError, 8 | }; 9 | 10 | use rand_core::{CryptoRng, RngCore}; 11 | use zeroize::Zeroize; 12 | 13 | /// Secret generated in `derive_enc_ctx` and stored in `AeadCtx` 14 | pub(crate) struct ExporterSecret(pub(crate) DigestArray); 15 | 16 | // We use this to get an empty buffer we can read secret bytes into 17 | impl Default for ExporterSecret { 18 | fn default() -> ExporterSecret { 19 | ExporterSecret(DigestArray::::default()) 20 | } 21 | } 22 | 23 | impl Clone for ExporterSecret { 24 | fn clone(&self) -> ExporterSecret { 25 | ExporterSecret(self.0.clone()) 26 | } 27 | } 28 | 29 | // Zero exporter secrets on drop 30 | impl Drop for ExporterSecret { 31 | fn drop(&mut self) { 32 | self.0.zeroize(); 33 | } 34 | } 35 | 36 | // RFC 9180 §5.1 37 | // def KeySchedule(mode, shared_secret, info, psk, psk_id): 38 | // VerifyPSKInputs(mode, psk, psk_id) 39 | // 40 | // psk_id_hash = LabeledExtract("", "psk_id_hash", psk_id) 41 | // info_hash = LabeledExtract("", "info_hash", info) 42 | // key_schedule_context = concat(mode, psk_id_hash, info_hash) 43 | // 44 | // secret = LabeledExtract(shared_secret, "secret", psk) 45 | // 46 | // key = LabeledExpand(secret, "key", key_schedule_context, Nk) 47 | // base_nonce = LabeledExpand(secret, "base_nonce", 48 | // key_schedule_context, Nn) 49 | // exporter_secret = LabeledExpand(secret, "exp", 50 | // key_schedule_context, Nh) 51 | // 52 | // return Context(key, base_nonce, 0, exporter_secret) 53 | 54 | // This is the KeySchedule function. It runs a KDF over all the parameters, inputs, and secrets, 55 | // and spits out a key-nonce pair to be used for symmetric encryption. 56 | fn derive_enc_ctx( 57 | mode: &O, 58 | shared_secret: SharedSecret, 59 | info: &[u8], 60 | ) -> AeadCtx 61 | where 62 | A: Aead, 63 | Kdf: KdfTrait, 64 | Kem: KemTrait, 65 | O: OpMode, 66 | { 67 | // Put together the binding context used for all KDF operations 68 | let suite_id = full_suite_id::(); 69 | 70 | // In KeySchedule(), 71 | // psk_id_hash = LabeledExtract("", "psk_id_hash", psk_id) 72 | // info_hash = LabeledExtract("", "info_hash", info) 73 | // key_schedule_context = concat(mode, psk_id_hash, info_hash) 74 | 75 | // We concat without allocation by making a buffer of the maximum possible size, then 76 | // taking the appropriately sized slice. 77 | let (sched_context_buf, sched_context_size) = { 78 | let (psk_id_hash, _) = 79 | labeled_extract::(&[], &suite_id, b"psk_id_hash", mode.get_psk_id()); 80 | let (info_hash, _) = labeled_extract::(&[], &suite_id, b"info_hash", info); 81 | 82 | // Yes it's overkill to bound the first input by MAX_DIGEST_SIZE, since it's only 1 byte. 83 | // But whatever, this is pretty clean. 84 | concat_with_known_maxlen!( 85 | MAX_DIGEST_SIZE, 86 | &[mode.mode_id()], 87 | psk_id_hash.as_slice(), 88 | info_hash.as_slice() 89 | ) 90 | }; 91 | let sched_context = &sched_context_buf[..sched_context_size]; 92 | 93 | // In KeySchedule(), 94 | // secret = LabeledExtract(shared_secret, "secret", psk) 95 | // key = LabeledExpand(secret, "key", key_schedule_context, Nk) 96 | // base_nonce = LabeledExpand(secret, "base_nonce", key_schedule_context, Nn) 97 | // exporter_secret = LabeledExpand(secret, "exp", key_schedule_context, Nh) 98 | // Instead of `secret` we derive an HKDF context which we run .expand() on to derive the 99 | // key-nonce pair. 100 | let (_, secret_ctx) = 101 | labeled_extract::(&shared_secret.0, &suite_id, b"secret", mode.get_psk_bytes()); 102 | 103 | // Empty fixed-size buffers 104 | let mut key = crate::aead::AeadKey::::default(); 105 | let mut base_nonce = crate::aead::AeadNonce::::default(); 106 | let mut exporter_secret = as Default>::default(); 107 | 108 | // Fill the key, base nonce, and exporter secret. This only errors if the output values are 109 | // 255x the digest size of the hash function. Since these values are fixed at compile time, we 110 | // don't worry about it. 111 | secret_ctx 112 | .labeled_expand(&suite_id, b"key", sched_context, key.0.as_mut_slice()) 113 | .expect("aead key len is way too big"); 114 | secret_ctx 115 | .labeled_expand( 116 | &suite_id, 117 | b"base_nonce", 118 | sched_context, 119 | base_nonce.0.as_mut_slice(), 120 | ) 121 | .expect("nonce len is way too big"); 122 | secret_ctx 123 | .labeled_expand( 124 | &suite_id, 125 | b"exp", 126 | sched_context, 127 | exporter_secret.0.as_mut_slice(), 128 | ) 129 | .expect("exporter secret len is way too big"); 130 | 131 | AeadCtx::new(&key, base_nonce, exporter_secret) 132 | } 133 | 134 | // RFC 9180 §5.1.4: 135 | // def SetupAuthPSKS(pkR, info, psk, psk_id, skS): 136 | // shared_secret, enc = AuthEncap(pkR, skS) 137 | // return enc, KeyScheduleS(mode_auth_psk, shared_secret, info, 138 | // psk, psk_id) 139 | 140 | /// Initiates an encryption context to the given recipient public key 141 | /// 142 | /// Return Value 143 | /// ============ 144 | /// On success, returns an encapsulated public key (intended to be sent to the recipient), and an 145 | /// encryption context. If an error happened during key encapsulation, returns 146 | /// `Err(HpkeError::EncapError)`. This is the only possible error. 147 | pub fn setup_sender( 148 | mode: &OpModeS, 149 | pk_recip: &Kem::PublicKey, 150 | info: &[u8], 151 | csprng: &mut R, 152 | ) -> Result<(Kem::EncappedKey, AeadCtxS), HpkeError> 153 | where 154 | A: Aead, 155 | Kdf: KdfTrait, 156 | Kem: KemTrait, 157 | R: CryptoRng + RngCore, 158 | { 159 | // If the identity key is set, use it 160 | let sender_id_keypair = mode.get_sender_id_keypair(); 161 | // Do the encapsulation 162 | let (shared_secret, encapped_key) = Kem::encap(pk_recip, sender_id_keypair, csprng)?; 163 | // Use everything to derive an encryption context 164 | let enc_ctx = derive_enc_ctx::<_, _, Kem, _>(mode, shared_secret, info); 165 | 166 | Ok((encapped_key, enc_ctx.into())) 167 | } 168 | 169 | // RFC 9180 §5.1.4 170 | // def SetupAuthPSKR(enc, skR, info, psk, psk_id, pkS): 171 | // shared_secret = AuthDecap(enc, skR, pkS) 172 | // return KeyScheduleR(mode_auth_psk, shared_secret, info, 173 | // psk, psk_id) 174 | 175 | /// Initiates a decryption context given a private key `sk_recip` and an encapsulated key which 176 | /// was encapsulated to `sk_recip`'s corresponding public key 177 | /// 178 | /// Return Value 179 | /// ============ 180 | /// On success, returns a decryption context. If an error happened during key decapsulation, 181 | /// returns `Err(HpkeError::DecapError)`. This is the only possible error. 182 | pub fn setup_receiver( 183 | mode: &OpModeR, 184 | sk_recip: &Kem::PrivateKey, 185 | encapped_key: &Kem::EncappedKey, 186 | info: &[u8], 187 | ) -> Result, HpkeError> 188 | where 189 | A: Aead, 190 | Kdf: KdfTrait, 191 | Kem: KemTrait, 192 | { 193 | // If the identity key is set, use it 194 | let pk_sender_id: Option<&Kem::PublicKey> = mode.get_pk_sender_id(); 195 | // Do the decapsulation 196 | let shared_secret = Kem::decap(sk_recip, pk_sender_id, encapped_key)?; 197 | 198 | // Use everything to derive an encryption context 199 | let enc_ctx = derive_enc_ctx::<_, _, Kem, _>(mode, shared_secret, info); 200 | Ok(enc_ctx.into()) 201 | } 202 | 203 | #[cfg(test)] 204 | mod test { 205 | use super::{setup_receiver, setup_sender}; 206 | use crate::test_util::{aead_ctx_eq, gen_rand_buf, new_op_mode_pair, OpModeKind}; 207 | use crate::{aead::ChaCha20Poly1305, kdf::HkdfSha256, kem::Kem as KemTrait}; 208 | 209 | use rand::{rngs::StdRng, SeedableRng}; 210 | 211 | /// This tests that `setup_sender` and `setup_receiver` derive the same context. We do this by 212 | /// testing that `gen_ctx_kem_pair` returns identical encryption contexts 213 | macro_rules! test_setup_correctness { 214 | ($test_name:ident, $aead_ty:ty, $kdf_ty:ty, $kem_ty:ty) => { 215 | #[test] 216 | fn $test_name() { 217 | type A = $aead_ty; 218 | type Kdf = $kdf_ty; 219 | type Kem = $kem_ty; 220 | 221 | let mut csprng = StdRng::from_os_rng(); 222 | 223 | let info = b"why would you think in a million years that that would actually work"; 224 | 225 | // Generate the receiver's long-term keypair 226 | let (sk_recip, pk_recip) = Kem::gen_keypair(&mut csprng); 227 | 228 | // Try a full setup for all the op modes 229 | for op_mode_kind in &[ 230 | OpModeKind::Base, 231 | OpModeKind::Auth, 232 | OpModeKind::Psk, 233 | OpModeKind::AuthPsk, 234 | ] { 235 | // Generate a mutually agreeing op mode pair 236 | let (psk, psk_id) = (gen_rand_buf(), gen_rand_buf()); 237 | let (sender_mode, receiver_mode) = 238 | new_op_mode_pair::(*op_mode_kind, &psk, &psk_id); 239 | 240 | // Construct the sender's encryption context, and get an encapped key 241 | let (encapped_key, mut aead_ctx1) = setup_sender::( 242 | &sender_mode, 243 | &pk_recip, 244 | &info[..], 245 | &mut csprng, 246 | ) 247 | .unwrap(); 248 | 249 | // Use the encapped key to derive the reciever's encryption context 250 | let mut aead_ctx2 = setup_receiver::( 251 | &receiver_mode, 252 | &sk_recip, 253 | &encapped_key, 254 | &info[..], 255 | ) 256 | .unwrap(); 257 | 258 | // Ensure that the two derived contexts are equivalent 259 | assert!(aead_ctx_eq(&mut aead_ctx1, &mut aead_ctx2)); 260 | } 261 | } 262 | }; 263 | } 264 | 265 | /// Tests that using different input data gives you different encryption contexts 266 | macro_rules! test_setup_soundness { 267 | ($test_name:ident, $aead:ty, $kdf:ty, $kem:ty) => { 268 | #[test] 269 | fn $test_name() { 270 | type A = $aead; 271 | type Kdf = $kdf; 272 | type Kem = $kem; 273 | 274 | let mut csprng = StdRng::from_os_rng(); 275 | 276 | let info = b"why would you think in a million years that that would actually work"; 277 | 278 | // Generate the receiver's long-term keypair 279 | let (sk_recip, pk_recip) = Kem::gen_keypair(&mut csprng); 280 | 281 | // Generate a mutually agreeing op mode pair 282 | let (psk, psk_id) = (gen_rand_buf(), gen_rand_buf()); 283 | let (sender_mode, receiver_mode) = 284 | new_op_mode_pair::(OpModeKind::Base, &psk, &psk_id); 285 | 286 | // Construct the sender's encryption context normally 287 | let (encapped_key, sender_ctx) = 288 | setup_sender::(&sender_mode, &pk_recip, &info[..], &mut csprng) 289 | .unwrap(); 290 | 291 | // Now make a receiver with the wrong info string and ensure it doesn't match the 292 | // sender 293 | let bad_info = b"something else"; 294 | let mut receiver_ctx = setup_receiver::<_, _, Kem>( 295 | &receiver_mode, 296 | &sk_recip, 297 | &encapped_key, 298 | &bad_info[..], 299 | ) 300 | .unwrap(); 301 | assert!(!aead_ctx_eq(&mut sender_ctx.clone(), &mut receiver_ctx)); 302 | 303 | // Now make a receiver with the wrong secret key and ensure it doesn't match the 304 | // sender 305 | let (bad_sk, _) = Kem::gen_keypair(&mut csprng); 306 | let mut aead_ctx2 = 307 | setup_receiver::<_, _, Kem>(&receiver_mode, &bad_sk, &encapped_key, &info[..]) 308 | .unwrap(); 309 | assert!(!aead_ctx_eq(&mut sender_ctx.clone(), &mut aead_ctx2)); 310 | 311 | // Now make a receiver with the wrong encapped key and ensure it doesn't match the 312 | // sender. The reason `bad_encapped_key` is bad is because its underlying key is 313 | // uniformly random, and therefore different from the key that the sender sent. 314 | let (bad_encapped_key, _) = 315 | setup_sender::(&sender_mode, &pk_recip, &info[..], &mut csprng) 316 | .unwrap(); 317 | let mut aead_ctx2 = setup_receiver::<_, _, Kem>( 318 | &receiver_mode, 319 | &sk_recip, 320 | &bad_encapped_key, 321 | &info[..], 322 | ) 323 | .unwrap(); 324 | assert!(!aead_ctx_eq(&mut sender_ctx.clone(), &mut aead_ctx2)); 325 | 326 | // Now make sure that this test was a valid test by ensuring that doing everything 327 | // the right way makes it pass 328 | let mut aead_ctx2 = setup_receiver::<_, _, Kem>( 329 | &receiver_mode, 330 | &sk_recip, 331 | &encapped_key, 332 | &info[..], 333 | ) 334 | .unwrap(); 335 | assert!(aead_ctx_eq(&mut sender_ctx.clone(), &mut aead_ctx2)); 336 | } 337 | }; 338 | } 339 | 340 | #[cfg(feature = "x25519")] 341 | mod x25519_tests { 342 | use super::*; 343 | 344 | test_setup_correctness!( 345 | test_setup_correctness_x25519, 346 | ChaCha20Poly1305, 347 | HkdfSha256, 348 | crate::kem::x25519_hkdfsha256::X25519HkdfSha256 349 | ); 350 | test_setup_soundness!( 351 | test_setup_soundness_x25519, 352 | ChaCha20Poly1305, 353 | HkdfSha256, 354 | crate::kem::x25519_hkdfsha256::X25519HkdfSha256 355 | ); 356 | } 357 | 358 | #[cfg(feature = "p256")] 359 | mod p256_tests { 360 | use super::*; 361 | 362 | test_setup_correctness!( 363 | test_setup_correctness_p256, 364 | ChaCha20Poly1305, 365 | HkdfSha256, 366 | crate::kem::dhp256_hkdfsha256::DhP256HkdfSha256 367 | ); 368 | test_setup_soundness!( 369 | test_setup_soundness_p256, 370 | ChaCha20Poly1305, 371 | HkdfSha256, 372 | crate::kem::dhp256_hkdfsha256::DhP256HkdfSha256 373 | ); 374 | } 375 | 376 | #[cfg(feature = "p384")] 377 | mod p384_tests { 378 | use super::*; 379 | use crate::kdf::HkdfSha384; 380 | 381 | test_setup_correctness!( 382 | test_setup_correctness_p384, 383 | ChaCha20Poly1305, 384 | HkdfSha384, 385 | crate::kem::dhp384_hkdfsha384::DhP384HkdfSha384 386 | ); 387 | test_setup_soundness!( 388 | test_setup_soundness_p384, 389 | ChaCha20Poly1305, 390 | HkdfSha384, 391 | crate::kem::dhp384_hkdfsha384::DhP384HkdfSha384 392 | ); 393 | } 394 | 395 | #[cfg(feature = "p521")] 396 | mod p521_tests { 397 | use super::*; 398 | use crate::kdf::HkdfSha512; 399 | 400 | test_setup_correctness!( 401 | test_setup_correctness_p521, 402 | ChaCha20Poly1305, 403 | HkdfSha512, 404 | crate::kem::dhp521_hkdfsha512::DhP521HkdfSha512 405 | ); 406 | test_setup_soundness!( 407 | test_setup_soundness_p521, 408 | ChaCha20Poly1305, 409 | HkdfSha512, 410 | crate::kem::dhp521_hkdfsha512::DhP521HkdfSha512 411 | ); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /src/single_shot.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | aead::{Aead, AeadTag}, 3 | kdf::Kdf as KdfTrait, 4 | kem::Kem as KemTrait, 5 | op_mode::{OpModeR, OpModeS}, 6 | setup::{setup_receiver, setup_sender}, 7 | HpkeError, 8 | }; 9 | 10 | use rand_core::{CryptoRng, RngCore}; 11 | 12 | // RFC 9180 §6.1 13 | // def SealAuthPSK(pkR, info, aad, pt, psk, psk_id, skS): 14 | // enc, ctx = SetupAuthPSKS(pkR, info, psk, psk_id, skS) 15 | // ct = ctx.Seal(aad, pt) 16 | // return enc, ct 17 | 18 | /// Does a `setup_sender` and `AeadCtxS::seal_in_place_detached` in one shot. That is, it does a 19 | /// key encapsulation to the specified recipient and encrypts the provided plaintext in place. See 20 | /// `setup::setup_sender` and `AeadCtxS::seal_in_place_detached` for more detail. 21 | /// 22 | /// Return Value 23 | /// ============ 24 | /// Returns `Ok((encapped_key, auth_tag))` on success. If an error happened during key 25 | /// encapsulation, returns `Err(HpkeError::EncapError)`. If an error happened during encryption, 26 | /// returns `Err(HpkeError::SealError)`. In this case, the contents of `plaintext` is undefined. 27 | pub fn single_shot_seal_in_place_detached( 28 | mode: &OpModeS, 29 | pk_recip: &Kem::PublicKey, 30 | info: &[u8], 31 | plaintext: &mut [u8], 32 | aad: &[u8], 33 | csprng: &mut R, 34 | ) -> Result<(Kem::EncappedKey, AeadTag), HpkeError> 35 | where 36 | A: Aead, 37 | Kdf: KdfTrait, 38 | Kem: KemTrait, 39 | R: CryptoRng + RngCore, 40 | { 41 | // Encap a key 42 | let (encapped_key, mut aead_ctx) = 43 | setup_sender::(mode, pk_recip, info, csprng)?; 44 | // Encrypt 45 | let tag = aead_ctx.seal_in_place_detached(plaintext, aad)?; 46 | 47 | Ok((encapped_key, tag)) 48 | } 49 | 50 | /// Does a `setup_sender` and `AeadCtxS::seal` in one shot. That is, it does a key encapsulation to 51 | /// the specified recipient and encrypts the provided plaintext. See `setup::setup_sender` and 52 | /// `AeadCtxS::seal` for more detail. 53 | /// 54 | /// Return Value 55 | /// ============ 56 | /// Returns `Ok((encapped_key, ciphertext))` on success. If an error happened during key 57 | /// encapsulation, returns `Err(HpkeError::EncapError)`. If an error happened during encryption, 58 | /// returns `Err(HpkeError::SealError)`. 59 | #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] 60 | #[cfg(any(feature = "alloc", feature = "std"))] 61 | pub fn single_shot_seal( 62 | mode: &OpModeS, 63 | pk_recip: &Kem::PublicKey, 64 | info: &[u8], 65 | plaintext: &[u8], 66 | aad: &[u8], 67 | csprng: &mut R, 68 | ) -> Result<(Kem::EncappedKey, crate::Vec), HpkeError> 69 | where 70 | A: Aead, 71 | Kdf: KdfTrait, 72 | Kem: KemTrait, 73 | R: CryptoRng + RngCore, 74 | { 75 | // Encap a key 76 | let (encapped_key, mut aead_ctx) = 77 | setup_sender::(mode, pk_recip, info, csprng)?; 78 | // Encrypt 79 | let ciphertext = aead_ctx.seal(plaintext, aad)?; 80 | 81 | Ok((encapped_key, ciphertext)) 82 | } 83 | 84 | // RFC 9180 §6.1 85 | // def OpenAuthPSK(enc, skR, info, aad, ct, psk, psk_id, pkS): 86 | // ctx = SetupAuthPSKR(enc, skR, info, psk, psk_id, pkS) 87 | // return ctx.Open(aad, ct) 88 | 89 | /// Does a `setup_receiver` and `AeadCtxR::open_in_place_detached` in one shot. That is, it does a 90 | /// key decapsulation for the specified recipient and decrypts the provided ciphertext in place. 91 | /// See `setup::setup_reciever` and `AeadCtxR::open_in_place_detached` for more detail. 92 | /// 93 | /// Return Value 94 | /// ============ 95 | /// Returns `Ok()` on success. If an error happened during key decapsulation, returns 96 | /// `Err(HpkeError::DecapError)`. If an error happened during decryption, returns 97 | /// `Err(HpkeError::OpenError)`. In this case, the contents of `ciphertext` is undefined. 98 | pub fn single_shot_open_in_place_detached( 99 | mode: &OpModeR, 100 | sk_recip: &Kem::PrivateKey, 101 | encapped_key: &Kem::EncappedKey, 102 | info: &[u8], 103 | ciphertext: &mut [u8], 104 | aad: &[u8], 105 | tag: &AeadTag, 106 | ) -> Result<(), HpkeError> 107 | where 108 | A: Aead, 109 | Kdf: KdfTrait, 110 | Kem: KemTrait, 111 | { 112 | // Decap the key 113 | let mut aead_ctx = setup_receiver::(mode, sk_recip, encapped_key, info)?; 114 | // Decrypt 115 | aead_ctx.open_in_place_detached(ciphertext, aad, tag) 116 | } 117 | 118 | /// Does a `setup_receiver` and `AeadCtxR::open` in one shot. That is, it does a key decapsulation 119 | /// for the specified recipient and decrypts the provided ciphertext. See `setup::setup_reciever` 120 | /// and `AeadCtxR::open` for more detail. 121 | /// 122 | /// Return Value 123 | /// ============ 124 | /// Returns `Ok(plaintext)` on success. If an error happened during key decapsulation, returns 125 | /// `Err(HpkeError::DecapError)`. If an error happened during decryption, returns 126 | /// `Err(HpkeError::OpenError)`. 127 | #[cfg_attr(docsrs, doc(cfg(any(feature = "alloc", feature = "std"))))] 128 | #[cfg(any(feature = "alloc", feature = "std"))] 129 | pub fn single_shot_open( 130 | mode: &OpModeR, 131 | sk_recip: &Kem::PrivateKey, 132 | encapped_key: &Kem::EncappedKey, 133 | info: &[u8], 134 | ciphertext: &[u8], 135 | aad: &[u8], 136 | ) -> Result, HpkeError> 137 | where 138 | A: Aead, 139 | Kdf: KdfTrait, 140 | Kem: KemTrait, 141 | { 142 | // Decap the key 143 | let mut aead_ctx = setup_receiver::(mode, sk_recip, encapped_key, info)?; 144 | // Decrypt 145 | aead_ctx.open(ciphertext, aad) 146 | } 147 | 148 | #[cfg(any(feature = "alloc", feature = "std"))] 149 | #[cfg(test)] 150 | mod test { 151 | use super::*; 152 | use crate::{ 153 | aead::ChaCha20Poly1305, 154 | kem::Kem as KemTrait, 155 | op_mode::{OpModeR, OpModeS, PskBundle}, 156 | test_util::gen_rand_buf, 157 | }; 158 | 159 | use rand::{rngs::StdRng, SeedableRng}; 160 | 161 | macro_rules! test_single_shot_correctness { 162 | ($test_name:ident, $aead:ty, $kdf:ty, $kem:ty) => { 163 | /// Tests that `single_shot_open` can open a `single_shot_seal` ciphertext. This 164 | /// doens't need to be tested for all ciphersuite combinations, since its correctness 165 | /// follows from the correctness of `seal/open` and `setup_sender/setup_receiver`. 166 | #[test] 167 | fn $test_name() { 168 | type A = $aead; 169 | type Kdf = $kdf; 170 | type Kem = $kem; 171 | 172 | let msg = b"Good night, a-ding ding ding ding ding"; 173 | let aad = b"Five four three two one"; 174 | 175 | let mut csprng = StdRng::from_os_rng(); 176 | 177 | // Set up an arbitrary info string, a random PSK, and an arbitrary PSK ID 178 | let info = b"why would you think in a million years that that would actually work"; 179 | let (psk, psk_id) = (gen_rand_buf(), gen_rand_buf()); 180 | let psk_bundle = PskBundle::new(&psk, &psk_id).unwrap(); 181 | 182 | // Generate the sender's and receiver's long-term keypairs 183 | let (sk_sender_id, pk_sender_id) = Kem::gen_keypair(&mut csprng); 184 | let (sk_recip, pk_recip) = Kem::gen_keypair(&mut csprng); 185 | 186 | // Construct the sender's encryption context, and get an encapped key 187 | let sender_mode = OpModeS::::AuthPsk( 188 | (sk_sender_id, pk_sender_id.clone()), 189 | psk_bundle.clone(), 190 | ); 191 | 192 | // Use the encapped key to derive the reciever's encryption context 193 | let receiver_mode = OpModeR::::AuthPsk(pk_sender_id, psk_bundle); 194 | 195 | // Encrypt with the first context 196 | let (encapped_key, ciphertext) = single_shot_seal::( 197 | &sender_mode, 198 | &pk_recip, 199 | info, 200 | msg, 201 | aad, 202 | &mut csprng, 203 | ) 204 | .expect("single_shot_seal() failed"); 205 | 206 | // Make sure seal() isn't a no-op 207 | assert!(&ciphertext[..] != &msg[..]); 208 | 209 | // Decrypt with the second context 210 | let decrypted = single_shot_open::( 211 | &receiver_mode, 212 | &sk_recip, 213 | &encapped_key, 214 | info, 215 | &ciphertext, 216 | aad, 217 | ) 218 | .expect("single_shot_open() failed"); 219 | assert_eq!(&decrypted, &msg); 220 | } 221 | }; 222 | } 223 | 224 | #[cfg(feature = "x25519")] 225 | test_single_shot_correctness!( 226 | test_single_shot_correctness_x25519, 227 | ChaCha20Poly1305, 228 | crate::kdf::HkdfSha256, 229 | crate::kem::x25519_hkdfsha256::X25519HkdfSha256 230 | ); 231 | 232 | #[cfg(feature = "p256")] 233 | test_single_shot_correctness!( 234 | test_single_shot_correctness_p256, 235 | ChaCha20Poly1305, 236 | crate::kdf::HkdfSha256, 237 | crate::kem::dhp256_hkdfsha256::DhP256HkdfSha256 238 | ); 239 | 240 | #[cfg(feature = "p384")] 241 | test_single_shot_correctness!( 242 | test_single_shot_correctness_p384, 243 | ChaCha20Poly1305, 244 | crate::kdf::HkdfSha384, 245 | crate::kem::dhp384_hkdfsha384::DhP384HkdfSha384 246 | ); 247 | 248 | #[cfg(feature = "p521")] 249 | test_single_shot_correctness!( 250 | test_single_shot_correctness_p521, 251 | ChaCha20Poly1305, 252 | crate::kdf::HkdfSha512, 253 | crate::kem::dhp521_hkdfsha512::DhP521HkdfSha512 254 | ); 255 | } 256 | -------------------------------------------------------------------------------- /src/test_util.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | aead::{Aead, AeadCtx, AeadCtxR, AeadCtxS, AeadKey, AeadNonce}, 3 | dhkex::DhKeyExchange, 4 | kdf::Kdf as KdfTrait, 5 | kem::Kem as KemTrait, 6 | op_mode::{OpModeR, OpModeS, PskBundle}, 7 | setup::ExporterSecret, 8 | Serializable, 9 | }; 10 | 11 | use generic_array::GenericArray; 12 | use rand::{rngs::StdRng, CryptoRng, Rng, RngCore, SeedableRng}; 13 | 14 | /// Returns a random 32-byte buffer 15 | pub(crate) fn gen_rand_buf() -> [u8; 32] { 16 | let mut csprng = StdRng::from_os_rng(); 17 | let mut buf = [0u8; 32]; 18 | csprng.fill_bytes(&mut buf); 19 | buf 20 | } 21 | 22 | /// Generates a keypair without the need of a KEM 23 | pub(crate) fn dhkex_gen_keypair( 24 | csprng: &mut R, 25 | ) -> (Kex::PrivateKey, Kex::PublicKey) { 26 | // Make some keying material that's the size of a private key 27 | let mut ikm: GenericArray::OutputSize> = 28 | GenericArray::default(); 29 | // Fill it with randomness 30 | csprng.fill_bytes(&mut ikm); 31 | // Run derive_keypair with a nonsense ciphersuite. We use SHA-512 to satisfy any security level 32 | Kex::derive_keypair::(b"31337", &ikm) 33 | } 34 | 35 | /// Creates a pair of `AeadCtx`s without doing a key exchange 36 | pub(crate) fn gen_ctx_simple_pair() -> (AeadCtxS, AeadCtxR) 37 | where 38 | A: Aead, 39 | Kdf: KdfTrait, 40 | Kem: KemTrait, 41 | { 42 | let mut csprng = StdRng::from_os_rng(); 43 | 44 | // Initialize the key and nonce 45 | let key = { 46 | let mut buf = AeadKey::::default(); 47 | csprng.fill_bytes(buf.0.as_mut_slice()); 48 | buf 49 | }; 50 | let base_nonce = { 51 | let mut buf = AeadNonce::::default(); 52 | csprng.fill_bytes(buf.0.as_mut_slice()); 53 | buf 54 | }; 55 | let exporter_secret = { 56 | let mut buf = ExporterSecret::::default(); 57 | csprng.fill_bytes(buf.0.as_mut_slice()); 58 | buf 59 | }; 60 | 61 | let ctx1 = AeadCtx::new(&key, base_nonce.clone(), exporter_secret.clone()); 62 | let ctx2 = AeadCtx::new(&key, base_nonce, exporter_secret); 63 | 64 | (ctx1.into(), ctx2.into()) 65 | } 66 | 67 | #[derive(Clone, Copy)] 68 | pub(crate) enum OpModeKind { 69 | Base, 70 | Auth, 71 | Psk, 72 | AuthPsk, 73 | } 74 | 75 | /// Makes an agreeing pair of `OpMode`s of the specified variant 76 | pub(crate) fn new_op_mode_pair<'a, Kdf: KdfTrait, Kem: KemTrait>( 77 | kind: OpModeKind, 78 | psk: &'a [u8], 79 | psk_id: &'a [u8], 80 | ) -> (OpModeS<'a, Kem>, OpModeR<'a, Kem>) { 81 | let mut csprng = StdRng::from_os_rng(); 82 | let (sk_sender, pk_sender) = Kem::gen_keypair(&mut csprng); 83 | let psk_bundle = PskBundle::new(psk, psk_id).unwrap(); 84 | 85 | match kind { 86 | OpModeKind::Base => { 87 | let sender_mode = OpModeS::Base; 88 | let receiver_mode = OpModeR::Base; 89 | (sender_mode, receiver_mode) 90 | } 91 | OpModeKind::Psk => { 92 | let sender_mode = OpModeS::Psk(psk_bundle); 93 | let receiver_mode = OpModeR::Psk(psk_bundle); 94 | (sender_mode, receiver_mode) 95 | } 96 | OpModeKind::Auth => { 97 | let sender_mode = OpModeS::Auth((sk_sender, pk_sender.clone())); 98 | let receiver_mode = OpModeR::Auth(pk_sender); 99 | (sender_mode, receiver_mode) 100 | } 101 | OpModeKind::AuthPsk => { 102 | let sender_mode = OpModeS::AuthPsk((sk_sender, pk_sender.clone()), psk_bundle); 103 | let receiver_mode = OpModeR::AuthPsk(pk_sender, psk_bundle); 104 | (sender_mode, receiver_mode) 105 | } 106 | } 107 | } 108 | 109 | /// Evaluates the equivalence of two encryption contexts by doing some encryption-decryption 110 | /// round trips. Returns `true` iff the contexts are equal after 1000 iterations 111 | pub(crate) fn aead_ctx_eq( 112 | sender: &mut AeadCtxS, 113 | receiver: &mut AeadCtxR, 114 | ) -> bool { 115 | let mut csprng = StdRng::from_os_rng(); 116 | 117 | // Some random input data 118 | let msg_len = csprng.random::() as usize; 119 | let msg_buf = { 120 | let mut buf = [0u8; 255]; 121 | csprng.fill_bytes(&mut buf); 122 | buf 123 | }; 124 | let aad_len = csprng.random::() as usize; 125 | let aad_buf = { 126 | let mut buf = [0u8; 255]; 127 | csprng.fill_bytes(&mut buf); 128 | buf 129 | }; 130 | let aad = &aad_buf[..aad_len]; 131 | 132 | // Do 1000 iterations of encryption-decryption. The underlying sequence number increments 133 | // each time. 134 | for i in 0..1000 { 135 | let plaintext = &mut msg_buf.clone()[..msg_len]; 136 | // Encrypt the plaintext 137 | let tag = sender 138 | .seal_in_place_detached(&mut plaintext[..], &aad) 139 | .unwrap_or_else(|_| panic!("seal() #{} failed", i)); 140 | // Rename for clarity 141 | let ciphertext = plaintext; 142 | 143 | // Now to decrypt on the other side 144 | if receiver 145 | .open_in_place_detached(&mut ciphertext[..], &aad, &tag) 146 | .is_err() 147 | { 148 | // An error occurred in decryption. These encryption contexts are not identical. 149 | return false; 150 | } 151 | // Rename for clarity 152 | let roundtrip_plaintext = ciphertext; 153 | 154 | // Make sure the output message was the same as the input message. If it doesn't match, 155 | // early return 156 | if &msg_buf[..msg_len] != roundtrip_plaintext { 157 | return false; 158 | } 159 | } 160 | 161 | true 162 | } 163 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::{aead::Aead, kdf::Kdf as KdfTrait, kem::Kem as KemTrait, HpkeError, Serializable}; 2 | 3 | /// Represents a ciphersuite context. That's "KEMXX", where `XX` is the KEM ID 4 | pub(crate) type KemSuiteId = [u8; 5]; 5 | 6 | /// Represents a ciphersuite context. That's "HPKEXXYYZZ", where `XX` is the KEM ID, `YY` is the 7 | /// KDF ID, and `ZZ` is the AEAD ID 8 | pub(crate) type FullSuiteId = [u8; 10]; 9 | 10 | /// Writes a u16 to a bytestring in big-endian order. `buf.len()` MUST be 2 11 | #[rustfmt::skip] 12 | pub(crate) fn write_u16_be(buf: &mut [u8], n: u16) { 13 | assert_eq!(buf.len(), 2); 14 | buf[0] = ((n & 0xff00) >> 8) as u8; 15 | buf[1] = (n & 0x00ff) as u8; 16 | } 17 | 18 | /// Writes a u64 to a bytestring in big-endian order. `buf.len()` MUST be 8 19 | #[rustfmt::skip] 20 | pub(crate) fn write_u64_be(buf: &mut [u8], n: u64) { 21 | assert_eq!(buf.len(), 8); 22 | buf[0] = ((n & 0xff00000000000000) >> 56) as u8; 23 | buf[1] = ((n & 0x00ff000000000000) >> 48) as u8; 24 | buf[2] = ((n & 0x0000ff0000000000) >> 40) as u8; 25 | buf[3] = ((n & 0x000000ff00000000) >> 32) as u8; 26 | buf[4] = ((n & 0x00000000ff000000) >> 24) as u8; 27 | buf[5] = ((n & 0x0000000000ff0000) >> 16) as u8; 28 | buf[6] = ((n & 0x000000000000ff00) >> 8) as u8; 29 | buf[7] = (n & 0x00000000000000ff) as u8; 30 | } 31 | 32 | // RFC 9180 §5.1 33 | // suite_id = concat( 34 | // "HPKE", 35 | // I2OSP(kem_id, 2), 36 | // I2OSP(kdf_id, 2), 37 | // I2OSP(aead_id, 2) 38 | // ) 39 | 40 | /// Constructs the `suite_id` used as binding context in all functions in `setup` and `aead` 41 | pub(crate) fn full_suite_id() -> FullSuiteId 42 | where 43 | A: Aead, 44 | Kdf: KdfTrait, 45 | Kem: KemTrait, 46 | { 47 | // XX is the KEM ID, YY is the KDF ID, ZZ is the AEAD ID 48 | let mut suite_id = *b"HPKEXXYYZZ"; 49 | 50 | // Write the ciphersuite identifiers to the buffer. Forgive the explicit indexing. 51 | write_u16_be(&mut suite_id[4..6], Kem::KEM_ID); 52 | write_u16_be(&mut suite_id[6..8], Kdf::KDF_ID); 53 | write_u16_be(&mut suite_id[8..10], A::AEAD_ID); 54 | 55 | suite_id 56 | } 57 | 58 | // RFC 9180 §4.1 59 | // suite_id = concat("KEM", I2OSP(kem_id, 2)) 60 | 61 | /// Constructs the `suite_id` used as binding context in all functions in `kem` 62 | pub(crate) fn kem_suite_id() -> KemSuiteId { 63 | // XX is the KEM ID 64 | let mut suite_id = *b"KEMXX"; 65 | 66 | // Write the KEM ID to the buffer. Forgive the explicit indexing. 67 | write_u16_be(&mut suite_id[3..5], Kem::KEM_ID); 68 | 69 | suite_id 70 | } 71 | 72 | /// Returns a const expression that evaluates to the number of arguments it received 73 | macro_rules! count { 74 | () => (0usize); 75 | ( $x:tt $($xs:tt)* ) => (1usize + count!($($xs)*)); 76 | } 77 | 78 | /// Given a length L and a sequence of n bytestrings with length at most L, this does a 79 | /// non-allocating concatentation of the bytestrings. It constructs a big buffer of n*L many bytes 80 | /// writes everything into there, and keeps track of how many bytes it wrote. The macro returns 81 | /// `(buf, num_bytes_written)`. 82 | macro_rules! concat_with_known_maxlen { 83 | ( $maxlen:expr, $( $slice:expr ),* ) => {{ 84 | // The length of the big buffer is the number of items we're concatting times the max 85 | // length of the items. count! tells us how many items we're concatting. 86 | const BUFLEN: usize = count!($($slice)*) * $maxlen; 87 | 88 | // Make the big buffer and iteratively write each slice to the remaining unused space. 89 | // This panics if if we ever run out of space. 90 | let mut buf = [0u8; BUFLEN]; 91 | let mut unused_space = &mut buf[..]; 92 | $( 93 | unused_space = crate::util::write_to_buf(unused_space, $slice); 94 | )* 95 | 96 | let num_bytes_written = BUFLEN - unused_space.len(); 97 | (buf, num_bytes_written) 98 | }}; 99 | } 100 | 101 | /// A helper function that writes to a buffer and returns a slice containing the unwritten portion. 102 | /// If this crate were allowed to use std, we'd just use std::io::Write instead. 103 | pub(crate) fn write_to_buf<'a>(buf: &'a mut [u8], to_write: &[u8]) -> &'a mut [u8] { 104 | buf[..to_write.len()].copy_from_slice(to_write); 105 | &mut buf[to_write.len()..] 106 | } 107 | 108 | /// Takes two lengths and returns an `Err(Error::IncorrectInputLength)` iff they don't match 109 | pub(crate) fn enforce_equal_len(expected_len: usize, given_len: usize) -> Result<(), HpkeError> { 110 | if given_len != expected_len { 111 | Err(HpkeError::IncorrectInputLength(expected_len, given_len)) 112 | } else { 113 | Ok(()) 114 | } 115 | } 116 | 117 | /// Helper function for `Serializable::write_exact`. Takes a buffer and a serializable type `T` and 118 | /// panics iff `buf.len() != T::size()`. 119 | pub(crate) fn enforce_outbuf_len(buf: &[u8]) { 120 | let size = T::size(); 121 | let buf_len = buf.len(); 122 | assert!( 123 | size == buf_len, 124 | "write_exact(): serialized size ({}) does not equal buffer length ({})", 125 | size, 126 | buf_len, 127 | ); 128 | } 129 | --------------------------------------------------------------------------------