diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 577158ea342..5a6fc904000 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -179,10 +179,10 @@ jobs: - name: Cross-compilation if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' || matrix.target == 'i686-unknown-linux-gnu' }} working-directory: ./aws-lc-rs - run: cross test --target ${{ matrix.target }} + run: cross test --features unstable --target ${{ matrix.target }} - name: Cross-compilation w/ bindgen working-directory: ./aws-lc-rs - run: cross test --release --features bindgen --target ${{ matrix.target }} + run: cross test --release --features bindgen,unstable --target ${{ matrix.target }} aws-lc-rs-platform-build: name: Cross-platform build @@ -205,7 +205,7 @@ jobs: target: ${{ matrix.target }} - name: Run cargo test working-directory: ./aws-lc-rs - run: cargo test --features bindgen --target ${{ matrix.target }} + run: cargo test --features bindgen,unstable --target ${{ matrix.target }} env: DYLD_ROOT_PATH: "/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot" @@ -242,12 +242,12 @@ jobs: rust: [ stable ] os: [ ubuntu-latest, macos-12, macos-13-xlarge ] args: - - --all-targets - - --release --all-targets - - --no-default-features --features non-fips - - --no-default-features --features non-fips,ring-io - - --no-default-features --features non-fips,ring-sig-verify - - --no-default-features --features non-fips,alloc + - --all-targets --features unstable + - --release --all-targets --features unstable + - --no-default-features --features non-fips,unstable + - --no-default-features --features non-fips,ring-io,unstable + - --no-default-features --features non-fips,ring-sig-verify,unstable + - --no-default-features --features non-fips,alloc,unstable steps: - uses: actions/checkout@v3 with: @@ -276,12 +276,11 @@ jobs: rust: [ stable ] os: [ ubuntu-latest, macos-12, macos-13-xlarge ] args: - - --release --all-targets --features fips - - --no-default-features --features fips - - --no-default-features --features fips,ring-io - - --no-default-features --features fips,ring-sig-verify - - --no-default-features --features fips,alloc - - --no-default-features --features fips,bindgen + - --release --all-targets --features fips,unstable + - --no-default-features --features fips,unstable + - --no-default-features --features fips,ring-io,unstable + - --no-default-features --features fips,ring-sig-verify,unstable + - --no-default-features --features fips,alloc,unstable steps: - uses: actions/checkout@v3 with: @@ -309,8 +308,8 @@ jobs: rust: [ stable ] os: [ ubuntu-latest, macos-12, macos-13-xlarge ] args: - - --no-default-features --features aws-lc-sys,bindgen - - --release --all-targets --features bindgen + - --no-default-features --features aws-lc-sys,bindgen,unstable + - --release --all-targets --features bindgen,unstable steps: - uses: actions/checkout@v3 with: @@ -333,13 +332,13 @@ jobs: rust: [ stable ] os: [ windows-2019 ] args: - - --all-targets - - --all-targets --features bindgen - - --release --all-targets - - --no-default-features --features non-fips - - --no-default-features --features non-fips,ring-io - - --no-default-features --features non-fips,ring-sig-verify - - --no-default-features --features non-fips,alloc + - --all-targets --features unstable + - --all-targets --features bindgen,unstable + - --release --all-targets --features unstable + - --no-default-features --features non-fips,unstable + - --no-default-features --features non-fips,ring-io,unstable + - --no-default-features --features non-fips,ring-sig-verify,unstable + - --no-default-features --features non-fips,alloc,unstable steps: - uses: ilammy/setup-nasm@v1 - uses: actions/checkout@v3 @@ -414,7 +413,7 @@ jobs: - name: Run coverage working-directory: ./aws-lc-rs - run: cargo llvm-cov --workspace --no-fail-fast --ignore-filename-regex "aws-lc-(fips-)?sys/.*" --lcov --output-path ${{ runner.temp }}/lcov.info + run: cargo llvm-cov --workspace --features unstable --no-fail-fast --ignore-filename-regex "aws-lc-(fips-)?sys/.*" --lcov --output-path ${{ runner.temp }}/lcov.info - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v3 env: diff --git a/aws-lc-rs-testing/Cargo.toml b/aws-lc-rs-testing/Cargo.toml index 4de691ec413..86b17a7c427 100644 --- a/aws-lc-rs-testing/Cargo.toml +++ b/aws-lc-rs-testing/Cargo.toml @@ -14,7 +14,7 @@ fips = ["aws-lc-rs/fips"] asan = ["aws-lc-rs/asan"] [dependencies] -aws-lc-rs = { version = "1.0", path = "../aws-lc-rs", features = ["ring-sig-verify"] } +aws-lc-rs = { version = "1.0", path = "../aws-lc-rs", features = ["ring-sig-verify", "unstable"] } untrusted = { version = "0.7" } [dev-dependencies] @@ -66,3 +66,7 @@ harness = false [[bench]] name = "cipher_benchmark" harness = false + +[[bench]] +name = "kem_benchmark" +harness = false diff --git a/aws-lc-rs-testing/benches/kem_benchmark.rs b/aws-lc-rs-testing/benches/kem_benchmark.rs new file mode 100644 index 00000000000..d3939acdd73 --- /dev/null +++ b/aws-lc-rs-testing/benches/kem_benchmark.rs @@ -0,0 +1,74 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +use aws_lc_rs::{ + kem::DecapsulationKey, + unstable::kem::{get_algorithm, AlgorithmId}, +}; +use criterion::{criterion_group, criterion_main, Criterion}; + +const UNSTABLE_ALGORITHMS: &[Option<&aws_lc_rs::kem::Algorithm>] = &[ + get_algorithm(AlgorithmId::Kyber512_R3), + get_algorithm(AlgorithmId::Kyber768_R3), + get_algorithm(AlgorithmId::Kyber1024_R3), +]; + +fn bench_kem_keygen(c: &mut Criterion) { + for ele in UNSTABLE_ALGORITHMS { + let ele = ele.unwrap(); + let bench_group_name = format!("KEM/{:?}/keygen", ele.id()); + let mut group = c.benchmark_group(bench_group_name); + group.bench_function("AWS-LC", |b| { + b.iter(|| { + aws_lc_rs::kem::DecapsulationKey::generate(ele).unwrap(); + }); + }); + } +} + +fn bench_kem_encapsulate(c: &mut Criterion) { + for ele in UNSTABLE_ALGORITHMS { + let ele = ele.unwrap(); + let bench_group_name = format!("KEM/{:?}/encapsulate", ele.id()); + let mut group = c.benchmark_group(bench_group_name); + group.bench_function("AWS-LC", |b| { + b.iter_batched( + || { + let private = DecapsulationKey::generate(ele).unwrap(); + private.encapsulation_key().unwrap() + }, + |key| key.encapsulate(), + criterion::BatchSize::LargeInput, + ); + }); + } +} + +fn bench_kem_decapsulate(c: &mut Criterion) { + for ele in UNSTABLE_ALGORITHMS { + let ele = ele.unwrap(); + let bench_group_name = format!("KEM/{:?}/decapsulate", ele.id()); + let mut group = c.benchmark_group(bench_group_name); + group.bench_function("AWS-LC", |b| { + b.iter_batched( + || { + let private = DecapsulationKey::generate(ele).unwrap(); + let public = private.encapsulation_key().unwrap(); + let (ciphertext, _) = public.encapsulate().unwrap(); + (private, ciphertext) + }, + |(key, ciphertext)| key.decapsulate(ciphertext).unwrap(), + criterion::BatchSize::LargeInput, + ); + }); + } +} + +fn bench_kem(c: &mut Criterion) { + bench_kem_keygen(c); + bench_kem_encapsulate(c); + bench_kem_decapsulate(c); +} + +criterion_group!(benches, bench_kem); +criterion_main!(benches); diff --git a/aws-lc-rs/Cargo.toml b/aws-lc-rs/Cargo.toml index 1d1cc61927c..77570c4e84d 100644 --- a/aws-lc-rs/Cargo.toml +++ b/aws-lc-rs/Cargo.toml @@ -33,6 +33,7 @@ ring-sig-verify = ["dep:untrusted"] bindgen = ["aws-lc-sys?/bindgen", "aws-lc-fips-sys?/bindgen"] asan = ["aws-lc-sys?/asan", "aws-lc-fips-sys?/asan"] test_logging = [] +unstable = [] # require non-FIPS non-fips = ["aws-lc-sys"] diff --git a/aws-lc-rs/Makefile b/aws-lc-rs/Makefile index e032b2e37a7..f6e3e3d153f 100644 --- a/aws-lc-rs/Makefile +++ b/aws-lc-rs/Makefile @@ -24,17 +24,17 @@ coverage: cargo llvm-cov --no-fail-fast --fail-under-lines 95 --ignore-filename-regex "aws-lc-sys/*" --lcov --output-path lcov.info test: - cargo test --all-targets --features ring-benchmarks + cargo test --all-targets --features unstable cargo test --release --all-targets - cargo test --release --all-targets --features bindgen + cargo test --release --all-targets --features bindgen,unstable ifeq ($(UNAME_S),Linux) - cargo test --release --all-targets --features fips - cargo test --no-default-features --features fips + cargo test --release --all-targets --features fips,unstable + cargo test --no-default-features --features fips,unstable endif - cargo test --no-default-features --features aws-lc-sys - cargo test --no-default-features --features aws-lc-sys,ring-sig-verify - cargo test --no-default-features --features aws-lc-sys,ring-io - cargo test --no-default-features --features aws-lc-sys,alloc + cargo test --no-default-features --features aws-lc-sys,unstable + cargo test --no-default-features --features aws-lc-sys,ring-sig-verify,unstable + cargo test --no-default-features --features aws-lc-sys,ring-io,unstable + cargo test --no-default-features --features aws-lc-sys,alloc,unstable msrv: cargo msrv verify diff --git a/aws-lc-rs/src/error.rs b/aws-lc-rs/src/error.rs index edf4457548a..8645e182e36 100644 --- a/aws-lc-rs/src/error.rs +++ b/aws-lc-rs/src/error.rs @@ -112,7 +112,7 @@ impl From for Unspecified { /// being used. /// /// * Unexpected errors: Report this as a bug. -#[derive(Copy, Clone, Debug)] +#[derive(Copy, Clone, Debug, PartialEq)] pub struct KeyRejected(&'static str); impl KeyRejected { diff --git a/aws-lc-rs/src/kem.rs b/aws-lc-rs/src/kem.rs new file mode 100644 index 00000000000..a3ab3f0f65e --- /dev/null +++ b/aws-lc-rs/src/kem.rs @@ -0,0 +1,490 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +//! Key-Encapsulation Mechanisms (KEMs), including support for Kyber Round 3 Submission. +//! +//! # Example +//! +//! Note that this example uses the Kyber-512 Round 3 algorithm, but other algorithms can be used +//! in the exact same way by substituting +//! `kem::` for `kem::KYBER512_R3`. +//! +//! ```ignore +//! use aws_lc_rs::{ +//! error::Unspecified, +//! kem::{Ciphertext, DecapsulationKey, EncapsulationKey}, +//! unstable::kem::{AlgorithmId, get_algorithm} +//! }; +//! +//! let kyber512_r3 = get_algorithm(AlgorithmId::Kyber512_R3).ok_or(Unspecified)?; +//! +//! // Alice generates their (private) decapsulation key. +//! let decapsulation_key = DecapsulationKey::generate(kyber512_r3)?; +//! +//! // Alices computes the (public) encapsulation key. +//! let encapsulation_key = decapsulation_key.encapsulation_key()?; +//! +//! let encapsulation_key_bytes = encapsulation_key.key_bytes()?; +//! +//! // Alice sends the encapsulation key bytes to bob through some +//! // protocol message. +//! let encapsulation_key_bytes = encapsulation_key_bytes.as_ref(); +//! +//! // Bob constructs the (public) encapsulation key from the key bytes provided by Alice. +//! let retrieved_encapsulation_key = EncapsulationKey::new(kyber512_r3, encapsulation_key_bytes)?; +//! +//! // Bob executes the encapsulation algorithm to to produce their copy of the secret, and associated ciphertext. +//! let (ciphertext, bob_secret) = retrieved_encapsulation_key.encapsulate()?; +//! +//! // Alice recieves ciphertext bytes from bob +//! let ciphertext_bytes = ciphertext.as_ref(); +//! +//! // Bob sends Alice the ciphertext computed from the encapsulation algorithm, Alice runs decapsulation to derive their +//! // copy of the secret. +//! let alice_secret = decapsulation_key.decapsulate(Ciphertext::from(ciphertext_bytes))?; +//! +//! // Alice and Bob have now arrived to the same secret +//! assert_eq!(alice_secret.as_ref(), bob_secret.as_ref()); +//! +//! # Ok::<(), aws_lc_rs::error::Unspecified>(()) +//! ``` +use crate::{ + buffer::Buffer, + error::{KeyRejected, Unspecified}, + ptr::LcPtr, + ptr::Pointer, +}; +use aws_lc::{ + EVP_PKEY_CTX_kem_set_params, EVP_PKEY_CTX_new, EVP_PKEY_CTX_new_id, EVP_PKEY_decapsulate, + EVP_PKEY_encapsulate, EVP_PKEY_get_raw_private_key, EVP_PKEY_get_raw_public_key, + EVP_PKEY_kem_new_raw_public_key, EVP_PKEY_keygen, EVP_PKEY_keygen_init, EVP_PKEY_up_ref, + EVP_PKEY, EVP_PKEY_KEM, +}; +use std::{borrow::Cow, cmp::Ordering, fmt::Debug, ptr::null_mut}; +use zeroize::Zeroize; + +/// An identifier for a KEM algorithm. +pub trait AlgorithmIdentifier: + Copy + Clone + Debug + PartialEq + crate::sealed::Sealed + 'static +{ + /// Returns the algorithm's associated AWS-LC nid. + fn nid(self) -> i32; +} + +/// A KEM algorithm +#[derive(PartialEq)] +pub struct Algorithm +where + Id: AlgorithmIdentifier, +{ + pub(crate) id: Id, + pub(crate) decapsulate_key_size: usize, + pub(crate) encapsulate_key_size: usize, + pub(crate) ciphertext_size: usize, + pub(crate) shared_secret_size: usize, +} + +impl Algorithm +where + Id: AlgorithmIdentifier, +{ + /// Returns the identifier for this algorithm. + #[must_use] + pub fn id(&self) -> Id { + self.id + } + + #[inline] + pub(crate) fn decapsulate_key_size(&self) -> usize { + self.decapsulate_key_size + } + + #[inline] + pub(crate) fn encapsulate_key_size(&self) -> usize { + self.encapsulate_key_size + } + + #[inline] + pub(crate) fn ciphertext_size(&self) -> usize { + self.ciphertext_size + } + + #[inline] + pub(crate) fn shared_secret_size(&self) -> usize { + self.shared_secret_size + } +} + +impl Debug for Algorithm +where + Id: AlgorithmIdentifier, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.id, f) + } +} + +/// A serializable decapulsation key usable with KEMs. This can be randomly generated with `DecapsulationKey::generate`. +pub struct DecapsulationKey +where + Id: AlgorithmIdentifier, +{ + algorithm: &'static Algorithm, + evp_pkey: LcPtr, +} + +/// Identifier for a KEM algorithm. +/// +/// See [`crate::unstable::kem::AlgorithmId`] and [`crate::unstable::kem::get_algorithm`] for +/// access to algorithms not subject to semantic versioning gurantees. +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AlgorithmId {} + +impl AlgorithmIdentifier for AlgorithmId { + fn nid(self) -> i32 { + unreachable!() + } +} + +impl crate::sealed::Sealed for AlgorithmId {} + +impl DecapsulationKey +where + Id: AlgorithmIdentifier, +{ + /// Generate a new KEM decapsulation key for the given algorithm. + /// + /// # Errors + /// `error::Unspecified` when operation fails due to internal error. + pub fn generate(alg: &'static Algorithm) -> Result { + let mut secret_key_size = alg.decapsulate_key_size(); + let mut priv_key_bytes = vec![0u8; secret_key_size]; + let kyber_key = kem_key_generate(alg.id.nid())?; + if 1 != unsafe { + EVP_PKEY_get_raw_private_key( + kyber_key.as_const_ptr(), + priv_key_bytes.as_mut_ptr(), + &mut secret_key_size, + ) + } { + return Err(Unspecified); + } + Ok(DecapsulationKey { + algorithm: alg, + evp_pkey: kyber_key, + }) + } + + /// Return the algorithm associated with the given KEM decapsulation key. + #[must_use] + pub fn algorithm(&self) -> &'static Algorithm { + self.algorithm + } + + /// Computes the KEM encapsulation key from the KEM decapsulation key. + /// + /// # Errors + /// `error::Unspecified` when operation fails due to internal error. + #[allow(clippy::missing_panics_doc)] + pub fn encapsulation_key(&self) -> Result, Unspecified> { + // This is pedantic this function always returns 1 + if 1 != unsafe { EVP_PKEY_up_ref(*self.evp_pkey) } { + return Err(Unspecified); + }; + + let evp_pkey = LcPtr::new(*self.evp_pkey).expect("AWS-LC EVP_PKEY should not be null"); + + Ok(EncapsulationKey { + algorithm: self.algorithm, + evp_pkey, + }) + } + + /// Performs the decapsulate operation using this KEM decapsulation key on the given ciphertext. + /// + /// `ciphertext` is the ciphertext generated by the encapsulate operation using the KEM encapsulation key + /// associated with this KEM decapsulation key. + /// + /// # Errors + /// `Unspecified` when operation fails due to internal error. + #[allow(clippy::needless_pass_by_value)] + pub fn decapsulate(&self, ciphertext: Ciphertext<'_>) -> Result { + let mut shared_secret_len = self.algorithm.shared_secret_size(); + let mut shared_secret: Vec = vec![0u8; shared_secret_len]; + + let ctx = LcPtr::new(unsafe { EVP_PKEY_CTX_new(*self.evp_pkey, null_mut()) })?; + + let ciphertext = ciphertext.as_ref(); + + if 1 != unsafe { + EVP_PKEY_decapsulate( + *ctx, + shared_secret.as_mut_ptr(), + &mut shared_secret_len, + // AWS-LC incorrectly has this as an unqualified `uint8_t *`, it should be qualified with const + ciphertext.as_ptr() as *mut u8, + ciphertext.len(), + ) + } { + return Err(Unspecified); + } + + // This is currently pedantic but done for safety in-case the shared_secret buffer + // size changes in the future. `EVP_PKEY_decapsulate` updates `shared_secret_len` with + // the length of the shared secret in the event the buffer provided was larger then the secret. + // This truncates the buffer to the proper length to match the shared secret written. + debug_assert_eq!(shared_secret_len, shared_secret.len()); + shared_secret.truncate(shared_secret_len); + + Ok(SharedSecret(shared_secret.into_boxed_slice())) + } +} + +unsafe impl Send for DecapsulationKey where Id: AlgorithmIdentifier {} + +unsafe impl Sync for DecapsulationKey where Id: AlgorithmIdentifier {} + +impl Debug for DecapsulationKey +where + Id: AlgorithmIdentifier, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DecapsulationKey") + .field("algorithm", &self.algorithm) + .finish_non_exhaustive() + } +} + +mod types { + pub struct EncapsulationKeyBytesType { + _priv: (), + } +} + +/// KEM Encapsulation Key Bytes. +pub type EncapsulationKeyBytes = Buffer<'static, types::EncapsulationKeyBytesType>; + +/// A serializable encapsulation key usable with KEM algorithms. Constructed +/// from either a `DecapsulationKey` or raw bytes. +pub struct EncapsulationKey +where + Id: AlgorithmIdentifier, +{ + algorithm: &'static Algorithm, + evp_pkey: LcPtr, +} + +impl EncapsulationKey +where + Id: AlgorithmIdentifier, +{ + /// Return the algorithm associated with the given KEM encapsulation key. + #[must_use] + pub fn algorithm(&self) -> &'static Algorithm { + self.algorithm + } + + /// Performs the encapsulate operation using this KEM encapsulation key, generating a ciphertext + /// and associated shared secret. + /// + /// # Errors + /// `error::Unspecified` when operation fails due to internal error. + pub fn encapsulate(&self) -> Result<(Ciphertext<'static>, SharedSecret), Unspecified> { + let mut ciphertext_len = self.algorithm.ciphertext_size(); + let mut shared_secret_len = self.algorithm.shared_secret_size(); + let mut ciphertext: Vec = vec![0u8; ciphertext_len]; + let mut shared_secret: Vec = vec![0u8; shared_secret_len]; + + let ctx = LcPtr::new(unsafe { EVP_PKEY_CTX_new(*self.evp_pkey, null_mut()) })?; + + if 1 != unsafe { + EVP_PKEY_encapsulate( + *ctx, + ciphertext.as_mut_ptr(), + &mut ciphertext_len, + shared_secret.as_mut_ptr(), + &mut shared_secret_len, + ) + } { + return Err(Unspecified); + } + + // The following two steps are currently pedantic but done for safety in-case the buffer allocation + // sizes change in the future. `EVP_PKEY_encapsulate` updates `ciphertext_len` and `shared_secret_len` with + // the length of the ciphertext and shared secret respectivly in the event the buffer provided for each was + // larger then the actual values. Thus these two steps truncate the buffers to the proper length to match the + // value lengths written. + debug_assert_eq!(ciphertext_len, ciphertext.len()); + ciphertext.truncate(ciphertext_len); + debug_assert_eq!(shared_secret_len, shared_secret.len()); + shared_secret.truncate(shared_secret_len); + + Ok(( + Ciphertext::new(ciphertext), + SharedSecret::new(shared_secret.into_boxed_slice()), + )) + } + + /// Returns the `EnscapsulationKey` bytes. + /// + /// # Errors + /// * `Unspecified`: Any failure to retrieve the `EnscapsulationKey` bytes. + pub fn key_bytes(&self) -> Result { + let mut encapsulate_key_size = self.algorithm.encapsulate_key_size(); + let mut encapsulate_bytes = vec![0u8; encapsulate_key_size]; + if 1 != unsafe { + EVP_PKEY_get_raw_public_key( + self.evp_pkey.as_const_ptr(), + encapsulate_bytes.as_mut_ptr(), + &mut encapsulate_key_size, + ) + } { + return Err(Unspecified); + } + + // This is currently pedantic but done for safety in-case the encapsulation key + // size changes in the future. `EVP_PKEY_get_raw_public_key` writes the total length + // to `encapsulate_key_size` in the event that the buffer we provide is larger then + // required. + debug_assert_eq!(encapsulate_key_size, encapsulate_bytes.len()); + encapsulate_bytes.truncate(encapsulate_key_size); + + Ok(Buffer::new(encapsulate_bytes)) + } + + /// Creates a new KEM encapsulation key from raw bytes. This method MUST NOT be used to generate + /// a new encapsulation key, rather it MUST be used to construct `EncapsulationKey` previously serialized + /// to raw bytes. + /// + /// `alg` is the [`Algorithm`] to be associated with the generated `EncapsulationKey`. + /// + /// `bytes` is a slice of raw bytes representing a `EncapsulationKey`. + /// + /// # Errors + /// `error::KeyRejected` when operation fails during key creation. + pub fn new(alg: &'static Algorithm, bytes: &[u8]) -> Result { + match bytes.len().cmp(&alg.encapsulate_key_size()) { + Ordering::Less => Err(KeyRejected::too_small()), + Ordering::Greater => Err(KeyRejected::too_large()), + Ordering::Equal => Ok(()), + }?; + let pubkey = LcPtr::new(unsafe { + EVP_PKEY_kem_new_raw_public_key(alg.id.nid(), bytes.as_ptr(), bytes.len()) + })?; + Ok(EncapsulationKey { + algorithm: alg, + evp_pkey: pubkey, + }) + } +} + +unsafe impl Send for EncapsulationKey where Id: AlgorithmIdentifier {} + +unsafe impl Sync for EncapsulationKey where Id: AlgorithmIdentifier {} + +impl Debug for EncapsulationKey +where + Id: AlgorithmIdentifier, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("EncapsulationKey") + .field("algorithm", &self.algorithm) + .finish_non_exhaustive() + } +} + +/// A set of encrypted bytes produced by [`EncapsulationKey::encapsulate`], +/// and used as an input to [`DecapsulationKey::decapsulate`]. +pub struct Ciphertext<'a>(Cow<'a, [u8]>); + +impl<'a> Ciphertext<'a> { + fn new(value: Vec) -> Ciphertext<'a> { + Self(Cow::Owned(value)) + } +} + +impl<'a> Drop for Ciphertext<'a> { + fn drop(&mut self) { + if let Cow::Owned(ref mut v) = self.0 { + v.zeroize(); + } + } +} + +impl<'a> AsRef<[u8]> for Ciphertext<'a> { + fn as_ref(&self) -> &[u8] { + match self.0 { + Cow::Borrowed(v) => v, + Cow::Owned(ref v) => v.as_ref(), + } + } +} + +impl<'a> From<&'a [u8]> for Ciphertext<'a> { + fn from(value: &'a [u8]) -> Self { + Self(Cow::Borrowed(value)) + } +} + +/// The cryptographic shared secret output from the KEM encapsulate / decapsulate process. +pub struct SharedSecret(Box<[u8]>); + +impl SharedSecret { + fn new(value: Box<[u8]>) -> Self { + Self(value) + } +} + +impl Drop for SharedSecret { + fn drop(&mut self) { + self.0.zeroize(); + } +} + +impl AsRef<[u8]> for SharedSecret { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +// Returns an LcPtr to an EVP_PKEY +#[inline] +fn kem_key_generate(nid: i32) -> Result, Unspecified> { + let ctx = LcPtr::new(unsafe { EVP_PKEY_CTX_new_id(EVP_PKEY_KEM, null_mut()) })?; + if 1 != unsafe { EVP_PKEY_CTX_kem_set_params(*ctx, nid) } + || 1 != unsafe { EVP_PKEY_keygen_init(*ctx) } + { + return Err(Unspecified); + } + + let mut key_raw: *mut EVP_PKEY = null_mut(); + if 1 != unsafe { EVP_PKEY_keygen(*ctx, &mut key_raw) } { + return Err(Unspecified); + } + Ok(LcPtr::new(key_raw)?) +} + +#[cfg(test)] +mod tests { + use super::{Ciphertext, SharedSecret}; + + #[test] + fn ciphertext() { + let ciphertext_bytes = vec![42u8; 4]; + let ciphertext = Ciphertext::from(ciphertext_bytes.as_ref()); + assert_eq!(ciphertext.as_ref(), &[42, 42, 42, 42]); + drop(ciphertext); + + let ciphertext_bytes = vec![42u8; 4]; + let ciphertext = Ciphertext::<'static>::new(ciphertext_bytes); + assert_eq!(ciphertext.as_ref(), &[42, 42, 42, 42]); + } + + #[test] + fn shared_secret() { + let secret_bytes = vec![42u8; 4]; + let shared_secret = SharedSecret::new(secret_bytes.into_boxed_slice()); + assert_eq!(shared_secret.as_ref(), &[42, 42, 42, 42]); + } +} diff --git a/aws-lc-rs/src/lib.rs b/aws-lc-rs/src/lib.rs index e4c80db3b47..907ad335214 100644 --- a/aws-lc-rs/src/lib.rs +++ b/aws-lc-rs/src/lib.rs @@ -138,9 +138,12 @@ mod evp_pkey; mod fips; mod hex; pub mod iv; +#[allow(clippy::module_name_repetitions)] +pub mod kem; mod ptr; pub mod rsa; pub mod tls_prf; +pub mod unstable; pub(crate) use debug::derive_debug_via_id; diff --git a/aws-lc-rs/src/unstable.rs b/aws-lc-rs/src/unstable.rs new file mode 100644 index 00000000000..46b6daedf97 --- /dev/null +++ b/aws-lc-rs/src/unstable.rs @@ -0,0 +1,12 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +#![cfg(feature = "unstable")] + +//! Unstable aws-lc-rs features. +//! +//! # ⚠️ Warning +//! Features contained within this module, or child modules are subject to changes, relocation, +//! or removal across minor releases, and thus are not subject to semantic versioning policies. + +pub mod kem; diff --git a/aws-lc-rs/src/unstable/kem.rs b/aws-lc-rs/src/unstable/kem.rs new file mode 100644 index 00000000000..97f02b6fe14 --- /dev/null +++ b/aws-lc-rs/src/unstable/kem.rs @@ -0,0 +1,263 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 OR ISC + +//! Unstable KEM Algorithms for usage with the [`crate::kem`] module. +//! +//! # ⚠️ Warning +//! Algorithms contained in this module are subject to changes, relocation, +//! or removal across minor releases, and thus are not subject to semantic versioning policies. +//! +//! # Example +//! +//! ``` +//! use aws_lc_rs::{ +//! error::Unspecified, +//! kem::{Ciphertext, DecapsulationKey, EncapsulationKey}, +//! unstable::kem::{AlgorithmId, get_algorithm} +//! }; +//! +//! let kyber512_r3 = get_algorithm(AlgorithmId::Kyber512_R3).ok_or(Unspecified)?; +//! +//! // Alice generates their (private) decapsulation key. +//! let decapsulation_key = DecapsulationKey::generate(kyber512_r3)?; +//! +//! // Alices computes the (public) encapsulation key. +//! let encapsulation_key = decapsulation_key.encapsulation_key()?; +//! +//! let encapsulation_key_bytes = encapsulation_key.key_bytes()?; +//! +//! // Alice sends the encapsulation key bytes to bob through some +//! // protocol message. +//! let encapsulation_key_bytes = encapsulation_key_bytes.as_ref(); +//! +//! // Bob constructs the (public) encapsulation key from the key bytes provided by Alice. +//! let retrieved_encapsulation_key = EncapsulationKey::new(kyber512_r3, encapsulation_key_bytes)?; +//! +//! // Bob executes the encapsulation algorithm to to produce their copy of the secret, and associated ciphertext. +//! let (ciphertext, bob_secret) = retrieved_encapsulation_key.encapsulate()?; +//! +//! // Alice recieves ciphertext bytes from bob +//! let ciphertext_bytes = ciphertext.as_ref(); +//! +//! // Bob sends Alice the ciphertext computed from the encapsulation algorithm, Alice runs decapsulation to derive their +//! // copy of the secret. +//! let alice_secret = decapsulation_key.decapsulate(Ciphertext::from(ciphertext_bytes))?; +//! +//! // Alice and Bob have now arrived to the same secret +//! assert_eq!(alice_secret.as_ref(), bob_secret.as_ref()); +//! +//! # Ok::<(), aws_lc_rs::error::Unspecified>(()) +//! ``` + +use core::fmt::Debug; + +use crate::kem::Algorithm; +use aws_lc::{NID_KYBER1024_R3, NID_KYBER512_R3, NID_KYBER768_R3}; + +// Key lengths defined as stated on the CRYSTALS website: +// https://pq-crystals.org/kyber/ + +const KYBER512_R3_SECRET_KEY_LENGTH: usize = 1632; +const KYBER512_R3_CIPHERTEXT_LENGTH: usize = 768; +const KYBER512_R3_PUBLIC_KEY_LENGTH: usize = 800; +const KYBER512_R3_SHARED_SECRET_LENGTH: usize = 32; + +const KYBER768_R3_SECRET_KEY_LENGTH: usize = 2400; +const KYBER768_R3_CIPHERTEXT_LENGTH: usize = 1088; +const KYBER768_R3_PUBLIC_KEY_LENGTH: usize = 1184; +const KYBER768_R3_SHARED_SECRET_LENGTH: usize = 32; + +const KYBER1024_R3_SECRET_KEY_LENGTH: usize = 3168; +const KYBER1024_R3_CIPHERTEXT_LENGTH: usize = 1568; +const KYBER1024_R3_PUBLIC_KEY_LENGTH: usize = 1568; +const KYBER1024_R3_SHARED_SECRET_LENGTH: usize = 32; + +/// NIST Round 3 submission of the Kyber-512 algorithm. +const KYBER512_R3: Algorithm = Algorithm { + id: AlgorithmId::Kyber512_R3, + decapsulate_key_size: KYBER512_R3_SECRET_KEY_LENGTH, + encapsulate_key_size: KYBER512_R3_PUBLIC_KEY_LENGTH, + ciphertext_size: KYBER512_R3_CIPHERTEXT_LENGTH, + shared_secret_size: KYBER512_R3_SHARED_SECRET_LENGTH, +}; + +/// NIST Round 3 submission of the Kyber-768 algorithm. +const KYBER768_R3: Algorithm = Algorithm { + id: AlgorithmId::Kyber768_R3, + decapsulate_key_size: KYBER768_R3_SECRET_KEY_LENGTH, + encapsulate_key_size: KYBER768_R3_PUBLIC_KEY_LENGTH, + ciphertext_size: KYBER768_R3_CIPHERTEXT_LENGTH, + shared_secret_size: KYBER768_R3_SHARED_SECRET_LENGTH, +}; + +/// NIST Round 3 submission of the Kyber-1024 algorithm. +const KYBER1024_R3: Algorithm = Algorithm { + id: AlgorithmId::Kyber1024_R3, + decapsulate_key_size: KYBER1024_R3_SECRET_KEY_LENGTH, + encapsulate_key_size: KYBER1024_R3_PUBLIC_KEY_LENGTH, + ciphertext_size: KYBER1024_R3_CIPHERTEXT_LENGTH, + shared_secret_size: KYBER1024_R3_SHARED_SECRET_LENGTH, +}; + +/// Identifier for an unstable KEM algorithm. +#[allow(non_camel_case_types)] +#[non_exhaustive] +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum AlgorithmId { + /// NIST Round 3 submission of the Kyber-512 algorithm. + Kyber512_R3, + + /// NIST Round 3 submission of the Kyber-768 algorithm. + Kyber768_R3, + + /// NIST Round 3 submission of the Kyber-1024 algorithm. + Kyber1024_R3, +} + +impl crate::kem::AlgorithmIdentifier for AlgorithmId { + #[inline] + fn nid(self) -> i32 { + match self { + AlgorithmId::Kyber512_R3 => NID_KYBER512_R3, + AlgorithmId::Kyber768_R3 => NID_KYBER768_R3, + AlgorithmId::Kyber1024_R3 => NID_KYBER1024_R3, + } + } +} + +impl crate::sealed::Sealed for AlgorithmId {} + +/// Retrieve an unstable KEM [`Algorithm`] using the [`AlgorithmId`] specified by `id`. +/// May return [`Option::None`] if support for the algorithm has been removed from the unstable module. +#[must_use] +pub const fn get_algorithm(id: AlgorithmId) -> Option<&'static Algorithm> { + match id { + AlgorithmId::Kyber512_R3 => Some(&KYBER512_R3), + AlgorithmId::Kyber768_R3 => Some(&KYBER768_R3), + AlgorithmId::Kyber1024_R3 => Some(&KYBER1024_R3), + } +} + +#[cfg(test)] +mod tests { + use crate::{ + error::KeyRejected, + kem::{DecapsulationKey, EncapsulationKey}, + }; + + use super::{get_algorithm, AlgorithmId, KYBER1024_R3, KYBER512_R3, KYBER768_R3}; + + #[test] + fn test_kem_serialize() { + for algorithm in [&KYBER512_R3, &KYBER768_R3, &KYBER1024_R3] { + let priv_key = DecapsulationKey::generate(algorithm).unwrap(); + assert_eq!(priv_key.algorithm(), algorithm); + + let pub_key = priv_key.encapsulation_key().unwrap(); + let pubkey_raw_bytes = pub_key.key_bytes().unwrap(); + let pub_key_from_bytes = + EncapsulationKey::new(algorithm, pubkey_raw_bytes.as_ref()).unwrap(); + + assert_eq!( + pub_key.key_bytes().unwrap().as_ref(), + pub_key_from_bytes.key_bytes().unwrap().as_ref() + ); + assert_eq!(pub_key.algorithm(), pub_key_from_bytes.algorithm()); + } + } + + #[test] + fn test_kem_wrong_sizes() { + for algorithm in [&KYBER512_R3, &KYBER768_R3, &KYBER1024_R3] { + let too_long_bytes = vec![0u8; algorithm.encapsulate_key_size() + 1]; + let long_pub_key_from_bytes = EncapsulationKey::new(algorithm, &too_long_bytes); + assert_eq!( + long_pub_key_from_bytes.err(), + Some(KeyRejected::too_large()) + ); + + let too_short_bytes = vec![0u8; algorithm.encapsulate_key_size() - 1]; + let short_pub_key_from_bytes = EncapsulationKey::new(algorithm, &too_short_bytes); + assert_eq!( + short_pub_key_from_bytes.err(), + Some(KeyRejected::too_small()) + ); + } + } + + #[test] + fn test_kem_e2e() { + for algorithm in [&KYBER512_R3, &KYBER768_R3, &KYBER1024_R3] { + let priv_key = DecapsulationKey::generate(algorithm).unwrap(); + assert_eq!(priv_key.algorithm(), algorithm); + + let pub_key = priv_key.encapsulation_key().unwrap(); + + let (alice_ciphertext, alice_secret) = + pub_key.encapsulate().expect("encapsulate successful"); + + let bob_secret = priv_key + .decapsulate(alice_ciphertext) + .expect("decapsulate successful"); + + assert_eq!(alice_secret.as_ref(), bob_secret.as_ref()); + } + } + + #[test] + fn test_serialized_kem_e2e() { + for algorithm in [&KYBER512_R3, &KYBER768_R3, &KYBER1024_R3] { + let priv_key = DecapsulationKey::generate(algorithm).unwrap(); + assert_eq!(priv_key.algorithm(), algorithm); + + let pub_key = priv_key.encapsulation_key().unwrap(); + + // Generate public key bytes to send to bob + let pub_key_bytes = pub_key.key_bytes().unwrap(); + + // Test that priv_key's EVP_PKEY isn't entirely freed since we remove this pub_key's reference. + drop(pub_key); + + let retrieved_pub_key = + EncapsulationKey::new(algorithm, pub_key_bytes.as_ref()).unwrap(); + let (ciphertext, bob_secret) = retrieved_pub_key + .encapsulate() + .expect("encapsulate successful"); + + let alice_secret = priv_key + .decapsulate(ciphertext) + .expect("encapsulate successful"); + + assert_eq!(alice_secret.as_ref(), bob_secret.as_ref()); + } + } + + #[test] + fn test_get_algorithm() { + for id in [ + AlgorithmId::Kyber512_R3, + AlgorithmId::Kyber768_R3, + AlgorithmId::Kyber1024_R3, + ] { + let alg = get_algorithm(id).expect("algorithm retrievable"); + assert_eq!(alg.id(), id); + } + } + + #[test] + fn test_debug_fmt() { + let alg = get_algorithm(AlgorithmId::Kyber512_R3).expect("algorithm retrievable"); + let private = DecapsulationKey::generate(alg).expect("successful generation"); + assert_eq!( + format!("{private:?}"), + "DecapsulationKey { algorithm: Kyber512_R3, .. }" + ); + assert_eq!( + format!( + "{:?}", + private.encapsulation_key().expect("public key retrievable") + ), + "EncapsulationKey { algorithm: Kyber512_R3, .. }" + ); + } +}