From d6df228e186bbb201d2ad2dc964c833a2f8361e9 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Wed, 1 Jul 2026 17:58:36 +0200 Subject: [PATCH 01/10] feat(crypto): add OpenSSL 1.1.x backends for azihsm_crypto azihsm_crypto is OpenSSL-3.x-only (EVP_MAC, OSSL_PARAM, library contexts, provider fetch), which blocks the OpenSSL 1.1.x engine that depends on it transitively via azihsm_api. Add a 1.1.x backend for each algorithm alongside the existing 3.x one, selected at build time: - build.rs emits cfg(ossl300) from the OpenSSL version openssl-sys exposes (DEP_OPENSSL_VERSION_NUMBER). - Each algorithm gains a *_ossl11.rs compiled under cfg(not(ossl300)); the existing *_ossl.rs stays unchanged as the cfg(ossl300) backend. - libctx (private OSSL_LIB_CTX isolation) is gated to 3.x; 1.1 has no library contexts or providers. Covers aes (cbc/ecb/gcm/xts), ecc/ecdh, hash, hmac, kdf/hkdf, and rsa (enc/sign/hash-sign). 3.x is unchanged. Signed-off-by: Christian Walter --- crates/crypto/build.rs | 21 + crates/crypto/src/aes/cbc_ossl11.rs | 688 ++++++++++++++++++ crates/crypto/src/aes/ecb_ossl11.rs | 209 ++++++ crates/crypto/src/aes/gcm_ossl11.rs | 581 +++++++++++++++ crates/crypto/src/aes/mod.rs | 16 + crates/crypto/src/aes/xts_ossl11.rs | 625 ++++++++++++++++ crates/crypto/src/ecc/ecc_ossl11.rs | 182 +++++ crates/crypto/src/ecc/ecdh_ossl11.rs | 132 ++++ crates/crypto/src/ecc/mod.rs | 8 + crates/crypto/src/hash/hash_ossl11.rs | 369 ++++++++++ crates/crypto/src/hash/mod.rs | 6 +- crates/crypto/src/hmac/hmac_ossl11.rs | 451 ++++++++++++ crates/crypto/src/hmac/mod.rs | 4 + crates/crypto/src/kdf/hkdf_ossl11.rs | 232 ++++++ crates/crypto/src/kdf/mod.rs | 4 + crates/crypto/src/lib.rs | 2 +- crates/crypto/src/rsa/mod.rs | 12 + crates/crypto/src/rsa/rsa_enc_ossl11.rs | 340 +++++++++ crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs | 426 +++++++++++ crates/crypto/src/rsa/rsa_sign_ossl11.rs | 302 ++++++++ 20 files changed, 4608 insertions(+), 2 deletions(-) create mode 100644 crates/crypto/build.rs create mode 100644 crates/crypto/src/aes/cbc_ossl11.rs create mode 100644 crates/crypto/src/aes/ecb_ossl11.rs create mode 100644 crates/crypto/src/aes/gcm_ossl11.rs create mode 100644 crates/crypto/src/aes/xts_ossl11.rs create mode 100644 crates/crypto/src/ecc/ecc_ossl11.rs create mode 100644 crates/crypto/src/ecc/ecdh_ossl11.rs create mode 100644 crates/crypto/src/hash/hash_ossl11.rs create mode 100644 crates/crypto/src/hmac/hmac_ossl11.rs create mode 100644 crates/crypto/src/kdf/hkdf_ossl11.rs create mode 100644 crates/crypto/src/rsa/rsa_enc_ossl11.rs create mode 100644 crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs create mode 100644 crates/crypto/src/rsa/rsa_sign_ossl11.rs diff --git a/crates/crypto/build.rs b/crates/crypto/build.rs new file mode 100644 index 000000000..a77edb74a --- /dev/null +++ b/crates/crypto/build.rs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Emits `cfg(ossl300)` when building against OpenSSL 3.0+ so the crate can +//! select 3.x-only backends or OpenSSL 1.1.x-compatible code. `openssl-sys` is +//! a direct dependency declaring `links = "openssl"`, so cargo exposes the +//! detected version here as `DEP_OPENSSL_VERSION_NUMBER` (hex `MNNFFPPS`). + +/// OpenSSL 3.0.0 as the packed `DEP_OPENSSL_VERSION_NUMBER` value. +const OPENSSL_3_0_0: u64 = 0x3000_0000; + +fn main() { + println!("cargo::rustc-check-cfg=cfg(ossl300)"); + println!("cargo::rerun-if-env-changed=DEP_OPENSSL_VERSION_NUMBER"); + if let Ok(v) = std::env::var("DEP_OPENSSL_VERSION_NUMBER") + && let Ok(n) = u64::from_str_radix(&v, 16) + && n >= OPENSSL_3_0_0 + { + println!("cargo::rustc-cfg=ossl300"); + } +} diff --git a/crates/crypto/src/aes/cbc_ossl11.rs b/crates/crypto/src/aes/cbc_ossl11.rs new file mode 100644 index 000000000..93690b444 --- /dev/null +++ b/crates/crypto/src/aes/cbc_ossl11.rs @@ -0,0 +1,688 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based AES-CBC encryption/decryption implementation. +//! +//! This module provides AES cipher block chaining (CBC) mode operations using OpenSSL +//! as the underlying cryptographic backend. It supports both single-operation and +//! streaming encryption/decryption with optional PKCS#7 padding. +//! +//! # CBC Mode +//! +//! Cipher Block Chaining (CBC) mode encrypts blocks sequentially, with each plaintext +//! block XORed with the previous ciphertext block before encryption. This creates +//! dependency between blocks, making CBC mode unsuitable for parallel processing but +//! providing better security than ECB mode. +//! +//! # Security Considerations +//! +//! - **IV Requirements**: The initialization vector (IV) must be unpredictable and unique +//! for each encryption operation with the same key +//! - **Padding Oracle Attacks**: Care must be taken when handling padding errors in decryption +//! - **Authentication**: CBC mode does not provide authentication; consider using AEAD modes +//! like AES-GCM for new applications +//! - **IV Reuse**: Never reuse the same key-IV pair for different plaintexts + +use openssl::symm::*; + +use super::*; + +/// OpenSSL AES-CBC encryption/decryption operation. +/// +/// This structure configures an AES-CBC operation with padding mode and initialization +/// vector. It implements both single-operation and streaming encryption/decryption. +/// +/// # Lifetime Parameters +/// +/// * `'a` - Lifetime of the initialization vector reference +/// +/// # Fields +/// +/// The structure maintains: +/// - Padding configuration (PKCS#7 padding enabled/disabled) +/// - Initialization vector for CBC mode +/// +/// # Thread Safety +/// +/// This structure is not `Send` or `Sync` due to the borrowed IV. For concurrent +/// operations, create separate instances with their own IVs. +pub struct OsslAesCbcAlgo { + /// Whether to use PKCS#7 padding for incomplete blocks + pad: bool, + + /// Initialization vector for CBC mode (must be 16 bytes for AES) + iv: Vec, +} + +impl OsslAesCbcAlgo { + /// Creates a new AES-CBC operation with the specified configuration. + /// + /// # Arguments + /// + /// * `pad` - Whether to enable PKCS#7 padding. When `true`, input data of any length + /// is accepted and automatically padded. When `false`, input must be a multiple of + /// the AES block size (16 bytes). + /// * `iv` - Initialization vector for CBC mode. Must be exactly 16 bytes. The IV should + /// be unpredictable and unique for each encryption operation. + /// + /// # Returns + /// + /// A new `OsslAesCbc` instance configured with the specified parameters. + /// + /// # Security + /// + /// The IV must be: + /// - Unpredictable (use a cryptographically secure RNG) + /// - Unique for each encryption with the same key + /// - Can be transmitted in plaintext with the ciphertext + pub fn with_padding(iv: &[u8]) -> Self { + Self { + pad: true, + iv: iv.to_vec(), + } + } + + /// Creates a new AES-CBC operation without PKCS#7 padding. + /// + /// This constructor disables padding, requiring input data to be a multiple of + /// the AES block size (16 bytes). This is useful for applications that implement + /// custom padding schemes or work with pre-padded data. + /// + /// # Arguments + /// + /// * `iv` - Initialization vector for CBC mode. Must be exactly 16 bytes. The IV should + /// be unpredictable and unique for each encryption operation. + /// + /// # Returns + /// + /// A new `OsslAesCbc` instance configured without padding. + /// + /// # Security + /// + /// The IV must be: + /// - Unpredictable (use a cryptographically secure RNG) + /// - Unique for each encryption with the same key + /// - Can be transmitted in plaintext with the ciphertext + pub fn with_no_padding(iv: &[u8]) -> Self { + Self { + pad: false, + iv: iv.to_vec(), + } + } + + /// Returns whether PKCS#7 padding is enabled. + /// + /// # Returns + /// + /// `true` if padding is enabled, `false` otherwise. + pub fn pad(&self) -> bool { + self.pad + } + + /// Returns a reference to the initialization vector. + /// + /// # Returns + /// + /// A byte slice containing the IV (16 bytes for AES). + pub fn iv(&self) -> &[u8] { + &self.iv + } + + /// Returns a mutable reference to the initialization vector. + /// + /// This is an internal method used to update the IV during encryption/decryption + /// operations for proper CBC chaining across multiple operations. + /// + /// # Returns + /// + /// A mutable byte slice containing the IV (16 bytes for AES). + fn iv_mut(&mut self) -> &mut [u8] { + &mut self.iv + } + + /// Returns the appropriate OpenSSL cipher based on key size. + /// + /// This internal method selects the correct AES-CBC cipher variant (128, 192, or 256-bit) + /// based on the provided key size. + /// + /// # Arguments + /// + /// * `key_size` - Size of the key in bytes (16, 24, or 32) + /// + /// # Returns + /// + /// The corresponding OpenSSL `Cipher` for AES-CBC with the specified key size. + /// + /// # Errors + /// + /// Returns `CryptoError::AesInvalidKeySize` if the key size is not 16, 24, or 32 bytes. + fn cipher(key_size: usize) -> Result { + match key_size { + 16 => Ok(Cipher::aes_128_cbc()), + 24 => Ok(Cipher::aes_192_cbc()), + 32 => Ok(Cipher::aes_256_cbc()), + _ => Err(CryptoError::AesInvalidKeySize), + } + } +} + +/// Implements single-operation encryption for AES-CBC. +impl EncryptOp for OsslAesCbcAlgo { + type Key = AesKey; + + /// Performs AES-CBC encryption in a single operation. + /// + /// This method processes the entire input data at once. For large data or streaming + /// scenarios, consider using `encrypt_init` to create a streaming context. + /// + /// # Arguments + /// + /// * `key` - The AES key (128, 192, or 256 bits) + /// * `input` - Input plaintext data to encrypt + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer size + /// if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - The input size is invalid (not a multiple of block size when padding is disabled) + /// - The IV size is incorrect (must be 16 bytes) + /// - The underlying OpenSSL operation fails + fn encrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + let mut count = 0; + let key_bytes = key.bytes(); + let cipher = Self::cipher(key_bytes.len())?; + if let Some(output) = output { + let pad = self.pad(); + let iv = self.iv_mut(); + let mut crypter = Crypter::new(cipher, Mode::Encrypt, key_bytes, Some(iv)) + .map_err(|_| CryptoError::AesError)?; + crypter.pad(pad); + count += crypter + .update(input, output) + .map_err(|_| CryptoError::AesEncryptError)?; + count += crypter + .finalize(&mut output[count..]) + .map_err(|_| CryptoError::AesEncryptError)?; + iv.copy_from_slice(&output[count - iv.len()..count]); + } else { + // The required output buffer size for OpenSSL's `update` is + // `input.len() + block_size` regardless of whether padding is enabled. + count = input.len() + cipher.block_size(); + } + Ok(count) + } +} + +/// Implements streaming encryption for AES-CBC. +impl<'a> EncryptStreamingOp<'a> for OsslAesCbcAlgo { + type Key = AesKey; + type Context = OsslAesCbcEncryptContext; + + /// Initializes a streaming AES-CBC encryption context. + /// + /// Creates a context for processing data in multiple chunks. This is useful for: + /// - Large files that don't fit in memory + /// - Streaming data from network or other sources + /// - Progressive encryption with intermediate buffering + /// + /// # Arguments + /// + /// * `key` - The AES key (128, 192, or 256 bits) + /// + /// # Returns + /// + /// A context implementing `EncryptStreamingOpContext` for streaming operations. + /// + /// # Errors + /// + /// Returns an error if: + /// - The key size is invalid + /// - The IV size is incorrect (must be 16 bytes) + /// - OpenSSL context initialization fails + fn encrypt_init(self, key: Self::Key) -> Result { + let key_bytes = key.bytes(); + let mut crypter = Crypter::new( + Self::cipher(key_bytes.len())?, + Mode::Encrypt, + key_bytes, + Some(&self.iv), + ) + .map_err(|_| CryptoError::AesError)?; + crypter.pad(self.pad); + + Ok(OsslAesCbcEncryptContext { + algo: self, + crypter, + block: AesBlock::default(), + }) + } +} + +/// Streaming context for AES-CBC encryption operations. +/// +/// This structure maintains the state for a multi-step AES-CBC encryption operation. +/// It is created by `OsslAesCbc::encrypt_init` and processes data incrementally +/// through `update` calls, with finalization via `finish`. +/// +/// # Lifecycle +/// +/// 1. Create context via `encrypt_init` +/// 2. Process data chunks with `update` (can be called multiple times) +/// 3. Finalize with `finish` to produce any remaining output and padding +/// +/// # Internal State +/// +/// The context maintains: +/// - OpenSSL cipher context with key and IV +/// - Buffered partial blocks (data smaller than 16 bytes) +/// - Padding configuration from the parent operation +/// +/// # Thread Safety +/// +/// This context is not thread-safe and should be used from a single thread. +pub struct OsslAesCbcEncryptContext { + algo: OsslAesCbcAlgo, + crypter: Crypter, + block: AesBlock, +} + +/// Implements streaming encryption operations for the AES-CBC encrypt context. +impl<'a> EncryptOpContext<'a> for OsslAesCbcEncryptContext { + type Algo = OsslAesCbcAlgo; + /// Processes a chunk of input data. + /// + /// This method can be called multiple times to process data incrementally. + /// For block ciphers like AES, data is processed in 16-byte blocks. Any + /// incomplete blocks are buffered internally and processed in subsequent + /// calls or during finalization. + /// + /// # Arguments + /// + /// * `input` - Input data chunk to process + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer + /// size if `output` is `None`. Note that the output size may be smaller than + /// the input size if insufficient data is available to form complete blocks. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - The context has already been finalized + /// - The underlying OpenSSL update operation fails + fn update( + &mut self, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + if let Some(output) = output { + let mut offset = 0; + self.block.update(input, |data| { + let count = self + .crypter + .update(data, &mut output[offset..]) + .map_err(|_| CryptoError::AesEncryptError)?; + offset += count; + Ok(count) + }) + } else { + self.block.update_len(input) + } + } + + /// Finalizes the encryption/decryption operation. + /// + /// This method completes the operation by: + /// - Processing any remaining buffered data + /// - Applying padding (encryption) or validating padding (decryption) + /// - Producing the final output block + /// + /// The context is consumed and cannot be used after this call. + /// + /// # Arguments + /// + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer (typically 0-16 bytes for + /// the final block), or the required buffer size if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - Padding validation fails during decryption (invalid padding) + /// - Input data size is not a multiple of block size (when padding is disabled) + /// - The underlying OpenSSL finalization fails + /// + /// # Security + /// + /// For decryption, this method validates PKCS#7 padding. Invalid padding may + /// indicate data corruption or tampering. Handle padding errors carefully to + /// avoid padding oracle vulnerabilities. + fn finish(&mut self, output: Option<&mut [u8]>) -> Result { + if let Some(output) = output { + self.block.r#final(|input| { + let mut count = self + .crypter + .update(input, output) + .map_err(|_| CryptoError::AesEncryptError)?; + count += self + .crypter + .finalize(&mut output[count..]) + .map_err(|_| CryptoError::AesEncryptError)?; + let iv_len = self.algo.iv.len(); + if count >= iv_len { + self.algo.iv.copy_from_slice(&output[count - iv_len..count]); + } + Ok(count) + }) + } else { + self.block.final_len() + } + } + + /// Returns a reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslHash` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslHash` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying hash algorithm. + /// + /// # Returns + /// + /// The `OsslHash` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} + +/// Implements single-operation decryption for AES-CBC. +impl DecryptOp for OsslAesCbcAlgo { + type Key = AesKey; + + /// Performs AES-CBC decryption in a single operation. + /// + /// This method processes the entire input data at once. For large data or streaming + /// scenarios, consider using `decrypt_init` to create a streaming context. + /// + /// # Arguments + /// + /// * `key` - The AES key (128, 192, or 256 bits) + /// * `input` - Input ciphertext data to decrypt + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer size + /// if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - The input size is invalid (not a multiple of block size when padding is disabled) + /// - The IV size is incorrect (must be 16 bytes) + /// - Padding validation fails + /// - The underlying OpenSSL operation fails + fn decrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + let mut count = 0; + let key_bytes = key.bytes(); + let cipher = Self::cipher(key_bytes.len())?; + if let Some(output) = output { + let pad = self.pad(); + let iv = self.iv_mut(); + let mut crypter = Crypter::new(cipher, Mode::Decrypt, key_bytes, Some(iv)) + .map_err(|_| CryptoError::AesError)?; + crypter.pad(pad); + count += crypter + .update(input, output) + .map_err(|_| CryptoError::AesDecryptError)?; + count += crypter + .finalize(&mut output[count..]) + .map_err(|_| CryptoError::AesDecryptError)?; + iv.copy_from_slice(&input[input.len() - iv.len()..]); + } else { + count = input.len() + cipher.block_size(); + } + Ok(count) + } +} + +/// Implements streaming decryption for AES-CBC. +impl<'a> DecryptStreamingOp<'a> for OsslAesCbcAlgo { + type Key = AesKey; + type Context = OsslAesCbcDecryptContext; + + /// Initializes a streaming AES-CBC decryption context. + /// + /// Creates a context for processing data in multiple chunks. This is useful for: + /// - Large files that don't fit in memory + /// - Streaming data from network or other sources + /// - Progressive decryption with intermediate buffering + /// + /// # Arguments + /// + /// * `key` - The AES key (128, 192, or 256 bits) + /// + /// # Returns + /// + /// A context implementing `DecryptStreamingOpContext` for streaming operations. + /// + /// # Errors + /// + /// Returns an error if: + /// - The key size is invalid + /// - The IV size is incorrect (must be 16 bytes) + /// - OpenSSL context initialization fails + fn decrypt_init(self, key: Self::Key) -> Result { + let key_bytes = key.bytes(); + let mut crypter = Crypter::new( + Self::cipher(key_bytes.len())?, + Mode::Decrypt, + key_bytes, + Some(&self.iv), + ) + .map_err(|_| CryptoError::AesError)?; + crypter.pad(self.pad); + + Ok(OsslAesCbcDecryptContext { + algo: self, + crypter, + block: AesBlock::default(), + }) + } +} + +/// Streaming context for AES-CBC decryption operations. +/// +/// This structure maintains the state for a multi-step AES-CBC decryption operation. +/// It is created by `OsslAesCbc::decrypt_init` and processes data incrementally +/// through `update` calls, with finalization via `finish`. +/// +/// # Lifecycle +/// +/// 1. Create context via `decrypt_init` +/// 2. Process data chunks with `update` (can be called multiple times) +/// 3. Finalize with `finish` to validate padding and produce final output +/// +/// # Internal State +/// +/// The context maintains: +/// - OpenSSL cipher context with key and IV +/// - Buffered partial blocks (data smaller than 16 bytes) +/// - Padding configuration from the parent operation +/// +/// # Thread Safety +/// +/// This context is not thread-safe and should be used from a single thread. +pub struct OsslAesCbcDecryptContext { + algo: OsslAesCbcAlgo, + crypter: Crypter, + block: AesBlock, +} + +/// Implements streaming decryption operations for the AES-CBC decrypt context. +impl<'a> DecryptOpContext<'a> for OsslAesCbcDecryptContext { + /// Algo associated with this context. + type Algo = OsslAesCbcAlgo; + + /// Processes a chunk of input data. + /// + /// This method can be called multiple times to process data incrementally. + /// For block ciphers like AES, data is processed in 16-byte blocks. Any + /// incomplete blocks are buffered internally and processed in subsequent + /// calls or during finalization. + /// + /// # Arguments + /// + /// * `input` - Input ciphertext chunk to process + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer + /// size if `output` is `None`. Note that the output size may be smaller than + /// the input size if insufficient data is available to form complete blocks. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - The context has already been finalized + /// - The underlying OpenSSL update operation fails + fn update( + &mut self, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + if let Some(output) = output { + let mut offset = 0; + self.block.update(input, |data| { + let count = self + .crypter + .update(data, &mut output[offset..]) + .map_err(|_| CryptoError::AesDecryptError)?; + offset += count; + Ok(count) + }) + } else { + self.block.update_len(input) + } + } + + /// Finalizes the decryption operation. + /// + /// This method completes the operation by: + /// - Processing any remaining buffered data + /// - Validating PKCS#7 padding + /// - Producing the final output block + /// + /// The context is consumed and cannot be used after this call. + /// + /// # Arguments + /// + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer (typically 0-16 bytes for + /// the final block), or the required buffer size if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - Padding validation fails during decryption (invalid padding) + /// - Input data size is not a multiple of block size (when padding is disabled) + /// - The underlying OpenSSL finalization fails + /// + /// # Security + /// + /// For decryption, this method validates PKCS#7 padding. Invalid padding may + /// indicate data corruption or tampering. Handle padding errors carefully to + /// avoid padding oracle vulnerabilities. + fn finish(&mut self, output: Option<&mut [u8]>) -> Result { + if let Some(output) = output { + self.block.r#final(|input| { + let mut count = self + .crypter + .update(input, output) + .map_err(|_| CryptoError::AesDecryptError)?; + count += self + .crypter + .finalize(&mut output[count..]) + .map_err(|_| CryptoError::AesDecryptError)?; + // Note: IV is updated in the context but not propagated back to the caller + // For proper IV chaining in streaming mode, the IV should be the last block of ciphertext + let iv_len = self.algo.iv.len(); + if input.len() >= iv_len { + self.algo.iv.copy_from_slice(&input[input.len() - iv_len..]); + } + Ok(count) + }) + } else { + self.block.final_len() + } + } + + /// Returns a reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslHash` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslHash` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying hash algorithm. + /// + /// # Returns + /// + /// The `OsslHash` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} diff --git a/crates/crypto/src/aes/ecb_ossl11.rs b/crates/crypto/src/aes/ecb_ossl11.rs new file mode 100644 index 000000000..9d49e2bc8 --- /dev/null +++ b/crates/crypto/src/aes/ecb_ossl11.rs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based AES-ECB encryption/decryption implementation. +//! +//! This module provides AES-ECB (Electronic Codebook) mode encryption using OpenSSL. +//! ECB is a block cipher mode that encrypts each block of plaintext independently. +//! +//! # Security Warning +//! +//! ECB mode is NOT recommended for most cryptographic applications because identical +//! plaintext blocks produce identical ciphertext blocks, which can leak information +//! about the plaintext structure. Consider using CBC, GCM, or other authenticated +//! encryption modes instead. +//! +//! # Block Size Requirement +//! +//! Input data must be a multiple of the AES block size (16 bytes). No padding is applied. + +use openssl::cipher::*; +use openssl::cipher_ctx::*; + +use super::*; + +/// OpenSSL AES-ECB encryption/decryption operation. +/// +/// This structure provides AES-ECB mode encryption and decryption using OpenSSL. +/// It supports AES with 128, 192, and 256-bit keys. +/// +/// # Security Considerations +/// +/// ECB mode encrypts each block independently, which means: +/// - Identical plaintext blocks produce identical ciphertext blocks +/// - Patterns in the plaintext are preserved in the ciphertext +/// - No protection against block reordering or replay attacks +/// +/// Use ECB mode only when: +/// - Processing random data with no patterns +/// - Encrypting single blocks +/// - Implementing higher-level protocols that handle security concerns +/// +/// # Thread Safety +/// +/// This structure is `Send` and `Sync`. + +#[derive(Default)] +pub struct OsslAesEcbAlgo; + +impl OsslAesEcbAlgo { + /// Returns the appropriate OpenSSL cipher based on key size. + /// + /// This internal method maps key sizes to their corresponding AES-ECB cipher variants. + /// + /// # Arguments + /// + /// * `key_size` - Size of the key in bytes (16 for AES-128, 24 for AES-192, 32 for AES-256) + /// + /// # Returns + /// + /// The corresponding OpenSSL `Cipher` for AES-ECB with the specified key size. + /// + /// # Errors + /// + /// Returns `CryptoError::AesInvalidKeySize` if the key size is not 16, 24, or 32 bytes. + fn cipher(key_size: usize) -> Result<&'static CipherRef, CryptoError> { + match key_size { + 16 => Ok(Cipher::aes_128_ecb()), + 24 => Ok(Cipher::aes_192_ecb()), + 32 => Ok(Cipher::aes_256_ecb()), + _ => Err(CryptoError::AesInvalidKeySize), + } + } +} + +impl EncryptOp for OsslAesEcbAlgo { + type Key = AesKey; + + /// Performs AES-ECB encryption in a single operation. + /// + /// Encrypts the input data using AES-ECB mode. The input must be a multiple of + /// the AES block size (16 bytes). No padding is applied. + /// + /// # Arguments + /// + /// * `key` - The AES key (16, 24, or 32 bytes for AES-128/192/256) + /// * `input` - Input plaintext data to encrypt (must be multiple of 16 bytes) + /// * `output` - Optional output buffer for ciphertext. If `None`, returns required size. + /// + /// # Returns + /// + /// * `Ok(usize)` - Number of bytes written to output, or required buffer size if output is `None` + /// * `Err(CryptoError)` - If encryption fails + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::AesInvalidKeySize` - Key size is not 16, 24, or 32 bytes + /// - `CryptoError::AesInvalidInputSize` - Input length is not a multiple of 16 bytes + /// - `CryptoError::AesBufferTooSmall` - Output buffer is too small + /// - `CryptoError::AesError` - Failed to initialize OpenSSL crypter + /// - `CryptoError::AesEncryptError` - Encryption operation failed + #[allow(unsafe_code)] + fn encrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + let key_bytes = key.bytes(); + let cipher = Self::cipher(key_bytes.len())?; + + if !input.len().is_multiple_of(cipher.block_size()) { + return Err(CryptoError::AesInvalidInputSize); + } + + let len = input.len() + cipher.block_size(); + let count = if let Some(output) = output { + // Validate output buffer size + if output.len() < len { + return Err(CryptoError::AesBufferTooSmall); + } + let mut ctx = CipherCtx::new().map_err(|_| CryptoError::AesEncryptError)?; + ctx.set_padding(false); + ctx.encrypt_init(Some(cipher), Some(key_bytes), None) + .map_err(|_| CryptoError::AesEncryptError)?; + let mut count = ctx + .cipher_update(input, Some(output)) + .map_err(|_| CryptoError::AesEncryptError)?; + count += ctx + .cipher_final(&mut output[count..]) + .map_err(|_| CryptoError::AesEncryptError)?; + debug_assert!(count == input.len()); + count + } else { + len + }; + + Ok(count) + } +} + +impl DecryptOp for OsslAesEcbAlgo { + type Key = AesKey; + + /// Performs AES-ECB decryption in a single operation. + /// + /// Decrypts the input ciphertext using AES-ECB mode. The input must be a multiple of + /// the AES block size (16 bytes). No padding is applied. + /// + /// # Arguments + /// + /// * `key` - The AES key (16, 24, or 32 bytes for AES-128/192/256) + /// * `input` - Input ciphertext data to decrypt (must be multiple of 16 bytes) + /// * `output` - Optional output buffer for plaintext. If `None`, returns required size. + /// + /// # Returns + /// + /// * `Ok(usize)` - Number of bytes written to output, or required buffer size if output is `None` + /// * `Err(CryptoError)` - If decryption fails + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::AesInvalidKeySize` - Key size is not 16, 24, or 32 bytes + /// - `CryptoError::AesInvalidInputSize` - Input length is not a multiple of 16 bytes + /// - `CryptoError::AesBufferTooSmall` - Output buffer is too small + /// - `CryptoError::AesError` - Failed to initialize OpenSSL crypter + /// - `CryptoError::AesDecryptError` - Decryption operation failed + #[allow(unsafe_code)] + fn decrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + let key_bytes = key.bytes(); + let cipher = Self::cipher(key_bytes.len())?; + + if !input.len().is_multiple_of(cipher.block_size()) { + return Err(CryptoError::AesInvalidInputSize); + } + + let len = input.len() + cipher.block_size(); + + let count = if let Some(output) = output { + // Validate output buffer size + if output.len() < len { + return Err(CryptoError::AesBufferTooSmall); + } + + let mut ctx = CipherCtx::new().map_err(|_| CryptoError::AesDecryptError)?; + ctx.decrypt_init(Some(cipher), Some(key_bytes), None) + .map_err(|_| CryptoError::AesDecryptError)?; + ctx.set_padding(false); + let mut count = ctx + .cipher_update(input, Some(output)) + .map_err(|_| CryptoError::AesDecryptError)?; + count += ctx + .cipher_final(&mut output[count..]) + .map_err(|_| CryptoError::AesDecryptError)?; + debug_assert!(count == input.len()); + count + } else { + len + }; + + Ok(count) + } +} diff --git a/crates/crypto/src/aes/gcm_ossl11.rs b/crates/crypto/src/aes/gcm_ossl11.rs new file mode 100644 index 000000000..21342d162 --- /dev/null +++ b/crates/crypto/src/aes/gcm_ossl11.rs @@ -0,0 +1,581 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use openssl::cipher::*; +use openssl::cipher_ctx::*; + +use super::*; + +/// AES GCM (Galois/Counter Mode) algorithm implementation for OpenSSL backend. +pub struct OsslAesGcmAlgo { + aad: Option>, + iv: Vec, + tag: Vec, +} + +impl OsslAesGcmAlgo { + const IV_SIZE: usize = 12; + const TAG_SIZE: usize = 16; + + /// Creates a new AES-GCM algorithm instance for encryption. + /// + /// # Arguments + /// + /// * `iv` - Initialization vector (IV) for encryption. + /// * `aad` - Optional additional authenticated data (AAD). + /// + /// # Returns + /// + /// Ok(Self) if the IV length is valid, otherwise an error. + pub fn for_encrypt(iv: &[u8], aad: Option<&[u8]>) -> Result { + if iv.len() != Self::IV_SIZE { + return Err(CryptoError::GcmInvalidIvLength); + } + let iv = iv.to_vec(); + let tag = vec![0u8; Self::TAG_SIZE]; + let aad = aad.map(|a| a.to_vec()); + Ok(Self { iv, tag, aad }) + } + + /// Creates a new AES-GCM algorithm instance for decryption. + /// + /// # Arguments + /// + /// * `iv` - Initialization vector (IV) for decryption. + /// * `aad` - Optional additional authenticated data (AAD). + /// * `tag` - Authentication tag for decryption. + /// + /// # Returns + /// + /// Ok(Self) if the IV and tag lengths are valid, otherwise an error. + pub fn for_decrypt(iv: &[u8], tag: &[u8], aad: Option<&[u8]>) -> Result { + if iv.len() != Self::IV_SIZE { + return Err(CryptoError::GcmInvalidIvLength); + } + if tag.len() != Self::TAG_SIZE { + return Err(CryptoError::GcmInvalidTagLength); + } + let iv = iv.to_vec(); + let tag = tag.to_vec(); + let aad = aad.map(|a| a.to_vec()); + Ok(Self { iv, tag, aad }) + } + + /// Returns the IV used in the algorithm. + pub fn iv(&self) -> &[u8] { + &self.iv + } + + /// Returns the authentication tag used in the algorithm. + pub fn tag(&self) -> &[u8] { + &self.tag + } + + fn cipher(&self, key: &AesKey) -> Result<&'static CipherRef, CryptoError> { + match key.size() { + 16 => Ok(Cipher::aes_128_gcm()), + 24 => Ok(Cipher::aes_192_gcm()), + 32 => Ok(Cipher::aes_256_gcm()), + _ => Err(CryptoError::GcmInvalidKeySize), + } + } +} + +impl EncryptOp for AesGcmAlgo { + /// The key type for AES-GCM encryption. + type Key = AesKey; + + /// Encrypts the input data using AES-GCM. + /// + /// # Arguments + /// + /// * `key` - The AES key to use for encryption. + /// * `input` - The plaintext data to encrypt. + /// * `output` - Optional buffer to write the ciphertext to. + /// + /// # Returns + /// + /// Ok(usize) indicating the number of bytes written to output, or an error. + fn encrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + let expected_len = input.len(); + + let Some(output) = output else { + return Ok(expected_len); + }; + + if output.len() < expected_len { + return Err(CryptoError::GcmBufferTooSmall); + } + + let cipher = self.cipher(key)?; + + let mut ctx = CipherCtx::new().map_err(|_| CryptoError::GcmEncryptionFailed)?; + ctx.encrypt_init(Some(cipher), Some(key.bytes()), Some(&self.iv)) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + if let Some(aad) = &self.aad { + ctx.cipher_update(aad, None) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + } + + let count = ctx + .cipher_update(input, Some(&mut output[..expected_len])) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + + let mut final_block = vec![0u8; cipher.block_size()]; + ctx.cipher_final(&mut final_block) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + + ctx.tag(&mut self.tag) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + + Ok(count) + } +} + +/// Implements streaming encryption for AES-GCM. +impl<'a> EncryptStreamingOp<'a> for OsslAesGcmAlgo { + type Key = AesKey; + type Context = OsslAesGcmEncryptContext; + + /// Initializes a streaming AES-GCM encryption context. + /// + /// Creates a context for processing data in multiple chunks. This is useful for: + /// - Large files that don't fit in memory + /// - Streaming data from network or other sources + /// - Progressive encryption with authentication + /// + /// # Arguments + /// + /// * `key` - The AES key (128, 192, or 256 bits) + /// + /// # Returns + /// + /// A context implementing `EncryptOpContext` for streaming operations. + /// + /// # Errors + /// + /// Returns an error if: + /// - The key size is invalid + /// - The IV size is incorrect (must be 12 bytes) + /// - OpenSSL context initialization fails + fn encrypt_init(self, key: Self::Key) -> Result { + let cipher = self.cipher(&key)?; + let mut ctx = CipherCtx::new().map_err(|_| CryptoError::GcmEncryptionFailed)?; + ctx.encrypt_init(Some(cipher), Some(key.bytes()), Some(&self.iv)) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + + // Process AAD if provided + if let Some(aad) = &self.aad { + ctx.cipher_update(aad, None) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + } + + Ok(OsslAesGcmEncryptContext { algo: self, ctx }) + } +} + +/// Streaming context for AES-GCM encryption operations. +/// +/// This structure maintains the state for a multi-step AES-GCM encryption operation. +/// It is created by `OsslAesGcmAlgo::encrypt_init` and processes data incrementally +/// through `update` calls, with finalization via `finish`. +/// +/// # Lifecycle +/// +/// 1. Create context via `encrypt_init` +/// 2. Process data chunks with `update` (can be called multiple times) +/// 3. Finalize with `finish` to produce the authentication tag +/// +/// # Internal State +/// +/// The context maintains: +/// - OpenSSL cipher context with key, IV, and AAD +/// - Authentication tag state +/// +/// # Thread Safety +/// +/// This context is not thread-safe and should be used from a single thread. +pub struct OsslAesGcmEncryptContext { + algo: OsslAesGcmAlgo, + ctx: CipherCtx, +} + +/// Implements streaming encryption operations for the AES-GCM encrypt context. +impl<'a> EncryptOpContext<'a> for OsslAesGcmEncryptContext { + type Algo = OsslAesGcmAlgo; + + /// Processes a chunk of input data. + /// + /// This method can be called multiple times to process data incrementally. + /// Unlike block cipher modes, GCM is a stream cipher mode and processes + /// data without requiring complete blocks. + /// + /// # Arguments + /// + /// * `input` - Input data chunk to encrypt + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer + /// size if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - The context has already been finalized + /// - The underlying OpenSSL update operation fails + fn update(&mut self, input: &[u8], output: Option<&mut [u8]>) -> Result { + let expected_len = input.len(); + + let Some(output) = output else { + return Ok(expected_len); + }; + + if output.len() < expected_len { + return Err(CryptoError::GcmBufferTooSmall); + } + + let count = self + .ctx + .cipher_update(input, Some(output)) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + Ok(count) + } + + /// Finalizes the encryption operation. + /// + /// This method completes the operation by: + /// - Processing any remaining buffered data + /// - Computing the authentication tag + /// - Storing the tag in the algorithm instance for later retrieval + /// + /// The context is consumed and cannot be used after this call. + /// + /// # Arguments + /// + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer (typically 0 for GCM), + /// or the required buffer size if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - The underlying OpenSSL finalization fails + /// - Tag extraction fails + /// + /// # Note + /// + /// After calling this method, the authentication tag can be retrieved via + /// `algo().tag()`. + fn finish(&mut self, output: Option<&mut [u8]>) -> Result { + // Finalize the encryption + let Some(output) = output else { + return Ok(0); + }; + + let count = self + .ctx + .cipher_final(output) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + + // Extract the authentication tag + self.ctx + .tag(&mut self.algo.tag) + .map_err(|_| CryptoError::GcmEncryptionFailed)?; + + Ok(count) + } + + /// Returns a reference to the underlying algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslAesGcmAlgo` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslAesGcmAlgo` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying algorithm. + /// + /// # Returns + /// + /// The `OsslAesGcmAlgo` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} + +impl DecryptOp for AesGcmAlgo { + /// The key type for AES-GCM decryption. + type Key = AesKey; + + /// Decrypts the input data using AES-GCM. + /// + /// # Arguments + /// + /// * `key` - The AES key to use for decryption. + /// * `input` - The ciphertext data to decrypt. + /// * `output` - Optional buffer to write the plaintext to. + /// + /// # Returns + /// + /// Ok(usize) indicating the number of bytes written to output, or an error. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - Authentication tag verification fails + /// - The underlying OpenSSL operation fails + fn decrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + let expected_len = input.len(); + + let Some(output) = output else { + return Ok(expected_len); + }; + + if output.len() < expected_len { + return Err(CryptoError::GcmBufferTooSmall); + } + + let cipher = self.cipher(key)?; + + let mut ctx = CipherCtx::new().map_err(|_| CryptoError::GcmDecryptionFailed)?; + ctx.decrypt_init(Some(cipher), Some(key.bytes()), Some(&self.iv)) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + + // Set the authentication tag for verification (must be done before AAD) + ctx.set_tag(&self.tag) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + + // Process AAD if provided + if let Some(aad) = &self.aad { + ctx.cipher_update(aad, None) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + } + + let count = ctx + .cipher_update(input, Some(&mut output[..expected_len])) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + + // Finalize will verify the tag + let mut final_block = vec![0u8; cipher.block_size()]; + ctx.cipher_final(&mut final_block) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + + Ok(count) + } +} + +/// Implements streaming decryption for AES-GCM. +impl<'a> DecryptStreamingOp<'a> for OsslAesGcmAlgo { + type Key = AesKey; + type Context = OsslAesGcmDecryptContext; + + /// Initializes a streaming AES-GCM decryption context. + /// + /// Creates a context for processing data in multiple chunks. This is useful for: + /// - Large files that don't fit in memory + /// - Streaming data from network or other sources + /// - Progressive decryption with authentication + /// + /// # Arguments + /// + /// * `key` - The AES key (128, 192, or 256 bits) + /// + /// # Returns + /// + /// A context implementing `DecryptOpContext` for streaming operations. + /// + /// # Errors + /// + /// Returns an error if: + /// - The key size is invalid + /// - The IV size is incorrect (must be 12 bytes) + /// - The tag size is incorrect (must be 16 bytes) + /// - OpenSSL context initialization fails + fn decrypt_init(self, key: Self::Key) -> Result { + let cipher = self.cipher(&key)?; + let mut ctx = CipherCtx::new().map_err(|_| CryptoError::GcmDecryptionFailed)?; + ctx.decrypt_init(Some(cipher), Some(key.bytes()), Some(&self.iv)) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + + // Set the authentication tag for verification (must be done before AAD) + ctx.set_tag(&self.tag) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + + // Process AAD if provided + if let Some(aad) = &self.aad { + ctx.cipher_update(aad, None) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + } + + Ok(OsslAesGcmDecryptContext { algo: self, ctx }) + } +} + +/// Streaming context for AES-GCM decryption operations. +/// +/// This structure maintains the state for a multi-step AES-GCM decryption operation. +/// It is created by `OsslAesGcmAlgo::decrypt_init` and processes data incrementally +/// through `update` calls, with finalization via `finish`. +/// +/// # Lifecycle +/// +/// 1. Create context via `decrypt_init` +/// 2. Process data chunks with `update` (can be called multiple times) +/// 3. Finalize with `finish` to verify the authentication tag +/// +/// # Internal State +/// +/// The context maintains: +/// - OpenSSL cipher context with key, IV, AAD, and tag +/// - Authentication tag for verification during finalization +/// +/// # Thread Safety +/// +/// This context is not thread-safe and should be used from a single thread. +pub struct OsslAesGcmDecryptContext { + algo: OsslAesGcmAlgo, + ctx: CipherCtx, +} + +/// Implements streaming decryption operations for the AES-GCM decrypt context. +impl<'a> DecryptOpContext<'a> for OsslAesGcmDecryptContext { + type Algo = OsslAesGcmAlgo; + + /// Processes a chunk of input data. + /// + /// This method can be called multiple times to process data incrementally. + /// Unlike block cipher modes, GCM is a stream cipher mode and processes + /// data without requiring complete blocks. + /// + /// # Arguments + /// + /// * `input` - Input data chunk to decrypt + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer + /// size if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - The context has already been finalized + /// - The underlying OpenSSL update operation fails + fn update(&mut self, input: &[u8], output: Option<&mut [u8]>) -> Result { + let expected_len = input.len(); + + let Some(output) = output else { + return Ok(expected_len); + }; + + if output.len() < expected_len { + return Err(CryptoError::GcmBufferTooSmall); + } + + let count = self + .ctx + .cipher_update(input, Some(output)) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + Ok(count) + } + + /// Finalizes the decryption operation. + /// + /// This method completes the operation by: + /// - Processing any remaining buffered data + /// - Verifying the authentication tag + /// - Producing final output + /// + /// The context is consumed and cannot be used after this call. + /// + /// # Arguments + /// + /// * `output` - Optional output buffer. If `None`, returns required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer (typically 0 for GCM), + /// or the required buffer size if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The output buffer is too small + /// - Authentication tag verification fails + /// - The underlying OpenSSL finalization fails + /// + /// # Security + /// + /// The authentication tag is verified during finalization. If verification + /// fails, the entire decryption is considered invalid and an error is returned. + fn finish(&mut self, output: Option<&mut [u8]>) -> Result { + let Some(output) = output else { + return Ok(0); + }; + + // Finalize the decryption and verify the tag + let count = self + .ctx + .cipher_final(output) + .map_err(|_| CryptoError::GcmDecryptionFailed)?; + + Ok(count) + } + + /// Returns a reference to the underlying algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslAesGcmAlgo` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslAesGcmAlgo` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying algorithm. + /// + /// # Returns + /// + /// The `OsslAesGcmAlgo` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} diff --git a/crates/crypto/src/aes/mod.rs b/crates/crypto/src/aes/mod.rs index 0f9c4e939..fc4cf330a 100644 --- a/crates/crypto/src/aes/mod.rs +++ b/crates/crypto/src/aes/mod.rs @@ -40,9 +40,25 @@ mod block; cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { mod key_ossl; + #[cfg(ossl300)] mod ecb_ossl; + #[cfg(not(ossl300))] + #[path = "ecb_ossl11.rs"] + mod ecb_ossl; + #[cfg(ossl300)] + mod cbc_ossl; + #[cfg(not(ossl300))] + #[path = "cbc_ossl11.rs"] mod cbc_ossl; + #[cfg(ossl300)] mod xts_ossl; + #[cfg(not(ossl300))] + #[path = "xts_ossl11.rs"] + mod xts_ossl; + #[cfg(ossl300)] + mod gcm_ossl; + #[cfg(not(ossl300))] + #[path = "gcm_ossl11.rs"] mod gcm_ossl; } else if #[cfg(target_os = "windows")] { mod key_cng; diff --git a/crates/crypto/src/aes/xts_ossl11.rs b/crates/crypto/src/aes/xts_ossl11.rs new file mode 100644 index 000000000..847b975eb --- /dev/null +++ b/crates/crypto/src/aes/xts_ossl11.rs @@ -0,0 +1,625 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based AES-XTS encryption/decryption implementation. +//! +//! This module provides AES XEX-based tweaked-codebook mode with ciphertext stealing (XTS) +//! operations using OpenSSL as the underlying cryptographic backend. XTS mode is specifically +//! designed for disk encryption where each sector can be encrypted independently. +//! +//! # XTS Mode +//! +//! XTS (XEX-based tweaked-codebook mode with ciphertext stealing) mode is designed for +//! encrypting data on block devices. It uses two keys: one for encryption and one for +//! generating the tweak. The tweak value typically represents the sector number, ensuring +//! that identical plaintext blocks at different locations produce different ciphertext. +//! +//! # Security Considerations +//! +//! - **Key Requirements**: XTS requires twice the key material of other modes +//! (e.g., AES-128-XTS uses two 128-bit keys for a total of 256 bits) +//! - **Tweak Uniqueness**: Each data unit (typically a disk sector) must have a unique tweak value +//! - **Minimum Data Size**: Input data must be at least one block (16 bytes) in size +//! - **No Authentication**: XTS mode does not provide authentication; it only provides confidentiality +//! - **Sector-based**: Designed for disk encryption, not for general-purpose data encryption + +use openssl::symm::*; + +use super::*; + +/// OpenSSL AES-XTS encryption/decryption operation. +/// +/// This structure configures an AES-XTS operation with a tweak value. The tweak is typically +/// a sector number for disk encryption, ensuring that identical data in different sectors +/// produces different ciphertext. +/// +/// # Fields +/// +/// The structure maintains: +/// - Tweak value (provided as an 8-byte little-endian counter and expanded to 16 bytes for OpenSSL) +/// +/// # Thread Safety +/// +/// This structure can be used across multiple operations with the same tweak, but is not +/// thread-safe. For concurrent operations, create separate instances. +pub struct OsslAesXtsAlgo { + /// Tweak value for XTS mode. + /// + /// The public API accepts an 8-byte tweak (little-endian) which is interpreted as a `u64`. + /// OpenSSL expects a 16-byte tweak/IV for AES-XTS, so we expand the `u64` to 16 bytes by + /// zero-padding the upper 8 bytes. + tweak: u64, + + /// Data unit length (bytes) for XTS operations. + /// + /// Input is processed in chunks of this size. The tweak is incremented once per data unit. + dul: usize, +} + +impl OsslAesXtsAlgo { + /// AES block size in bytes (16 bytes / 128 bits) + const BLOCK_SIZE: usize = 16; + const TWEAK_SIZE: usize = 8; + + /// Creates a new AES-XTS operation with the specified tweak value and data unit length. + /// + /// # Arguments + /// + /// * `tweak` - Tweak value for XTS mode. Must be exactly 8 bytes and is interpreted as a + /// little-endian `u64`. The tweak should be unique for each data unit being encrypted + /// (e.g., disk sector number). + /// * `dul` - Data unit length in bytes. This controls how the input is split into chunks + /// for XTS processing. Each chunk is processed independently with an incremented tweak. + /// Must be a multiple of the AES block size (16 bytes). + /// + /// # Returns + /// + /// A new `OsslAesXtsAlgo` instance configured with the specified tweak. + /// + /// # Security + /// + /// The tweak must be: + /// - Unique for each data unit (sector) being encrypted + /// - Can be stored or transmitted in plaintext + /// - Typically derived from the sector number or logical block address + pub fn new(tweak: &[u8], dul: usize) -> Result { + if tweak.len() != Self::TWEAK_SIZE { + Err(CryptoError::AesXtsInvalidTweakSize)?; + } + let tweak_val = tweak + .try_into() + .map(u64::from_le_bytes) + .map_err(|_| CryptoError::AesXtsInvalidTweakSize)?; + // Check if data unit length is valid. + if dul == 0 || !dul.is_multiple_of(Self::BLOCK_SIZE) { + Err(CryptoError::AesXtsInvalidDataUnitLen)?; + } + Ok(OsslAesXtsAlgo { + tweak: tweak_val, + dul, + }) + } + + /// Returns the current tweak value. + /// + /// # Returns + /// + /// The 8-byte tweak value (little-endian) used for XTS operations. + /// + /// # Notes + /// + /// The tweak value is automatically incremented once per processed data unit during + /// encryption or decryption operations. + pub fn tweak(&self) -> Vec { + self.tweak.to_le_bytes().to_vec() + } + + /// Returns the appropriate OpenSSL cipher based on key size. + /// + /// # Arguments + /// + /// * `key_size` - Total key size in bytes (32 for AES-128-XTS, 64 for AES-256-XTS) + /// + /// # Returns + /// + /// The OpenSSL cipher object for the specified key size. + /// + /// # Errors + /// + /// Returns `CryptoError::AesXtsInvalidKeySize` if the key size is not 32 or 64 bytes. + fn cipher(key_size: usize) -> Result { + match key_size { + 32 => Ok(Cipher::aes_128_xts()), + 64 => Ok(Cipher::aes_256_xts()), + _ => Err(CryptoError::AesXtsInvalidKeySize), + } + } + + /// Increments the tweak value for the next data unit. + /// + /// Treats the tweak as a little-endian `u64` counter and increments it by `inc_val` + /// without allowing wraparound. + fn increment_tweak(&mut self, inc_val: u64) -> Result<(), CryptoError> { + let incremented = self + .tweak + .checked_add(inc_val) + .ok_or(CryptoError::AesXtsTweakOverflow)?; + + //copy value back to tweak + self.tweak = incremented; + + Ok(()) + } + + /// Validates that incrementing the tweak by `inc_val` will not overflow. + /// AES XTS spec requires unique tweaks for each data unit; wraparound is not allowed. + fn validate_tweak_increment(&self, inc_val: u64) -> Result<(), CryptoError> { + // Check if tweak + inc_val overflows u64. + let current = self.tweak; + current + .checked_add(inc_val) + .ok_or(CryptoError::AesXtsTweakOverflow)?; + Ok(()) + } + + /// Creates and configures an OpenSSL Crypter for AES-XTS operations. + /// + /// # Arguments + /// + /// * `cipher` - The OpenSSL cipher to use + /// * `mode` - Encryption or decryption mode + /// * `key_bytes` - The key material + /// + /// The tweak used for the operation is taken from `self.tweak` and expanded to 16 bytes. + /// The lower 8 bytes come from the little-endian `u64`; the upper 8 bytes are zero. + /// + /// # Returns + /// + /// A configured `Crypter` with padding disabled (required for XTS mode). + /// + /// # Errors + /// + /// Returns `CryptoError::AesXtsEncryptError` or `CryptoError::AesXtsDecryptError` + /// depending on the mode if crypter creation fails. + fn init_crypter( + &self, + cipher: Cipher, + mode: Mode, + key_bytes: &[u8], + ) -> Result { + // get tweak expanded to block size + let full_tweak = (self.tweak as u128).to_le_bytes(); + + // Initialize and configure OpenSSL Crypter for AES-XTS + let mut crypter = + Crypter::new(cipher, mode, key_bytes, Some(&full_tweak)).map_err(|_| match mode { + Mode::Encrypt => CryptoError::AesXtsEncryptError, + Mode::Decrypt => CryptoError::AesXtsDecryptError, + })?; + // Disable padding for XTS mode + crypter.pad(false); + + Ok(crypter) + } + + /// Encrypts or decrypts one data block (chunk) using AES-XTS. + /// + /// This helper exists to share the common OpenSSL `Crypter` setup and + /// `update`/`finalize` flow between encrypt and decrypt. + /// + /// # Arguments + /// + /// * `cipher` - The OpenSSL cipher to use + /// * `mode` - Encryption or decryption mode + /// * `key_bytes` - The AES-XTS key material + /// * `input` - The input chunk to process + /// * `output` - Output buffer for the processed chunk (must be at least `input.len()`) + /// + /// # Returns + /// + /// The number of bytes written (always equals `input.len()` for XTS with padding disabled). + /// + /// # Errors + /// + /// Returns `CryptoError::AesXtsEncryptError` or `CryptoError::AesXtsDecryptError` + /// depending on the mode if the OpenSSL operation fails. + fn crypt_chunk( + &self, + cipher: Cipher, + mode: Mode, + key_bytes: &[u8], + input: &[u8], + output: &mut [u8], + ) -> Result { + let mut crypter = self.init_crypter(cipher, mode, key_bytes)?; + + let mut count = crypter.update(input, output).map_err(|_| match mode { + Mode::Encrypt => CryptoError::AesXtsEncryptError, + Mode::Decrypt => CryptoError::AesXtsDecryptError, + })?; + count += crypter + .finalize(&mut output[count..]) + .map_err(|_| match mode { + Mode::Encrypt => CryptoError::AesXtsEncryptError, + Mode::Decrypt => CryptoError::AesXtsDecryptError, + })?; + + // With XTS + padding disabled, output should match the input length. + if count != input.len() { + Err(match mode { + Mode::Encrypt => CryptoError::AesXtsEncryptError, + Mode::Decrypt => CryptoError::AesXtsDecryptError, + })?; + } + + Ok(count) + } + + /// Encrypts or decrypts a contiguous sequence of whole data units. + /// + /// This helper splits `input` into `self.dul` chunks, processes each chunk with the + /// current tweak, and increments the tweak once per data unit. + /// + /// # Arguments + /// + /// * `mode` - Encryption or decryption mode + /// * `key` - The AES-XTS key + /// * `input` - Input bytes to process (must be a multiple of `self.dul`) + /// * `output` - Optional output buffer. If `None`, returns the required output size. + /// + /// # Returns + /// + /// The number of bytes written to `output` (always equals `input.len()` on success). + /// + /// # Errors + /// + /// Returns an error if: + /// - The input size is not a multiple of the data unit length + /// - The output buffer is too small + /// - The tweak would overflow while processing the data units + /// - The key size is invalid + /// - The underlying OpenSSL operation fails + fn crypt_data_units( + &mut self, + mode: Mode, + key: &AesXtsKey, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + //check if input size is multiple of data unit length + if !input.len().is_multiple_of(self.dul) { + Err(CryptoError::AesXtsInvalidInputSize)?; + } + + //process data units if output buffer is provided + if let Some(output) = output { + //check if output buffer is large enough + if output.len() < input.len() { + Err(CryptoError::AesXtsBufferTooSmall)?; + } + + // Avoid partially writing output and then failing mid-loop. + // One tweak increment is performed per data unit. + let data_units = input.len() / self.dul; + + // Validate that incrementing the tweak by the number of data units will not overflow. + self.validate_tweak_increment(data_units as u64)?; + + let mut offset = 0usize; + + // Extract key bytes. + let key_bytes = key.bytes(); + + //get cipher based on key size + let cipher = Self::cipher(key.size())?; + + for unit in input.chunks(self.dul) { + let out_end = offset + unit.len(); + + // OpenSSL does not expose the evolving tweak, so we create a new Crypter + // per data unit with the current tweak. + self.crypt_chunk(cipher, mode, key_bytes, unit, &mut output[offset..out_end])?; + + offset = out_end; + self.increment_tweak(1)?; + } + + Ok(offset) + } else { + // If output is None, just return the input length as the required output size. + Ok(input.len()) + } + } +} + +/// Encryption operation implementation for AES-XTS using OpenSSL. +impl EncryptOp for OsslAesXtsAlgo { + type Key = AesXtsKey; + + /// Encrypts data using AES-XTS mode. + /// + /// # Arguments + /// + /// * `key` - The AES-XTS key (32 bytes for AES-128-XTS, 64 bytes for AES-256-XTS) + /// * `input` - Plaintext data to encrypt (must be a multiple of the data unit length) + /// * `output` - Optional output buffer. If `None`, returns the required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer size if + /// `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The input size is not a multiple of the data unit length + /// - The tweak size is invalid + /// - The key size is invalid + /// - The output buffer is too small + /// - The underlying OpenSSL operation fails + fn encrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + //call crypt data units with encrypt mode + self.crypt_data_units(Mode::Encrypt, key, input, output) + } +} + +/// Decryption operation implementation for AES-XTS using OpenSSL. +impl DecryptOp for OsslAesXtsAlgo { + type Key = AesXtsKey; + + /// Decrypts data using AES-XTS mode. + /// + /// # Arguments + /// + /// * `key` - The AES-XTS key (32 bytes for AES-128-XTS, 64 bytes for AES-256-XTS) + /// * `input` - Ciphertext data to decrypt (must be a multiple of the data unit length) + /// * `output` - Optional output buffer. If `None`, returns the required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer size if + /// `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The input size is not a multiple of the data unit length + /// - The tweak size is invalid + /// - The key size is invalid + /// - The output buffer is too small + /// - The underlying OpenSSL operation fails + fn decrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + self.crypt_data_units(Mode::Decrypt, key, input, output) + } +} + +/// AES-XTS streaming encryption context (OpenSSL backend). +/// +/// This context does not buffer data. Each `update()` call must provide input whose +/// length is a multiple of the configured data unit length (`dul`). The tweak is +/// incremented once per processed data unit. +pub struct OsslAesXtsEncryptContext { + algo: OsslAesXtsAlgo, + key: AesXtsKey, +} + +impl<'a> EncryptStreamingOp<'a> for OsslAesXtsAlgo { + type Key = AesXtsKey; + type Context = OsslAesXtsEncryptContext; + + /// Initializes a streaming AES-XTS encryption context. + /// + /// # Arguments + /// + /// * `key` - The AES-XTS key + /// + /// # Returns + /// + /// A streaming context that can be used with `update()`/`finish()`. + /// + /// # Errors + /// + /// Returns an error if the tweak size or key size is invalid. + fn encrypt_init(self, key: Self::Key) -> Result { + Ok(OsslAesXtsEncryptContext { algo: self, key }) + } +} + +/// Streaming encryption operation implementation for OpenSSL AES-XTS. +/// +/// `update()` processes whole data units. +/// `finish()` is a no-op for this backend. +impl<'a> EncryptOpContext<'a> for OsslAesXtsEncryptContext { + type Algo = OsslAesXtsAlgo; + + /// Encrypts input data in a streaming fashion. + /// + /// The input length must be a multiple of the configured data unit length (`dul`). + /// + /// # Arguments + /// + /// * `input` - Plaintext bytes to add to the stream + /// * `output` - Optional output buffer. If `None`, returns the number of bytes that would be written. + /// + /// # Returns + /// + /// Number of bytes written (or that would be written) for complete data units. + /// + /// # Errors + /// + /// Returns an error if the output buffer is too small or the OpenSSL operation fails. + fn update( + &mut self, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + self.algo + .crypt_data_units(Mode::Encrypt, &self.key, input, output) + } + + /// Finalizes streaming encryption. + /// + /// This backend does not buffer, so `finish()` is a no-op. + /// + /// # Arguments + /// + /// * `output` - Optional output buffer. If `None`, returns the required buffer size. + /// + /// # Returns + /// + /// Number of bytes written (or required). + /// + /// # Errors + /// + /// Returns an error if the final buffered length is not a multiple of the data unit length, + /// the output buffer is too small, or the OpenSSL operation fails. + fn finish(&mut self, _output: Option<&mut [u8]>) -> Result { + // AES XTS does not buffer data in this implementation, so finish is a no-op. + Ok(0) + } + /// Returns a reference to the algorithm state. + /// + /// This exposes the current AES-XTS configuration (including the tweak and + /// data unit length). The tweak is updated as data units are processed. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the algorithm state. + /// + /// Modifying the tweak or data unit length mid-stream will affect subsequent + /// encryption and can render the ciphertext undecryptable. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the algorithm state. + /// + /// This is useful if the caller needs to recover the updated tweak after a + /// streaming operation completes. + fn into_algo(self) -> Self::Algo { + self.algo + } +} + +/// AES-XTS streaming decryption context (OpenSSL backend). +/// +/// This context does not buffer data. Each `update()` call must provide input whose +/// length is a multiple of the configured data unit length (`dul`). The tweak is +/// incremented once per processed data unit. +pub struct OsslAesXtsDecryptContext { + algo: OsslAesXtsAlgo, + key: AesXtsKey, +} + +impl<'a> DecryptStreamingOp<'a> for OsslAesXtsAlgo { + type Key = AesXtsKey; + type Context = OsslAesXtsDecryptContext; + + /// Initializes a streaming AES-XTS decryption context. + /// + /// # Arguments + /// + /// * `key` - The AES-XTS key + /// + /// # Returns + /// + /// A streaming context that can be used with `update()`/`finish()`. + /// + /// # Errors + /// + /// Returns an error if the tweak size or key size is invalid. + fn decrypt_init(self, key: Self::Key) -> Result { + Ok(OsslAesXtsDecryptContext { algo: self, key }) + } +} + +/// Streaming decryption operation implementation for OpenSSL AES-XTS. +/// +/// `update()` processes whole data units. +/// `finish()` is a no-op for this backend. +impl<'a> DecryptOpContext<'a> for OsslAesXtsDecryptContext { + type Algo = OsslAesXtsAlgo; + + /// Decrypts input data in a streaming fashion. + /// + /// The input length must be a multiple of the configured data unit length (`dul`). + /// + /// # Arguments + /// + /// * `input` - Ciphertext bytes to add to the stream + /// * `output` - Optional output buffer. If `None`, returns the number of bytes that would be written. + /// + /// # Returns + /// + /// Number of bytes written (or that would be written) for complete data units. + /// + /// # Errors + /// + /// Returns an error if the output buffer is too small or the OpenSSL operation fails. + fn update( + &mut self, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + //call crypt data units with decrypt mode + self.algo + .crypt_data_units(Mode::Decrypt, &self.key, input, output) + } + + /// Finalizes streaming decryption. + /// + /// This backend does not buffer, so `finish()` is a no-op. + /// + /// # Arguments + /// + /// * `output` - Optional output buffer. If `None`, returns the required buffer size. + /// + /// # Returns + /// + /// Number of bytes written (or required). + /// + /// # Errors + /// + /// Returns an error if the final buffered length is not a multiple of the data unit length, + /// the output buffer is too small, or the OpenSSL operation fails. + fn finish(&mut self, _output: Option<&mut [u8]>) -> Result { + // AES XTS does not buffer data in this implementation, so finish is a no-op. + Ok(0) + } + + /// Returns a reference to the algorithm state. + /// + /// This exposes the current AES-XTS configuration (including the tweak and + /// data unit length). The tweak is updated as data units are processed. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the algorithm state. + /// + /// Modifying the tweak or data unit length mid-stream will affect subsequent + /// decryption and can cause authentication/validation failures at higher + /// layers (or produce incorrect plaintext). + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the algorithm state. + /// + /// This is useful if the caller needs to recover the updated tweak after a + /// streaming operation completes. + fn into_algo(self) -> Self::Algo { + self.algo + } +} diff --git a/crates/crypto/src/ecc/ecc_ossl11.rs b/crates/crypto/src/ecc/ecc_ossl11.rs new file mode 100644 index 000000000..3f2edfbca --- /dev/null +++ b/crates/crypto/src/ecc/ecc_ossl11.rs @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based Elliptic Curve Digital Signature Algorithm (ECDSA) operations. +//! +//! This module provides ECDSA signature generation and verification using OpenSSL +//! as the underlying cryptographic backend. It supports NIST standard curves +//! (P-256, P-384, P-521) for digital signatures. +//! +//! # ECDSA Algorithm +//! +//! ECDSA (Elliptic Curve Digital Signature Algorithm) is a cryptographic algorithm +//! used to generate and verify digital signatures. It provides: +//! - **Authentication**: Verifies the identity of the signer +//! - **Non-repudiation**: Signer cannot deny having signed the message +//! - **Integrity**: Detects any modifications to the signed data +//! +//! # Signature Format +//! +//! Signatures are generated in DER-encoded format, containing two integers (r, s) +//! that form the ECDSA signature. The signature size depends on the curve: +//! - P-256: ~70-72 bytes +//! - P-384: ~102-104 bytes +//! - P-521: ~137-139 bytes +//! +//! # Security Considerations +//! +//! - Always use a cryptographically secure hash function (SHA-256 or stronger) +//! - Private keys must be kept secure and never shared +//! - Each signature uses a unique random value (nonce) generated securely +//! - Verify signatures before trusting signed data + +use openssl::pkey_ctx::*; + +use super::*; + +/// OpenSSL ECDSA signature and verification operations. +/// +/// This structure provides stateless ECDSA operations for signing and verifying +/// data with elliptic curve keys. It implements the `SignOp` and `VerifyOp` traits +/// for NIST curves (P-256, P-384, P-521). +/// +/// # Thread Safety +/// +/// This structure is thread-safe and can be used concurrently from multiple threads. +/// Each operation creates its own OpenSSL context. +#[derive(Default)] +pub struct OsslEccAlgo {} + +impl SignOp for OsslEccAlgo { + type Key = EccPrivateKey; + + /// Generates an ECDSA signature for the provided data. + /// + /// This method creates a digital signature using the private key. The signature + /// is deterministic based on the input data and the random nonce generated by + /// OpenSSL's secure random number generator. + /// + /// # Arguments + /// + /// * `key` - The ECC private key used for signing + /// * `data` - The data to sign (typically a hash of the original message) + /// * `signature` - Optional output buffer. If `None`, returns the required buffer size. + /// + /// # Returns + /// + /// The number of bytes written to the signature buffer, or the required buffer size + /// if `signature` is `None`. The signature size depends on the curve: + /// - P-256: ~70-72 bytes + /// - P-384: ~102-104 bytes + /// - P-521: ~137-139 bytes + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::EccError` - Context initialization fails + /// - `CryptoError::EccSignError` - Signing operation fails + /// + /// # Security + /// + /// - Always hash the input data before signing (e.g., with SHA-256) + /// - Never sign raw user input directly + /// - Each signature uses a unique cryptographically secure random nonce + fn sign( + &mut self, + key: &Self::Key, + data: &[u8], + signature: Option<&mut [u8]>, + ) -> Result { + let len = key.curve().point_size() * 2; + if let Some(signature) = signature { + if signature.len() < len { + return Err(CryptoError::EccSignError); + } + + let mut ctx = PkeyCtx::new(key.pkey()).map_err(|_| CryptoError::EccError)?; + + ctx.sign_init().map_err(|_| CryptoError::EccSignError)?; + + let len = ctx + .sign(data, None) + .map_err(|_| CryptoError::EccSignError)?; + + let mut vec = vec![0u8; len]; + + let len = ctx + .sign(data, Some(&mut vec)) + .map_err(|_| CryptoError::EccSignError)?; + + let der = DerEccSignature::from_der(key.curve(), &vec[..len])?; + + signature[..key.curve().point_size()].copy_from_slice(der.r()); + signature[key.curve().point_size()..].copy_from_slice(der.s()); + } + + Ok(len) + } +} + +impl VerifyOp for OsslEccAlgo { + type Key = EccPublicKey; + + /// Verifies an ECDSA signature against the provided data. + /// + /// This method checks whether the signature is valid for the given data and + /// public key. It uses the public key to verify that the signature was created + /// by the corresponding private key. + /// + /// # Arguments + /// + /// * `key` - The ECC public key used for verification + /// * `data` - The data that was signed (typically a hash of the original message) + /// * `signature` - The signature to verify (DER-encoded) + /// + /// # Returns + /// + /// Returns `Ok(true)` if the signature is valid, `Ok(false)` if the signature is + /// invalid (does not match the data and key). + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::EccError` - Context initialization fails + /// - `CryptoError::EccVerifyError` - Verification operation fails (malformed signature) + /// + /// # Security + /// + /// - Always use the same hash algorithm for verification as was used for signing + /// - Invalid signatures return `Ok(false)`, not an error + /// - Malformed signatures that cannot be parsed return an error + fn verify( + &mut self, + key: &Self::Key, + data: &[u8], + signature: &[u8], + ) -> Result { + let len = key.curve().point_size() * 2; + if signature.len() != len { + Err(CryptoError::EccVerifyError)? + } + + let der_sig = DerEccSignature::new( + key.curve(), + &signature[..key.curve().point_size()], + &signature[key.curve().point_size()..], + )?; + + let der_len = der_sig.to_der(None)?; + let mut der = vec![0u8; der_len]; + let der_len = der_sig.to_der(Some(&mut der))?; + + let mut ctx = PkeyCtx::new(key.pkey()).map_err(|_| CryptoError::EccError)?; + ctx.verify_init().map_err(|_| CryptoError::EccVerifyError)?; + + // Returns true for valid signatures, false for invalid ones. + // + // The underlying OpenSSL function (EVP_PKEY_verify) may return an error + // when encountering malformed signatures or corrupt data rather than returning false. + // We treat such errors as invalid signatures and return false. + ctx.verify(data, &der[..der_len]).or(Ok(false)) + } +} diff --git a/crates/crypto/src/ecc/ecdh_ossl11.rs b/crates/crypto/src/ecc/ecdh_ossl11.rs new file mode 100644 index 000000000..a9d9e9186 --- /dev/null +++ b/crates/crypto/src/ecc/ecdh_ossl11.rs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based ECDH (Elliptic Curve Diffie-Hellman) key derivation implementation. +//! +//! This module provides ECDH key agreement operations using OpenSSL's cryptographic +//! primitives. ECDH is a key agreement protocol that allows two parties to establish +//! a shared secret over an insecure channel using elliptic curve cryptography. +//! +//! # Protocol +//! +//! ECDH works by: +//! 1. Each party has an ECC private/public key pair +//! 2. Each party shares their public key with the other +//! 3. Each party combines their own private key with the peer's public key +//! 4. Both parties arrive at the same shared secret +//! +//! # Security +//! +//! - The shared secret should be used for key derivation (e.g., HKDF) rather than directly +//! - Ephemeral keys (ECDHE) provide forward secrecy +//! - Public keys should be authenticated to prevent man-in-the-middle attacks + +use super::*; + +/// OpenSSL-backed ECDH key derivation operation. +/// +/// This structure performs Elliptic Curve Diffie-Hellman key agreement, producing +/// a shared secret from a local private key and a peer's public key. The shared +/// secret can then be used as key material for symmetric encryption or further +/// key derivation. +/// +/// # Lifetime +/// +/// The lifetime parameter `'a` ensures that the peer's public key remains valid +/// for the duration of the ECDH operation. +/// +/// # Security Considerations +/// +/// - The derived shared secret should not be used directly as an encryption key +/// - Apply a key derivation function (KDF) like HKDF to the shared secret +/// - Validate peer public keys to prevent invalid curve attacks +/// - Use ephemeral keys when forward secrecy is required +/// - The same shared secret is derived by both parties +pub struct OsslEcdhAlgo<'a> { + /// The peer's public key used for key agreement + peer_key: &'a EccPublicKey, +} + +impl<'a> OsslEcdhAlgo<'a> { + /// Creates a new ECDH operation with a peer's public key. + /// + /// This constructor initializes an ECDH key agreement operation that will + /// use the provided peer public key to derive a shared secret when combined + /// with a local private key. + /// + /// # Arguments + /// + /// * `peer_key` - Reference to the peer's ECC public key + /// + /// # Returns + /// + /// A new `OsslEcdh` instance ready to perform key derivation. + /// + /// # Security + /// + /// - Ensure the peer public key is authentic (e.g., via certificate validation) + /// - The peer key should be on the same curve as the local private key + /// - Validate that the peer key is a valid curve point + pub fn new(peer_key: &'a EccPublicKey) -> Self { + Self { peer_key } + } +} + +impl<'a> DeriveOp for OsslEcdhAlgo<'a> { + type Key = EccPrivateKey; + type DerivedKey = GenericSecretKey; + + /// Performs ECDH key agreement to derive a shared secret. + /// + /// This method combines the local private key with the peer's public key + /// to compute a shared secret using the ECDH algorithm. Both parties using + /// matching key pairs will derive the same shared secret. + /// + /// # Arguments + /// + /// * `key` - The local ECC private key + /// * `derived_len` - The desired length of the derived key in bytes (currently unused; + /// ECDH produces a fixed-length shared secret based on the curve) + /// + /// # Returns + /// + /// A `GenericSecretKey` containing the derived shared secret. + /// + /// # Errors + /// + /// Returns an error if: + /// - The key derivation operation fails + /// - The peer key is invalid or on a different curve + /// - The private key is invalid + /// - OpenSSL encounters an internal error + /// + /// # Security + /// + /// The derived shared secret should be processed through a key derivation + /// function (KDF) before use: + /// - Use HKDF or similar to derive actual encryption keys + /// - Include context information to prevent key reuse + /// - Consider adding a salt for additional security + /// - Never use the raw shared secret directly for encryption + fn derive(&self, key: &Self::Key, derived_len: usize) -> Result { + use openssl::derive::Deriver; + + let mut deriver = Deriver::new(key.pkey()).map_err(|_| CryptoError::EcdhError)?; + + deriver + .set_peer(self.peer_key.pkey()) + .map_err(|_| CryptoError::EcdhSetPropertyError)?; + + let len = deriver.len().map_err(|_| CryptoError::EcdhError)?; + + if derived_len != len { + return Err(CryptoError::EcdhInvalidDerivedKeyLength); + } + + let secret = deriver + .derive_to_vec() + .map_err(|_| CryptoError::EcdhDeriveError)?; + + GenericSecretKey::from_bytes(&secret) + } +} diff --git a/crates/crypto/src/ecc/mod.rs b/crates/crypto/src/ecc/mod.rs index 5808c8414..50b3bdaec 100644 --- a/crates/crypto/src/ecc/mod.rs +++ b/crates/crypto/src/ecc/mod.rs @@ -48,7 +48,15 @@ cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { mod key_ossl; + #[cfg(ossl300)] mod ecc_ossl; + #[cfg(not(ossl300))] + #[path = "ecc_ossl11.rs"] + mod ecc_ossl; + #[cfg(ossl300)] + mod ecdh_ossl; + #[cfg(not(ossl300))] + #[path = "ecdh_ossl11.rs"] mod ecdh_ossl; } else if #[cfg(target_os = "windows")] { mod key_cng; diff --git a/crates/crypto/src/hash/hash_ossl11.rs b/crates/crypto/src/hash/hash_ossl11.rs new file mode 100644 index 000000000..729978b44 --- /dev/null +++ b/crates/crypto/src/hash/hash_ossl11.rs @@ -0,0 +1,369 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based cryptographic hash function implementations for Linux systems. +//! +//! This module provides concrete implementations of various hash algorithms using +//! the OpenSSL cryptographic library. It serves as the Linux-specific backend +//! for the platform-agnostic hash interface defined in the parent module. +//! +//! # Supported Algorithms +//! +//! - **SHA-1**: Legacy hash function (cryptographically broken) +//! - **SHA-256**: Secure 256-bit hash from SHA-2 family +//! - **SHA-384**: Secure 384-bit hash from SHA-2 family +//! - **SHA-512**: Secure 512-bit hash from SHA-2 family +//! +//! # Implementation Strategy +//! +//! The module provides the `OsslHash` type that stores the selected hash algorithm +//! and corresponding OpenSSL `MessageDigest`. Instances can be created using the +//! `new()` constructor or convenient factory methods like `sha256()`. +//! +//! # Platform Integration +//! +//! - Leverages OpenSSL's optimized hash implementations +//! - Automatically benefits from hardware acceleration (AES-NI, etc.) +//! - Uses system-provided OpenSSL for security updates +//! - Provides memory-safe Rust wrappers around OpenSSL APIs +//! +//! # Performance +//! +//! OpenSSL implementations are highly optimized and include: +//! - Assembly-optimized code paths for various architectures +//! - Hardware acceleration when available +//! - Efficient memory management +//! - Vectorized operations for large data processing + +use openssl::hash::*; +use openssl::md::*; + +use super::*; + +/// OpenSSL-based hash implementation. +/// +/// This structure provides a hash implementation using OpenSSL APIs. +/// It stores the hash algorithm selection and the corresponding OpenSSL +/// `MessageDigest` for efficient hash operations. +#[derive(Clone)] +pub struct OsslHashAlgo { + md: MessageDigest, +} + +impl OsslHashAlgo { + /// Creates a new instance of the OpenSSL hash implementation. + /// + /// Initializes the hash implementation with the specified algorithm and + /// obtains the corresponding OpenSSL `MessageDigest`. + /// + /// # Arguments + /// + /// * `algo` - The hash algorithm to use + /// + /// # Returns + /// + /// A new `OsslHash` instance ready to perform hash operations. + pub fn new(md: MessageDigest) -> Self { + Self { md } + } + + /// Creates a new SHA-1 hash instance. + /// + /// # Returns + /// + /// A new `OsslHash` instance configured for SHA-1 hashing. + /// + /// # Security Warning + /// + /// SHA-1 is cryptographically broken and should not be used for security-sensitive + /// applications. Use SHA-256 or stronger algorithms instead. + pub fn sha1() -> Self { + Self::new(MessageDigest::sha1()) + } + + /// Creates a new SHA-256 hash instance. + /// + /// SHA-256 is part of the SHA-2 family and provides 256-bit hash values. + /// It is recommended for most cryptographic applications. + /// + /// # Returns + /// + /// A new `OsslHash` instance configured for SHA-256 hashing. + pub fn sha256() -> Self { + Self::new(MessageDigest::sha256()) + } + + /// Creates a new SHA-384 hash instance. + /// + /// SHA-384 is part of the SHA-2 family and provides 384-bit hash values. + /// It is a truncated version of SHA-512 and is suitable for high-security applications. + /// + /// # Returns + /// + /// A new `OsslHash` instance configured for SHA-384 hashing. + pub fn sha384() -> Self { + Self::new(MessageDigest::sha384()) + } + + /// Creates a new SHA-512 hash instance. + /// + /// SHA-512 is part of the SHA-2 family and provides 512-bit hash values. + /// It is suitable for high-security applications requiring larger hash outputs. + /// + /// # Returns + /// + /// A new `OsslHash` instance configured for SHA-512 hashing. + pub fn sha512() -> Self { + Self::new(MessageDigest::sha512()) + } + + /// Returns the hash output size in bytes for this algorithm. + /// + /// This method provides the size of the hash digest produced by this + /// hash algorithm without performing any cryptographic operations. + /// + /// # Returns + /// + /// The hash output size in bytes: + /// - SHA-1: 20 bytes + /// - SHA-256: 32 bytes + /// - SHA-384: 48 bytes + /// - SHA-512: 64 bytes + pub fn size(&self) -> usize { + self.md.size() + } + + /// Returns the OpenSSL MessageDigest for this hash algorithm. + /// + /// # Returns + /// + /// The `MessageDigest` configured for this hash instance. + pub(crate) fn message_digest(&self) -> MessageDigest { + self.md + } + + /// Returns a reference to the OpenSSL MdRef for this hash algorithm. + /// + /// # Returns + /// + /// A reference to the `MdRef` configured for this hash instance. + pub(crate) fn md(&self) -> &MdRef { + #[allow(clippy::unwrap_used)] + Md::from_nid(self.message_digest().type_()).unwrap() + } + + /// Returns the DER digest algorithm identifier for this hash algorithm. + /// + /// # Returns + /// + /// The `DerDigestAlgo` corresponding to this hash algorithm. + pub(crate) fn der_algo(&self) -> DerDigestAlgo { + use openssl::nid::Nid; + match self.md.type_() { + Nid::SHA1 => DerDigestAlgo::Sha1, + Nid::SHA256 => DerDigestAlgo::Sha256, + Nid::SHA384 => DerDigestAlgo::Sha384, + Nid::SHA512 => DerDigestAlgo::Sha512, + _ => panic!("Unsupported hash algorithm for DER OID"), + } + } +} + +/// Implementation of one-shot hash operations using OpenSSL. +/// +/// This implementation provides both hash calculation and output size +/// determination through OpenSSL's optimized hash functions. +impl HashOp for OsslHashAlgo { + /// Computes a hash using OpenSSL's optimized implementation. + /// + /// This method leverages OpenSSL's `hash::hash` function for one-shot + /// hash computation. It handles both size queries and actual hash + /// computation based on whether an output buffer is provided. + /// + /// # Implementation Details + /// + /// - Uses OpenSSL's optimized one-shot hash function + /// - Validates output buffer size before computation + /// - Copies result to user-provided buffer + /// - Returns actual hash size regardless of operation mode + /// + /// # Buffer Management + /// + /// The method ensures the output buffer is large enough before performing + /// the hash computation, preventing buffer overflows and ensuring safe + /// operation. + fn hash(&mut self, data: &[u8], hash: Option<&mut [u8]>) -> Result { + use openssl::hash; + if let Some(hash) = hash { + if hash.len() < self.md.size() { + Err(CryptoError::HashBufferTooSmall)?; + } + let digest = hash::hash(self.md, data).map_err(|_| CryptoError::HashError)?; + hash[..self.md.size()].copy_from_slice(&digest[..self.md.size()]); + } + Ok(self.md.size()) + } +} + +impl HashStreamingOp for OsslHashAlgo { + type Context = OsslHashAlgoContext; + + /// Initializes a new OpenSSL hash context for streaming operations. + /// + /// Creates a new OpenSSL `Hasher` instance configured with the + /// appropriate `MessageDigest` for the algorithm. The context + /// maintains internal state for incremental hash computation. + /// + /// # Context Initialization + /// + /// - Creates OpenSSL `Hasher` with algorithm-specific configuration + /// - Stores the `MessageDigest` for later size queries + /// - Handles OpenSSL initialization errors gracefully + /// + /// # Error Handling + /// + /// Returns `CryptoError::HashInitError` if OpenSSL context + /// initialization fails, which may occur due to memory allocation + /// failures or invalid algorithm configurations. + fn hash_init(self) -> Result { + let context = + openssl::hash::Hasher::new(self.md).map_err(|_| CryptoError::HashInitError)?; + Ok(OsslHashAlgoContext { + algo: self, + hasher: context, + }) + } +} + +/// OpenSSL-based streaming hash context. +/// +/// This structure maintains the state for streaming hash operations, +/// wrapping OpenSSL's `Hasher` and providing the necessary metadata +/// for proper operation. +/// +/// # State Management +/// +/// The context encapsulates: +/// - OpenSSL's internal hash state via `Hasher` +/// - Algorithm metadata via `MessageDigest` +/// - All necessary information for multi-step hash computation +/// +/// # Thread Safety +/// +/// This context is not thread-safe and should be used from a single +/// thread. OpenSSL's `Hasher` maintains internal state that could +/// be corrupted by concurrent access. +pub struct OsslHashAlgoContext { + /// The hash algorithm instance. + algo: OsslHashAlgo, + /// OpenSSL hasher maintaining the algorithm state. + hasher: openssl::hash::Hasher, +} + +/// Implementation of streaming hash operations for OpenSSL contexts. +/// +/// This implementation provides incremental hash computation through +/// OpenSSL's streaming interface, allowing efficient processing of +/// large datasets. +impl HashOpContext for OsslHashAlgoContext { + /// The associated hash algorithm type. + type Algo = OsslHashAlgo; + + /// Updates the hash state with new input data. + /// + /// This method feeds new data into OpenSSL's incremental hash + /// computation engine. The data is processed immediately and + /// the internal state is updated accordingly. + /// + /// # Implementation Details + /// + /// - Delegates directly to OpenSSL's `Hasher::update` + /// - Handles OpenSSL errors and converts to `CryptoError` + /// - Maintains hash state across multiple update calls + /// - Optimized for processing data in chunks + /// + /// # Error Conditions + /// + /// Returns `CryptoError::HashUpdateError` if OpenSSL's update + /// operation fails, which may indicate memory issues or + /// corrupted context state. + fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> { + self.hasher + .update(data) + .map_err(|_| CryptoError::HashUpdateError) + } + + /// Finalizes the hash computation and produces the final result. + /// + /// This method completes the hash computation by calling OpenSSL's + /// finalization process, which applies the algorithm-specific padding + /// and produces the final hash value. + /// + /// # Finalization Process + /// + /// 1. Validates output buffer size if provided + /// 2. Calls OpenSSL's `Hasher::finish` to complete computation + /// 3. Copies result to output buffer if provided + /// 4. Returns the hash size regardless of operation mode + /// + /// # Buffer Management + /// + /// The method ensures the output buffer is sufficiently large to + /// hold the complete hash result before attempting the finalization, + /// preventing buffer overflows. + /// + /// # Context Consumption + /// + /// This method consumes the context by taking ownership (`self`), + /// ensuring that the context cannot be accidentally reused after + /// finalization, which would be a programming error. + /// + /// # Error Handling + /// + /// - `CryptoError::HashBufferTooSmall`: Output buffer insufficient + /// - `CryptoError::HashFinalizeError`: OpenSSL finalization failed + fn finish(&mut self, hash: Option<&mut [u8]>) -> Result { + let len = self.algo.md.size(); + if let Some(hash) = hash { + if hash.len() < len { + Err(CryptoError::HashBufferTooSmall)?; + } + + let digest = self + .hasher + .finish() + .map_err(|_| CryptoError::HashFinishError)?; + + hash[..digest.len()].copy_from_slice(&digest); + } + + Ok(len) + } + + /// Returns a reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslHash` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslHash` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying hash algorithm. + /// + /// # Returns + /// + /// The `OsslHash` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} diff --git a/crates/crypto/src/hash/mod.rs b/crates/crypto/src/hash/mod.rs index a4536fc50..d530106be 100644 --- a/crates/crypto/src/hash/mod.rs +++ b/crates/crypto/src/hash/mod.rs @@ -40,7 +40,11 @@ use super::*; -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", ossl300))] +mod hash_ossl; + +#[cfg(all(target_os = "linux", not(ossl300)))] +#[path = "hash_ossl11.rs"] mod hash_ossl; #[cfg(target_os = "windows")] diff --git a/crates/crypto/src/hmac/hmac_ossl11.rs b/crates/crypto/src/hmac/hmac_ossl11.rs new file mode 100644 index 000000000..48eefcd33 --- /dev/null +++ b/crates/crypto/src/hmac/hmac_ossl11.rs @@ -0,0 +1,451 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based HMAC implementation for Linux systems. +//! +//! This module provides concrete implementations of HMAC operations using +//! the OpenSSL cryptographic library. It serves as the Linux-specific backend +//! for the platform-agnostic HMAC interface defined in the parent module. +//! +//! # Supported Algorithms +//! +//! - **HMAC-SHA1**: Legacy algorithm (20-byte output, use with caution) +//! - **HMAC-SHA256**: Recommended for most applications (32-byte output) +//! - **HMAC-SHA384**: High security applications (48-byte output) +//! - **HMAC-SHA512**: Maximum security applications (64-byte output) +//! +//! # Implementation Strategy +//! +//! The module uses a generic approach with the `OsslHmacKey` type that wraps +//! algorithm-specific implementations. Each algorithm implements the `OsslHmacAlgo` +//! trait to provide its OpenSSL `MessageDigest` configuration and key size limits. +//! +//! # Platform Integration +//! +//! - Leverages OpenSSL's optimized HMAC implementations +//! - Automatically benefits from hardware acceleration when available +//! - Uses system-provided OpenSSL for security updates +//! - Provides memory-safe Rust wrappers around OpenSSL APIs +//! +//! # Performance +//! +//! OpenSSL implementations are highly optimized and include: +//! - Assembly-optimized code paths for various architectures +//! - Hardware acceleration when available (AES-NI, etc.) +//! - Efficient memory management for large data processing +//! - Vectorized operations for bulk HMAC computations +//! +//! # Security Features +//! +//! - Constant-time verification to prevent timing attacks +//! - Secure key material handling with automatic zeroization +//! - Protection against side-channel attacks through OpenSSL's implementations +//! - Proper validation of key sizes according to algorithm specifications + +use super::*; + +/// OpenSSL-backed HMAC operation provider. +/// +/// This structure configures and executes HMAC (Hash-based Message Authentication Code) +/// operations using OpenSSL's cryptographic primitives. It supports both single-operation +/// and streaming interfaces for signing and verification. +/// +/// # Algorithm Support +/// +/// Supports SHA-1, SHA-256, SHA-384, and SHA-512 as the underlying hash functions. +/// The hash algorithm is specified at construction time and determines the output size. +/// +/// # Thread Safety +/// +/// This structure is `Send` and `Sync` as it only stores configuration data. +/// Actual cryptographic operations are performed through OpenSSL APIs. +/// +/// # Security +/// +/// - Uses OpenSSL's constant-time verification to prevent timing attacks +/// - Leverages hardware acceleration when available +/// - Provides both oneshot and streaming APIs for different use cases +pub struct OsslHmacAlgo { + /// The hash algorithm to use for HMAC. + hash: HashAlgo, +} + +impl OsslHmacAlgo { + /// Creates a new HMAC operation provider from a hash instance. + /// + /// This constructor configures the HMAC provider but does not perform any + /// cryptographic operations. Actual signing or verification occurs when + /// calling the trait methods. + /// + /// # Arguments + /// + /// * `hash` - The hash instance specifying the algorithm to use + /// + /// # Returns + /// + /// A new `OsslHmac` instance configured for the specified algorithm. + pub fn new(hash: HashAlgo) -> Self { + Self { hash } + } +} + +/// Implements single-operation HMAC signing. +/// +/// This implementation uses OpenSSL's `Signer` to compute HMAC values in a single call, +/// suitable for when all data is available at once. +impl SignOp for OsslHmacAlgo { + type Key = HmacKey; + + /// Computes an HMAC signature over the provided data. + /// + /// This method can either query the required buffer size (when `signature` is `None`) + /// or compute and write the HMAC to the provided buffer. + /// + /// # Arguments + /// + /// * `key` - The HMAC key to use for signing + /// * `data` - The data to authenticate + /// * `signature` - Optional output buffer. If `None`, only returns required size. + /// + /// # Returns + /// + /// The number of bytes written to the signature buffer, or the required buffer size. + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::HmacBufferTooSmall` - Output buffer is too small + /// - `CryptoError::HmacSignError` - OpenSSL signing operation fails + fn sign( + &mut self, + key: &Self::Key, + data: &[u8], + signature: Option<&mut [u8]>, + ) -> Result { + use openssl::sign::Signer; + + let len = self.hash.size(); + if let Some(signature) = signature { + if signature.len() < len { + return Err(CryptoError::HmacBufferTooSmall); + } + + let mut signer = Signer::new(self.hash.message_digest(), key.pkey()) + .map_err(|_| CryptoError::HmacSignError)?; + + signer + .sign_oneshot(signature, data) + .map_err(|_| CryptoError::HmacSignError)?; + } + + Ok(len) + } +} + +/// Implements streaming HMAC signing. +/// +/// This implementation allows processing data in multiple chunks, useful for large +/// files or streaming data sources. +impl<'a> SignStreamingOp<'a> for OsslHmacAlgo { + type Key = HmacKey; + type Context = OsslHmacAlgoSignContext<'a>; + + /// Initializes a streaming HMAC signing context. + /// + /// Creates a new context that can process data incrementally via the + /// `update()` method before finalizing with `finish()`. + /// + /// # Arguments + /// + /// * `key` - The HMAC key to use for signing + /// + /// # Returns + /// + /// A streaming context that implements `SignStreamingOpContext`. + /// + /// # Errors + /// + /// Returns `CryptoError::HmacSignError` if context initialization fails. + fn sign_init(self, key: Self::Key) -> Result { + use openssl::sign::Signer; + + let signer = Signer::new(self.hash.message_digest(), key.pkey()) + .map_err(|_| CryptoError::HmacSignError)?; + + Ok(OsslHmacAlgoSignContext { signer, algo: self }) + } +} + +/// Streaming context for HMAC signing operations. +/// +/// This structure maintains the state for incremental HMAC computation, +/// allowing data to be processed in chunks before producing the final MAC. +/// +/// # Lifetime +/// +/// The lifetime parameter ensures the key remains valid for the duration +/// of the streaming operation. +pub struct OsslHmacAlgoSignContext<'a> { + /// OpenSSL signer for computing the HMAC + signer: openssl::sign::Signer<'a>, + /// Expected output size in bytes + algo: OsslHmacAlgo, +} + +impl<'a> SignStreamingOpContext<'a> for OsslHmacAlgoSignContext<'a> { + type Algo = OsslHmacAlgo; + /// Processes a chunk of data. + /// + /// Updates the internal HMAC state with the provided data. Can be called + /// multiple times before finalizing. + /// + /// # Arguments + /// + /// * `data` - Data chunk to process + /// + /// # Errors + /// + /// Returns `CryptoError::HmacSignError` if the update operation fails. + fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> { + self.signer + .update(data) + .map_err(|_| CryptoError::HmacSignError) + } + + /// Finalizes the HMAC computation and produces the signature. + /// + /// Completes the HMAC operation and writes the result to the provided buffer, + /// or returns the required buffer size if `signature` is `None`. + /// + /// # Arguments + /// + /// * `signature` - Optional output buffer. If `None`, only returns required size. + /// + /// # Returns + /// + /// The number of bytes written or required. + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::HmacBufferTooSmall` - Output buffer is too small + /// - `CryptoError::HmacSignError` - Finalization fails + fn finish(&mut self, signature: Option<&mut [u8]>) -> Result { + let len = self.algo.hash.size(); + if let Some(signature) = signature { + if signature.len() < len { + return Err(CryptoError::HmacBufferTooSmall); + } + + self.signer + .sign(signature) + .map_err(|_| CryptoError::HmacSignError)?; + } + Ok(len) + } + + /// Returns a reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslHash` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslHash` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying hash algorithm. + /// + /// # Returns + /// + /// The `OsslHash` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} + +/// Implements single-operation HMAC verification. +/// +/// This implementation uses OpenSSL's `Verifier` which performs constant-time +/// comparison to prevent timing attacks. +impl VerifyOp for OsslHmacAlgo { + type Key = HmacKey; + + /// Verifies an HMAC signature over the provided data. + /// + /// Uses constant-time comparison internally to prevent timing side-channel attacks. + /// + /// # Arguments + /// + /// * `key` - The HMAC key to use for verification + /// * `data` - The data that was authenticated + /// * `signature` - The signature to verify + /// + /// # Returns + /// + /// `Ok(true)` if the signature is valid, `Ok(false)` if invalid. + /// + /// # Errors + /// + /// Returns `CryptoError::HmacVerifyError` if the verification operation fails. + fn verify( + &mut self, + key: &Self::Key, + data: &[u8], + signature: &[u8], + ) -> Result { + use openssl::sign::Signer; + + let mut result = vec![0u8; self.hash.size()]; + + let mut verifier = Signer::new(self.hash.message_digest(), key.pkey()) + .map_err(|_| CryptoError::HmacVerifyError)?; + + verifier + .sign_oneshot(&mut result, data) + .map_err(|_| CryptoError::HmacVerifyError)?; + + Ok(result == signature) + } +} + +/// Implements streaming HMAC verification. +/// +/// This implementation allows processing data in multiple chunks before verifying +/// the signature, useful for large files or streaming data sources. +impl<'a> VerifyStreamingOp<'a> for OsslHmacAlgo { + /// The HMAC key type used for this verification operation. + type Key = HmacKey; + + /// The context type for streaming HMAC verification. + type Context = OsslHmacAlgoVerifyContext<'a>; + + /// Initializes a streaming HMAC verification context. + /// + /// Creates a new context that can process data incrementally via the + /// `update()` method before verifying with `finish()`. + /// + /// # Arguments + /// + /// * `key` - The HMAC key to use for verification + /// + /// # Returns + /// + /// A streaming context that implements `VerifyStreamingOpContext`. + /// + /// # Errors + /// + /// Returns `CryptoError::HmacVerifyError` if context initialization fails. + fn verify_init(self, key: Self::Key) -> Result { + use openssl::sign::Signer; + + let verifier = Signer::new(self.hash.message_digest(), key.pkey()) + .map_err(|_| CryptoError::HmacVerifyInitError)?; + + Ok(OsslHmacAlgoVerifyContext { + algo: self, + verifier, + }) + } +} + +/// Streaming context for HMAC verification operations. +/// +/// This structure maintains the state for incremental HMAC verification, +/// allowing data to be processed in chunks before verifying the signature. +/// +/// # Lifetime +/// +/// The lifetime parameter ensures the key remains valid for the duration +/// of the streaming operation. +pub struct OsslHmacAlgoVerifyContext<'a> { + /// Algorithm configuration + algo: OsslHmacAlgo, + + /// OpenSSL verifier for checking the HMAC + verifier: openssl::sign::Signer<'a>, +} + +impl<'a> VerifyStreamingOpContext<'a> for OsslHmacAlgoVerifyContext<'a> { + /// The signature algorithm type associated with this context. + type Algo = OsslHmacAlgo; + + /// Processes a chunk of data. + /// + /// Updates the internal HMAC state with the provided data. Can be called + /// multiple times before finalizing. + /// + /// # Arguments + /// + /// * `data` - Data chunk to process + /// + /// # Errors + /// + /// Returns `CryptoError::HmacVerifyError` if the update operation fails. + fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> { + self.verifier + .update(data) + .map_err(|_| CryptoError::HmacVerifyUpdateError) + } + + /// Finalizes the verification and checks the signature. + /// + /// Completes the HMAC computation and verifies it against the provided signature + /// using constant-time comparison. + /// + /// # Arguments + /// + /// * `signature` - The signature to verify + /// + /// # Returns + /// + /// `Ok(true)` if the signature is valid, `Ok(false)` if invalid. + /// + /// # Errors + /// + /// Returns `CryptoError::HmacVerifyError` if the verification operation fails. + fn finish(&mut self, signature: &[u8]) -> Result { + let mut result = vec![0u8; self.algo.hash.size()]; + + self.verifier + .sign(&mut result) + .map_err(|_| CryptoError::HmacVerifyFinishError)?; + + Ok(result == signature) + } + + /// Returns a reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslHash` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslHash` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying hash algorithm. + /// + /// # Returns + /// + /// The `OsslHash` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} diff --git a/crates/crypto/src/hmac/mod.rs b/crates/crypto/src/hmac/mod.rs index a14161434..cd2587e0f 100644 --- a/crates/crypto/src/hmac/mod.rs +++ b/crates/crypto/src/hmac/mod.rs @@ -48,6 +48,10 @@ use super::*; cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { mod key_ossl; + #[cfg(ossl300)] + mod hmac_ossl; + #[cfg(not(ossl300))] + #[path = "hmac_ossl11.rs"] mod hmac_ossl; } else if #[cfg(target_os = "windows")] { mod key_cng; diff --git a/crates/crypto/src/kdf/hkdf_ossl11.rs b/crates/crypto/src/kdf/hkdf_ossl11.rs new file mode 100644 index 000000000..e2ee62526 --- /dev/null +++ b/crates/crypto/src/kdf/hkdf_ossl11.rs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based HKDF implementation for Linux systems. +//! +//! This module provides concrete implementations of HKDF (HMAC-based Key Derivation Function) +//! operations using the OpenSSL cryptographic library. It serves as the Linux-specific backend +//! for the platform-agnostic HKDF interface defined in the parent module. +//! +//! # HKDF Overview +//! +//! HKDF is a key derivation function specified in RFC 5869 that extracts and expands +//! key material. It consists of two phases: +//! +//! - **Extract**: Derives a pseudorandom key (PRK) from input keying material (IKM) and salt +//! - **Expand**: Expands the PRK into multiple output keys using optional context information +//! +//! # Supported Modes +//! +//! - **ExtractAndExpand**: Full HKDF operation (Extract → Expand) +//! - **Extract**: Only performs Extract phase, outputs PRK +//! - **Expand**: Only performs Expand phase, requires PRK as input +//! +//! # Supported Hash Algorithms +//! +//! - **HMAC-SHA1**: Legacy algorithm (20-byte PRK, use with caution) +//! - **HMAC-SHA256**: Recommended for most applications (32-byte PRK) +//! - **HMAC-SHA384**: High security applications (48-byte PRK) +//! - **HMAC-SHA512**: Maximum security applications (64-byte PRK) +//! +//! # Platform Integration +//! +//! - Leverages OpenSSL's optimized HKDF implementations +//! - Uses system-provided OpenSSL for security updates +//! - Provides memory-safe Rust wrappers around OpenSSL APIs +//! - Zero-copy design using slice references + +use openssl::md::*; + +use super::*; + +/// OpenSSL-backed HKDF operation provider. +/// +/// This structure configures and executes HKDF (HMAC-based Key Derivation Function) +/// operations using OpenSSL's cryptographic primitives. It supports Extract-only, +/// Expand-only, and full Extract-then-Expand modes as specified in RFC 5869. +/// +/// # Lifetime Parameters +/// +/// The `'a` lifetime ensures that all borrowed data (hash reference, salt, info) +/// remains valid for the duration of the HKDF operation. This enables zero-copy +/// operation without heap allocations for these parameters. +/// +/// # Thread Safety +/// +/// This structure is `Send` and `Sync` as it only stores configuration data. +/// Actual cryptographic operations are performed through OpenSSL APIs. +/// +/// # Security +/// +/// - Uses OpenSSL's HKDF implementation following RFC 5869 +/// - Supports proper salt usage in Extract phase +/// - Allows context binding through info parameter in Expand phase +pub struct OsslHkdfAlgo<'a> { + /// Message digest context for OpenSSL + md: &'a MdRef, + /// HKDF derivation mode + mode: HkdfMode, + /// Optional salt for extract phase + salt: Option<&'a [u8]>, + /// Optional info for expand phase + info: Option<&'a [u8]>, +} + +impl<'a> OsslHkdfAlgo<'a> { + /// Creates a new HKDF operation provider from a hash instance. + /// + /// This constructor configures the HKDF provider but does not perform any + /// cryptographic operations. Actual key derivation occurs when calling + /// the `derive()` method. + /// + /// # Arguments + /// + /// * `hash` - The hash instance specifying the algorithm to use for HKDF + /// * `mode` - The HKDF mode (Extract, Expand, or ExtractAndExpand) + /// * `salt` - Optional salt for Extract phase (recommended for Extract modes) + /// * `info` - Optional context/application-specific info for Expand phase + /// * `derived_length` - Desired output length (defaults to hash output size) + /// + /// # Returns + /// + /// A new `OsslHkdf` instance configured for the specified parameters. + pub fn new( + mode: HkdfMode, + hash: &'a HashAlgo, + salt: Option<&'a [u8]>, + info: Option<&'a [u8]>, + ) -> Self { + Self { + md: hash.md(), + mode, + salt, + info, + } + } +} + +/// Converts platform-agnostic HKDF mode to OpenSSL-specific mode constant. +/// +/// This conversion enables the platform-agnostic HKDF interface to map +/// to OpenSSL's specific mode enumeration for HKDF operations. +impl From for openssl::pkey_ctx::HkdfMode { + fn from(mode: HkdfMode) -> Self { + match mode { + HkdfMode::Extract => openssl::pkey_ctx::HkdfMode::EXTRACT_ONLY, + HkdfMode::ExtractAndExpand => openssl::pkey_ctx::HkdfMode::EXTRACT_THEN_EXPAND, + HkdfMode::Expand => openssl::pkey_ctx::HkdfMode::EXPAND_ONLY, + } + } +} + +/// Implements HKDF key derivation operation. +/// +/// This implementation uses OpenSSL's HKDF functionality to derive key material +/// according to RFC 5869 specification. It supports all three derivation modes +/// and handles the complete lifecycle of the derivation operation. +impl<'a> DeriveOp for OsslHkdfAlgo<'a> { + type Key = GenericSecretKey; + type DerivedKey = GenericSecretKey; + + /// Derives key material using the HKDF algorithm. + /// + /// Depending on the configured mode, this performs: + /// - **Extract**: HMAC-Extract(salt, IKM) → PRK + /// - **Expand**: HKDF-Expand(PRK, info, L) → OKM + /// - **ExtractAndExpand**: Extract followed by Expand + /// + /// # Arguments + /// + /// * `key` - Input key material (IKM) for Extract modes, or PRK for Expand mode + /// + /// # Returns + /// + /// The derived key material as a `GenericSecretKey`: + /// - For Extract: Pseudorandom key (PRK) of hash output size + /// - For Expand/ExtractAndExpand: Output key material (OKM) of requested length + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::HkdfError` - HKDF context creation fails + /// - `CryptoError::HkdfInitError` - HKDF property setting or key setting fails + /// - `CryptoError::HkdfDeriveError` - HKDF derivation operation fails + /// - `CryptoError::InvalidKeySize` - Key material extraction fails + fn derive(&self, key: &Self::Key, derive_len: usize) -> Result { + // Extract key bytes + let key_bytes = key.to_vec()?; + + // Create and configure HKDF context + let mut ctx = openssl::pkey_ctx::PkeyCtx::new_id(openssl::pkey::Id::HKDF) + .map_err(|_| CryptoError::HkdfError)?; + self.configure_pkey_ctx(&mut ctx)?; + + // Set input keying material + ctx.set_hkdf_key(&key_bytes) + .map_err(|_| CryptoError::HkdfSetPropertyError)?; + + // Derive the key + let mut derived_key = vec![0u8; derive_len]; + let derived_size = ctx + .derive(Some(&mut derived_key)) + .map_err(|_| CryptoError::HkdfDeriveError)?; + + // Return only the actual derived bytes + GenericSecretKey::from_bytes(&derived_key[..derived_size]) + } +} + +impl<'a> OsslHkdfAlgo<'a> { + /// Configures OpenSSL PkeyCtx with HKDF parameters. + /// + /// This method sets up the OpenSSL context with the hash algorithm, salt, + /// and info parameters required for HKDF derivation. The initialization + /// sequence is critical: `derive_init()` must be called before setting + /// any HKDF-specific parameters. + /// + /// # Arguments + /// + /// * `pkey_ctx` - The OpenSSL public key context to configure + /// + /// # Returns + /// + /// `Ok(())` on successful configuration. + /// + /// # Errors + /// + /// Returns `CryptoError::HkdfInitError` if any OpenSSL property setting fails. + fn configure_pkey_ctx( + &self, + pkey_ctx: &mut openssl::pkey_ctx::PkeyCtx, + ) -> Result<(), CryptoError> { + // Call derive_init() BEFORE setting any HKDF parameters + pkey_ctx + .derive_init() + .map_err(|_| CryptoError::HkdfSetPropertyError)?; + + // Set message digest + pkey_ctx + .set_hkdf_md(self.md) + .map_err(|_| CryptoError::HkdfSetPropertyError)?; + + // Set HKDF mode + pkey_ctx + .set_hkdf_mode(self.mode.into()) + .map_err(|_| CryptoError::HkdfSetPropertyError)?; + // Set salt if provided + if let Some(salt) = self.salt { + pkey_ctx + .set_hkdf_salt(salt) + .map_err(|_| CryptoError::HkdfSetPropertyError)?; + } + + // Set info if provided + if let Some(info) = self.info { + pkey_ctx + .add_hkdf_info(info) + .map_err(|_| CryptoError::HkdfSetPropertyError)?; + } + + Ok(()) + } +} diff --git a/crates/crypto/src/kdf/mod.rs b/crates/crypto/src/kdf/mod.rs index c4b6876b3..a50e3d68a 100644 --- a/crates/crypto/src/kdf/mod.rs +++ b/crates/crypto/src/kdf/mod.rs @@ -3,6 +3,10 @@ cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { + #[cfg(ossl300)] + mod hkdf_ossl; + #[cfg(not(ossl300))] + #[path = "hkdf_ossl11.rs"] mod hkdf_ossl; } else if #[cfg(target_os = "windows")] { mod hkdf_cng; diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index 6698b5fde..22628d7cf 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -39,7 +39,7 @@ mod traits; /// Crate-private `OSSL_LIB_CTX` (default-provider-only) for the OpenSSL /// backends, so the mock SDK's crypto never re-enters the azihsm provider on /// OpenSSL 3.5. Linux-only (the Windows backends use CNG). -#[cfg(target_os = "linux")] +#[cfg(all(target_os = "linux", ossl300))] mod libctx; pub use aes::*; diff --git a/crates/crypto/src/rsa/mod.rs b/crates/crypto/src/rsa/mod.rs index 87e408447..16f111733 100644 --- a/crates/crypto/src/rsa/mod.rs +++ b/crates/crypto/src/rsa/mod.rs @@ -49,8 +49,20 @@ cfg_if::cfg_if! { if #[cfg(target_os = "linux")] { mod key_ossl; + #[cfg(ossl300)] mod rsa_enc_ossl; + #[cfg(not(ossl300))] + #[path = "rsa_enc_ossl11.rs"] + mod rsa_enc_ossl; + #[cfg(ossl300)] + mod rsa_sign_ossl; + #[cfg(not(ossl300))] + #[path = "rsa_sign_ossl11.rs"] mod rsa_sign_ossl; + #[cfg(ossl300)] + mod rsa_hash_sign_ossl; + #[cfg(not(ossl300))] + #[path = "rsa_hash_sign_ossl11.rs"] mod rsa_hash_sign_ossl; } else if #[cfg(target_os = "windows")] { mod key_cng; diff --git a/crates/crypto/src/rsa/rsa_enc_ossl11.rs b/crates/crypto/src/rsa/rsa_enc_ossl11.rs new file mode 100644 index 000000000..40bc2dd19 --- /dev/null +++ b/crates/crypto/src/rsa/rsa_enc_ossl11.rs @@ -0,0 +1,340 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! OpenSSL-based RSA encryption and decryption operations. +//! +//! This module provides RSA encryption and decryption functionality using OpenSSL +//! as the underlying cryptographic backend. It supports various padding schemes +//! including OAEP (Optimal Asymmetric Encryption Padding) for enhanced security. +//! +//! # Supported Padding Schemes +//! +//! - **OAEP**: Optimal Asymmetric Encryption Padding with configurable hash algorithms +//! - **PKCS#1 v1.5**: Legacy padding (use OAEP for new applications) +//! +//! # Security Considerations +//! +//! - Always use OAEP padding for new applications +//! - OAEP provides semantic security and protection against various attacks +//! - Choose appropriate hash algorithms (SHA-256 or stronger recommended) +//! - RSA encryption is typically used for small data (e.g., symmetric key wrapping) + +use openssl::rsa::*; + +use super::*; + +/// OpenSSL-backed RSA encryption and decryption implementation. +/// +/// This structure provides RSA encryption and decryption operations with support +/// for various padding schemes. It maintains configuration for padding mode, +/// hash algorithm selection, and optional OAEP labels. +/// +/// # Lifetime Parameter +/// +/// The lifetime parameter `'a` is used for the OAEP label, which must remain +/// valid for the duration of the encryption/decryption operation. +/// +/// # Padding Modes +/// +/// - **NONE**: No padding (use with caution) +/// - **PKCS1_OAEP**: Optimal Asymmetric Encryption Padding with hash function +/// +/// # Thread Safety +/// +/// This structure is `Send` and `Sync` as OpenSSL's RSA operations are thread-safe. +pub struct OsslRsaEncryptAlgo<'a> { + /// The padding scheme to use for encryption/decryption + padding: Padding, + /// The hash instance for OAEP padding (if applicable) + hash: Option, + /// The label for OAEP padding (optional, typically empty) + label: Option<&'a [u8]>, +} + +/// Implements RSA encryption operations using OpenSSL. +/// +/// This implementation performs RSA encryption with the configured padding scheme. +/// Encryption uses the RSA public key and produces ciphertext that can only be +/// decrypted with the corresponding private key. +impl EncryptOp for OsslRsaEncryptAlgo<'_> { + type Key = RsaPublicKey; + + /// Encrypts data using RSA with the configured padding scheme. + /// + /// This method encrypts the input data using the provided RSA public key. + /// The output buffer pattern allows querying the required buffer size before + /// performing the actual encryption. + /// + /// # Arguments + /// + /// * `key` - The RSA public key to use for encryption + /// * `input` - The plaintext data to encrypt + /// * `output` - Optional output buffer. If `None`, only calculates required size. + /// + /// # Returns + /// + /// The number of bytes written to the buffer, or the required buffer size + /// if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::RsaError` - Encrypter creation or length calculation fails + /// - `CryptoError::RsaBufferTooSmall` - Output buffer is too small + /// - `CryptoError::RsaEncryptError` - Encryption operation fails + /// + /// # Security + /// + /// - RSA encryption should only be used for small data (typically symmetric keys) + /// - Use OAEP padding for new applications + /// - Ensure the public key is authenticated to prevent substitution attacks + fn encrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + use openssl::encrypt::Encrypter; + let mut encrypter = Encrypter::new(key.pkey()).map_err(|_| CryptoError::RsaError)?; + self.configure_encrypter(&mut encrypter)?; + let len = encrypter + .encrypt_len(input) + .map_err(|_| CryptoError::RsaError)?; + if let Some(output) = output { + if output.len() < len { + return Err(CryptoError::RsaBufferTooSmall); + } + encrypter + .encrypt(input, output) + .map_err(|_| CryptoError::RsaEncryptError)?; + } + Ok(len) + } +} + +/// Implements RSA decryption operations using OpenSSL. +/// +/// This implementation performs RSA decryption with the configured padding scheme. +/// Decryption uses the RSA private key to recover the original plaintext from +/// ciphertext that was encrypted with the corresponding public key. +impl DecryptOp for OsslRsaEncryptAlgo<'_> { + type Key = RsaPrivateKey; + + /// Decrypts data using RSA with the configured padding scheme. + /// + /// This method decrypts the input ciphertext using the provided RSA private key. + /// The output buffer pattern allows querying the required buffer size before + /// performing the actual decryption. + /// + /// # Arguments + /// + /// * `key` - The RSA private key to use for decryption + /// * `input` - The ciphertext data to decrypt + /// * `output` - Optional output buffer. If `None`, only calculates required size. + /// + /// # Returns + /// + /// The number of bytes written to the buffer, or the required buffer size + /// if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::RsaError` - Decrypter creation or length calculation fails + /// - `CryptoError::RsaBufferTooSmall` - Output buffer is too small + /// - `CryptoError::RsaDecryptError` - Decryption operation fails + /// + /// # Security + /// + /// - Protect private keys from unauthorized access + /// - Use constant-time operations when possible to prevent timing attacks + /// - Validate decrypted data before use + fn decrypt( + &mut self, + key: &Self::Key, + input: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + use openssl::encrypt::Decrypter; + let mut decrypter = Decrypter::new(key.pkey()).map_err(|_| CryptoError::RsaError)?; + self.configure_decrypter(&mut decrypter)?; + let len = decrypter + .decrypt_len(input) + .map_err(|_| CryptoError::RsaError)?; + let len = if let Some(output) = output { + if output.len() < len { + return Err(CryptoError::RsaBufferTooSmall); + } + decrypter + .decrypt(input, output) + .map_err(|_| CryptoError::RsaDecryptError)? + } else { + len + }; + Ok(len) + } +} + +impl<'a> OsslRsaEncryptAlgo<'a> { + /// Creates a new RSA encryption/decryption context with default settings. + /// + /// The default configuration uses no padding. For secure encryption, + /// use `with_oaep_padding()` to configure OAEP padding with a hash algorithm. + /// + /// # Returns + /// + /// A new `OsslRsaEncryption` instance with: + /// - No padding (must be configured before use) + /// - No hash algorithm + /// - Empty label + pub fn with_no_padding() -> Self { + Self { + padding: Padding::NONE, + hash: None, + label: None, + } + } + + /// Creates a new RSA encryption/decryption context with PKCS#1 v1.5 padding. + /// + /// PKCS#1 v1.5 padding is a legacy padding scheme that should only be used + /// for compatibility with existing systems. For new applications, use OAEP + /// padding via `with_oaep_padding()` instead. + /// + /// # Returns + /// + /// A new `OsslRsaEncryption` instance configured with PKCS#1 v1.5 padding. + /// + /// # Security Warning + /// + /// PKCS#1 v1.5 padding is vulnerable to padding oracle attacks (Bleichenbacher's attack). + /// It is considered legacy and should not be used in new applications unless required + /// for compatibility with existing systems that cannot be upgraded. + pub fn with_pkcs1_padding() -> Self { + Self { + padding: Padding::PKCS1, + hash: None, + label: None, + } + } + + /// Configures OAEP padding with the specified hash algorithm and label. + /// + /// OAEP (Optimal Asymmetric Encryption Padding) provides semantic security + /// and protection against various attacks. It is the recommended padding + /// scheme for new applications. + /// + /// # Arguments + /// + /// * `hash` - The hash instance to use for OAEP (SHA-256 or stronger recommended) + /// * `label` - Optional label for OAEP (typically empty, but can be used for domain separation) + /// + /// # Returns + /// + /// The modified `OsslRsaEncryption` instance configured with OAEP padding. + /// + /// # Security + /// + /// - Use SHA-256 or stronger hash algorithms for new applications + /// - The label parameter can be used for domain separation but is typically empty + /// - OAEP provides protection against chosen-ciphertext attacks + pub fn with_oaep_padding(hash: HashAlgo, label: Option<&'a [u8]>) -> Self { + Self { + padding: Padding::PKCS1_OAEP, + hash: Some(hash), + label, + } + } + + /// Configures the OpenSSL encrypter with the specified padding parameters. + /// + /// This internal method applies the padding configuration to the OpenSSL + /// encrypter, including OAEP padding mode, hash algorithms, and label. + /// + /// # Arguments + /// + /// * `encrypter` - The OpenSSL encrypter to configure + /// + /// # Returns + /// + /// `Ok(())` if padding configuration succeeds. + /// + /// # Errors + /// + /// Returns `CryptoError::RsaSetPropertyError` if: + /// - Setting the padding mode fails + /// - Setting the OAEP hash algorithm fails + /// - Setting the MGF1 hash algorithm fails + /// - Setting the OAEP label fails + fn configure_encrypter<'b>( + &mut self, + encrypter: &mut openssl::encrypt::Encrypter<'b>, + ) -> Result<(), CryptoError> { + // Set the padding mode first, OAEP or NONE + encrypter + .set_rsa_padding(self.padding) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + + if self.padding == Padding::PKCS1_OAEP { + if let Some(hash) = &self.hash { + encrypter + .set_rsa_oaep_md(hash.message_digest()) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + encrypter + .set_rsa_mgf1_md(hash.message_digest()) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + } + if let Some(label) = self.label { + encrypter + .set_rsa_oaep_label(label) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + } + } + Ok(()) + } + + /// Configures the OpenSSL decrypter with the specified padding parameters. + /// + /// This internal method applies the padding configuration to the OpenSSL + /// decrypter, including OAEP padding mode, hash algorithms, and label. + /// + /// # Arguments + /// + /// * `decrypter` - The OpenSSL decrypter to configure + /// + /// # Returns + /// + /// `Ok(())` if padding configuration succeeds. + /// + /// # Errors + /// + /// Returns an error if: + /// - `CryptoError::RsaSetPropertyError` - Setting padding, OAEP hash algorithm, or label fails + /// - `CryptoError::RsaError` - Setting MGF1 hash algorithm fails + fn configure_decrypter<'b>( + &mut self, + decrypter: &mut openssl::encrypt::Decrypter<'b>, + ) -> Result<(), CryptoError> { + // Set the padding mode first, OAEP or NONE + decrypter + .set_rsa_padding(self.padding) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + + if self.padding == Padding::PKCS1_OAEP { + if let Some(hash) = &self.hash { + decrypter + .set_rsa_oaep_md(hash.message_digest()) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + decrypter + .set_rsa_mgf1_md(hash.message_digest()) + .map_err(|_| CryptoError::RsaError)?; + } + if let Some(label) = self.label { + decrypter + .set_rsa_oaep_label(label) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + } + } + Ok(()) + } +} diff --git a/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs new file mode 100644 index 000000000..8759e3944 --- /dev/null +++ b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs @@ -0,0 +1,426 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! RSA signature generation and verification using OpenSSL. +//! +//! This module provides RSA signing and verification operations using the OpenSSL +//! library. It supports both PKCS#1 v1.5 and PSS (Probabilistic Signature Scheme) +//! padding modes for both one-shot and streaming operations. +//! +//! # Padding Schemes +//! +//! - **PKCS#1 v1.5**: Traditional deterministic padding scheme, widely supported +//! - **PSS**: Probabilistic padding with stronger security properties, recommended for new applications +//! +//! # Operation Modes +//! +//! - **One-shot**: Sign or verify entire message in a single operation +//! - **Streaming**: Process large messages incrementally using init/update/finish pattern +//! +//! # Security Considerations +//! +//! - PSS padding is recommended over PKCS#1 v1.5 for new applications +//! - Use SHA-256 or stronger hash algorithms +//! - For PSS, salt length should typically match the hash output length +//! - PKCS#1 v1.5 is deterministic and may be vulnerable to certain attacks + +use openssl::md_ctx::*; +use openssl::pkey_ctx::*; +use openssl::rsa::*; + +use super::*; + +/// RSA signing and verification context using OpenSSL. +/// +/// This structure manages the configuration for RSA signature operations, +/// including padding scheme selection, hash algorithm, and PSS-specific parameters. +/// +/// # Padding Configuration +/// +/// The context can be configured for: +/// - **PKCS#1 v1.5**: Traditional deterministic padding +/// - **PSS**: Probabilistic signature scheme with configurable salt length +/// +/// # Trait Implementations +/// +/// - `SignOp`: One-shot signature generation +/// - `SignStreamingOp`: Streaming signature generation for large messages +/// - `VerifyOp`: One-shot signature verification +/// - `VerifyStreamingOp`: Streaming signature verification for large messages +pub struct OsslRsaHashSignAlgo { + /// The padding scheme to use (PKCS#1 or PSS). + padding: Padding, + /// The hash instance to use. + hash: HashAlgo, + /// The salt length for PSS padding (ignored for PKCS#1). + salt_len: usize, +} + +impl SignOp for OsslRsaHashSignAlgo { + type Key = RsaPrivateKey; + + /// Generates an RSA signature for the given data. + /// + /// This is a one-shot operation that signs the entire message in a single call. + /// The data is hashed using the configured hash algorithm before signing. + /// + /// # Arguments + /// + /// * `key` - The RSA private key to use for signing + /// * `data` - The message to sign + /// * `signature` - Optional buffer for the signature. If `None`, returns required size. + /// + /// # Returns + /// + /// The number of bytes written to the signature buffer, or the required buffer size + /// if `signature` is `None`. The signature size equals the key size in bytes. + fn sign( + &mut self, + key: &Self::Key, + data: &[u8], + signature: Option<&mut [u8]>, + ) -> Result { + fn len(ctx: &mut MdCtxRef, data: &[u8]) -> Result { + ctx.digest_sign(data, None) + .map_err(|_| CryptoError::RsaError) + } + + let mut ctx = MdCtx::new().map_err(|_| CryptoError::RsaError)?; + let pkey_ctx = ctx + .digest_sign_init(Some(self.hash.md()), key.pkey()) + .map_err(|_| CryptoError::RsaError)?; + self.configure_pkey_ctx(pkey_ctx)?; + + let sig_len = len(&mut ctx, data)?; + + if let Some(signature) = signature { + if signature.len() < sig_len { + return Err(CryptoError::RsaBufferTooSmall); + } + let len = ctx + .digest_sign(data, Some(&mut signature[..sig_len])) + .map_err(|_| CryptoError::RsaSignError)?; + return Ok(len); + } + + Ok(sig_len) + } +} + +impl<'a> SignStreamingOp<'a> for OsslRsaHashSignAlgo { + type Key = RsaPrivateKey; + type Context = OsslRsaHashSignAlgoSignContext; + + /// Initializes a streaming signature operation. + /// + /// Creates a signing context that can process data incrementally using + /// the update/finish pattern. Useful for signing large messages that + /// don't fit in memory. + /// + /// # Arguments + /// + /// * `key` - The RSA private key to use for signing + /// + /// # Returns + /// + /// A streaming context that can be updated with message data and finalized. + fn sign_init(self, key: Self::Key) -> Result { + let mut ctx = MdCtx::new().map_err(|_| CryptoError::RsaError)?; + let pkey_ctx = ctx + .digest_sign_init(Some(self.hash.md()), key.pkey()) + .map_err(|_| CryptoError::RsaError)?; + self.configure_pkey_ctx(pkey_ctx)?; + Ok(OsslRsaHashSignAlgoSignContext { algo: self, ctx }) + } +} + +/// Streaming context for RSA signature generation. +/// +/// This context manages the incremental hashing and signature generation process. +/// Data can be added using `update()` and the signature finalized with `finish()`. +pub struct OsslRsaHashSignAlgoSignContext { + algo: OsslRsaHashSignAlgo, + ctx: MdCtx, +} + +impl<'a> SignStreamingOpContext<'a> for OsslRsaHashSignAlgoSignContext { + type Algo = OsslRsaHashSignAlgo; + /// Adds more data to the message being signed. + /// + /// Can be called multiple times to process the message incrementally. + /// + /// # Arguments + /// + /// * `data` - The next chunk of message data to include in the signature + fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> { + self.ctx + .digest_sign_update(data) + .map_err(|_| CryptoError::RsaSignUpdateError) + } + + /// Finalizes the signature generation. + /// + /// Completes the hashing process and generates the RSA signature. + /// + /// # Arguments + /// + /// * `signature` - Optional buffer for the signature. If `None`, returns required size. + /// + /// # Returns + /// + /// The number of bytes written to the signature buffer, or the required buffer size. + fn finish(&mut self, signature: Option<&mut [u8]>) -> Result { + fn len(ctx: &mut MdCtxRef) -> Result { + ctx.digest_sign_final(None) + .map_err(|_| CryptoError::RsaError) + } + + let sig_len = len(&mut self.ctx)?; + if let Some(signature) = signature { + if signature.len() < sig_len { + return Err(CryptoError::RsaBufferTooSmall); + } + let len = self + .ctx + .digest_sign_final(Some(&mut signature[..sig_len])) + .map_err(|_| CryptoError::RsaSignFinishError)?; + return Ok(len); + } + + Ok(sig_len) + } + + /// Returns a reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslHash` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslHash` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying hash algorithm. + /// + /// # Returns + /// + /// The `OsslHash` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} + +impl VerifyOp for OsslRsaHashSignAlgo { + type Key = RsaPublicKey; + + /// Verifies an RSA signature for the given data. + /// + /// This is a one-shot operation that verifies the signature against the entire + /// message in a single call. The data is hashed using the configured hash algorithm. + /// + /// # Arguments + /// + /// * `key` - The RSA public key to use for verification + /// * `data` - The message that was signed + /// * `signature` - The signature to verify + /// + /// # Returns + /// + /// `true` if the signature is valid, `false` if invalid. + fn verify( + &mut self, + key: &Self::Key, + data: &[u8], + signature: &[u8], + ) -> Result { + let mut ctx = MdCtx::new().map_err(|_| CryptoError::RsaError)?; + let pkey_ctx = ctx + .digest_verify_init(Some(self.hash.md()), key.pkey()) + .map_err(|_| CryptoError::RsaError)?; + self.configure_pkey_ctx(pkey_ctx)?; + ctx.digest_verify(data, signature) + .map_err(|_| CryptoError::RsaVerifyError) + } +} + +impl<'a> VerifyStreamingOp<'a> for OsslRsaHashSignAlgo { + type Key = RsaPublicKey; + type Context = OsslRsaHashSignAlgoVerifyContext; + + /// Initializes a streaming verification operation. + /// + /// Creates a verification context that can process data incrementally using + /// the update/finish pattern. Useful for verifying signatures on large messages. + /// + /// # Arguments + /// + /// * `key` - The RSA public key to use for verification + /// + /// # Returns + /// + /// A streaming context that can be updated with message data and finalized. + fn verify_init(self, key: Self::Key) -> Result { + let mut ctx = MdCtx::new().map_err(|_| CryptoError::RsaError)?; + let pkey_ctx = ctx + .digest_verify_init(Some(self.hash.md()), key.pkey()) + .map_err(|_| CryptoError::RsaError)?; + self.configure_pkey_ctx(pkey_ctx)?; + Ok(OsslRsaHashSignAlgoVerifyContext { + algo: self, + md_ctx: ctx, + }) + } +} + +/// Streaming context for RSA signature verification. +/// +/// This context manages the incremental hashing and signature verification process. +/// Data can be added using `update()` and the verification finalized with `finish()`. +pub struct OsslRsaHashSignAlgoVerifyContext { + /// The underlying signing algorithm. + algo: OsslRsaHashSignAlgo, + md_ctx: MdCtx, +} + +impl<'a> VerifyStreamingOpContext<'a> for OsslRsaHashSignAlgoVerifyContext { + type Algo = OsslRsaHashSignAlgo; + /// Adds more data to the message being verified. + /// + /// Can be called multiple times to process the message incrementally. + /// + /// # Arguments + /// + /// * `data` - The next chunk of message data to include in the verification + fn update(&mut self, data: &[u8]) -> Result<(), CryptoError> { + self.md_ctx + .digest_verify_update(data) + .map_err(|_| CryptoError::RsaVerifyUpdateError) + } + + /// Finalizes the signature verification. + /// + /// Completes the hashing process and verifies the RSA signature. + /// + /// # Arguments + /// + /// * `signature` - The signature to verify against the accumulated message data + /// + /// # Returns + /// + /// `true` if the signature is valid, `false` if invalid. + fn finish(&mut self, signature: &[u8]) -> Result { + self.md_ctx + .digest_verify_final(signature) + .map_err(|_| CryptoError::RsaVerifyFinishError) + } + + /// Returns a reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A reference to the `OsslHash` algorithm instance. + fn algo(&self) -> &Self::Algo { + &self.algo + } + + /// Returns a mutable reference to the underlying hash algorithm. + /// + /// # Returns + /// + /// A mutable reference to the `OsslHash` algorithm instance. + fn algo_mut(&mut self) -> &mut Self::Algo { + &mut self.algo + } + + /// Consumes the context and returns the underlying hash algorithm. + /// + /// # Returns + /// + /// The `OsslHash` algorithm instance. + fn into_algo(self) -> Self::Algo { + self.algo + } +} + +impl OsslRsaHashSignAlgo { + /// Creates a new RSA signing operation with PKCS#1 v1.5 padding. + /// + /// PKCS#1 v1.5 is the traditional RSA signature padding scheme. It is deterministic + /// and widely supported but has weaker security properties than PSS. + /// + /// # Arguments + /// + /// * `hash` - The hash instance to use (SHA-256 or stronger recommended) + /// + /// # Returns + /// + /// A new `OsslRsaSigning` instance configured for PKCS#1 v1.5 padding. + /// + /// # Security Considerations + /// + /// - PKCS#1 v1.5 is deterministic, which can be a security concern in some contexts + /// - Consider using PSS padding for new applications + /// - Use SHA-256 or stronger hash algorithms + pub fn with_pkcs1_padding(hash: HashAlgo) -> Self { + Self { + padding: Padding::PKCS1, + hash, + salt_len: 0, + } + } + + /// Creates a new RSA signing operation with PSS padding. + /// + /// PSS (Probabilistic Signature Scheme) is a randomized padding scheme with + /// stronger security properties than PKCS#1 v1.5. It is recommended for new applications. + /// + /// # Arguments + /// + /// * `hash_algo` - The hash algorithm to use (SHA-256 or stronger recommended) + /// * `salt_len` - The salt length in bytes (typically matches hash output length) + /// + /// # Returns + /// + /// A new `OsslRsaSigning` instance configured for PSS padding. + /// + /// # Security Considerations + /// + /// - PSS provides stronger security guarantees than PKCS#1 v1.5 + /// - Salt length typically matches the hash output length for optimal security + /// - PSS is randomized, providing better protection against certain attacks + /// - Use SHA-256 or stronger hash algorithms + pub fn with_pss_padding(hash: HashAlgo, salt_len: usize) -> Self { + Self { + padding: Padding::PKCS1_PSS, + hash, + salt_len, + } + } + + /// Configures the OpenSSL signer with the appropriate padding and parameters. + /// + /// Sets the padding mode and, for PSS, configures the salt length and MGF1 hash algorithm. + /// + /// # Arguments + /// + /// * `signer` - The OpenSSL signer to configure + fn configure_pkey_ctx(&self, pkey: &mut PkeyCtxRef) -> Result<(), CryptoError> { + pkey.set_rsa_padding(self.padding) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + if self.padding == Padding::PKCS1_PSS { + pkey.set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(self.salt_len as i32)) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + pkey.set_rsa_mgf1_md(self.hash.md()) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + } + Ok(()) + } +} diff --git a/crates/crypto/src/rsa/rsa_sign_ossl11.rs b/crates/crypto/src/rsa/rsa_sign_ossl11.rs new file mode 100644 index 000000000..e2e659cfb --- /dev/null +++ b/crates/crypto/src/rsa/rsa_sign_ossl11.rs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! RSA signature generation and verification with pre-hashed data using OpenSSL. +//! +//! This module provides RSA signing and verification operations for pre-hashed digests +//! using the OpenSSL library. It supports both PKCS#1 v1.5 and PSS (Probabilistic +//! Signature Scheme) padding modes. +//! +//! **Note**: This module operates on message digests (hashes), not raw message data. +//! The caller must hash the message before passing it to the sign/verify operations. +//! +//! # Padding Schemes +//! +//! - **PKCS#1 v1.5**: Traditional deterministic padding scheme, widely supported +//! - **PSS**: Probabilistic padding with stronger security properties, recommended for new applications +//! +//! # Security Considerations +//! +//! - PSS padding is recommended over PKCS#1 v1.5 for new applications +//! - Use SHA-256 or stronger hash algorithms for message digests +//! - For PSS, salt length should typically match the hash output length +//! - PKCS#1 v1.5 is deterministic and may be vulnerable to certain attacks +//! - Always hash messages before signing (this module expects pre-computed digests) + +use openssl::pkey_ctx::*; +use openssl::rsa::*; + +use super::*; + +/// RSA signing and verification context for pre-hashed data using OpenSSL. +/// +/// This structure manages the configuration for RSA signature operations on message +/// digests (hashes), including padding scheme selection, hash algorithm identification, +/// and PSS-specific parameters. +/// +/// **Important**: This context operates on pre-computed message digests, not raw messages. +/// The caller must hash the message using the appropriate hash algorithm before calling +/// sign or verify operations. +/// +/// # Padding Configuration +/// +/// The context can be configured for: +/// - **PKCS#1 v1.5**: Traditional deterministic padding for digests +/// - **PSS**: Probabilistic signature scheme with configurable salt length +/// +/// # Trait Implementations +/// +/// - `SignOp`: Signs a pre-computed message digest +/// - `VerifyOp`: Verifies a signature against a pre-computed message digest +pub struct OsslRsaSignAlgo { + /// The padding scheme to use (PKCS#1 or PSS). + padding: Padding, + /// The hash instance to use. + hash: Option, + /// The salt length for PSS padding (ignored for PKCS#1). + salt_len: usize, +} + +impl SignOp for OsslRsaSignAlgo { + type Key = RsaPrivateKey; + + /// Generates an RSA signature for a pre-hashed message digest. + /// + /// This operation signs a message digest (hash) that has already been computed + /// by the caller. The digest size must match the output size of the hash algorithm + /// configured for this signing context. + /// + /// # Arguments + /// + /// * `key` - The RSA private key to use for signing + /// * `data` - The pre-computed message digest (hash output) + /// * `signature` - Optional buffer for the signature. If `None`, returns required size. + /// + /// # Returns + /// + /// The number of bytes written to the signature buffer, or the required buffer size + /// if `signature` is `None`. The signature size equals the key size in bytes. + /// + /// # Errors + /// + /// Returns `CryptoError::RsaSignError` if: + /// - The digest size doesn't match the expected hash output size + /// - The OpenSSL signing operation fails + /// - The signature buffer is too small + fn sign( + &mut self, + key: &Self::Key, + data: &[u8], + signature: Option<&mut [u8]>, + ) -> Result { + let mut pkey_ctx = PkeyCtx::new(key.pkey()).map_err(|_| CryptoError::RsaError)?; + pkey_ctx + .sign_init() + .map_err(|_| CryptoError::RsaSignError)?; + self.configure_pkey_ctx(&mut pkey_ctx)?; + let len = pkey_ctx + .sign(data, signature) + .map_err(|_| CryptoError::RsaSignError)?; + Ok(len) + } +} + +impl VerifyOp for OsslRsaSignAlgo { + type Key = RsaPublicKey; + + /// Verifies an RSA signature against a pre-computed message digest. + /// + /// This operation verifies that a signature is valid for a given message digest (hash) + /// that has already been computed by the caller. The digest must be computed using + /// the same hash algorithm configured for this verification context. + /// + /// # Arguments + /// + /// * `key` - The RSA public key to use for verification + /// * `data` - The pre-computed message digest (hash output) + /// * `signature` - The signature to verify + /// + /// # Returns + /// + /// `true` if the signature is valid for the given digest, `false` otherwise. + /// + /// # Errors + /// + /// Returns an error only for setup/configuration failures before the final + /// OpenSSL verify step (for example context creation, `verify_init`, or + /// padding/hash configuration). + /// + /// Any error from the final OpenSSL `verify` call is treated as an invalid + /// signature and returns `Ok(false)` (fail-closed). + fn verify( + &mut self, + key: &Self::Key, + data: &[u8], + signature: &[u8], + ) -> Result { + let mut pkey_ctx = PkeyCtx::new(key.pkey()).map_err(|_| CryptoError::RsaError)?; + pkey_ctx + .verify_init() + .map_err(|_| CryptoError::RsaVerifyError)?; + self.configure_pkey_ctx(&mut pkey_ctx)?; + // After successful setup, OpenSSL may report an invalid RSA signature + // either as Ok(false) or, in some cases/platforms, by pushing an error + // onto its stack. All operational failure modes (allocation, init, + // configuration) have already been handled above, so treat any error + // from the final verify step as an invalid signature (fail-closed). + match pkey_ctx.verify(data, signature) { + Ok(valid) => Ok(valid), + Err(_) => Ok(false), + } + } +} + +impl VerifyRecoverOp for OsslRsaSignAlgo { + type Key = RsaPublicKey; + + /// Verifies an RSA signature and recovers the signed message digest. + /// + /// This operation verifies a signature and recovers the original message digest + /// (hash) that was signed. The recovered digest must match the expected hash output + /// size for the configured hash algorithm. + /// + /// # Arguments + /// + /// * `key` - The RSA public key to use for verification + /// * `signature` - The signature to verify and recover from + /// * `output` - Optional buffer to receive the recovered digest. If `None`, only calculates required size. + /// + /// # Returns + /// + /// The number of bytes written to the output buffer, or the required buffer size + /// if `output` is `None`. + /// + /// # Errors + /// + /// Returns an error if: + /// - The OpenSSL verification or recovery operation fails + /// - The output buffer is too small + fn verify_recover( + &mut self, + key: &Self::Key, + signature: &[u8], + output: Option<&mut [u8]>, + ) -> Result { + let mut pkey_ctx = PkeyCtx::new(key.pkey()).map_err(|_| CryptoError::RsaError)?; + pkey_ctx + .verify_recover_init() + .map_err(|_| CryptoError::RsaVerifyError)?; + self.configure_pkey_ctx(&mut pkey_ctx)?; + let len = pkey_ctx + .verify_recover(signature, output) + .map_err(|_| CryptoError::RsaVerifyError)?; + Ok(len) + } +} + +impl OsslRsaSignAlgo { + /// Creates a new RSA signing operation with no padding. + /// + /// This is a low-level operation that performs raw RSA signing without any padding + /// or hashing. It should only be used when implementing custom padding schemes or + /// for specific cryptographic protocols. + /// + /// # Security Warning + /// + /// Raw RSA operations without padding are vulnerable to various attacks and should + /// not be used for general-purpose signing. Use PKCS#1 or PSS padding instead. + /// + /// # Returns + /// + /// A new signing context configured for raw RSA operations. + pub fn with_no_padding() -> Self { + Self { + padding: Padding::NONE, + hash: None, + salt_len: 0, + } + } + /// Creates a new RSA signing operation with PKCS#1 v1.5 padding. + /// + /// PKCS#1 v1.5 is the traditional RSA signature padding scheme. It is deterministic + /// and widely supported but has weaker security properties than PSS. + /// + /// # Arguments + /// + /// * `hash` - The hash instance to use (SHA-256 or stronger recommended) + /// + /// # Returns + /// + /// A new `OsslRsaSigning` instance configured for PKCS#1 v1.5 padding. + /// + /// # Security Considerations + /// + /// - PKCS#1 v1.5 is deterministic, which can be a security concern in some contexts + /// - Consider using PSS padding for new applications + /// - Use SHA-256 or stronger hash algorithms + pub fn with_pkcs1_padding(hash: HashAlgo) -> Self { + Self { + padding: Padding::PKCS1, + hash: Some(hash), + salt_len: 0, + } + } + + /// Creates a new RSA signing operation with PSS padding. + /// + /// PSS (Probabilistic Signature Scheme) is a randomized padding scheme with + /// stronger security properties than PKCS#1 v1.5. It is recommended for new applications. + /// + /// # Arguments + /// + /// * `hash` - The hash instance to use (SHA-256 or stronger recommended) + /// * `salt_len` - The salt length in bytes (typically matches hash output length) + /// + /// # Returns + /// + /// A new `OsslRsaSigning` instance configured for PSS padding. + /// + /// # Security Considerations + /// + /// - PSS provides stronger security guarantees than PKCS#1 v1.5 + /// - Salt length typically matches the hash output length for optimal security + /// - PSS is randomized, providing better protection against certain attacks + /// - Use SHA-256 or stronger hash algorithms + pub fn with_pss_padding(hash: HashAlgo, salt_len: usize) -> Self { + Self { + padding: Padding::PKCS1_PSS, + hash: Some(hash), + salt_len, + } + } + + /// Configures the OpenSSL signer with the appropriate padding and parameters. + /// + /// Sets the padding mode and, for PSS, configures the salt length and MGF1 hash algorithm. + /// + /// # Arguments + /// + /// * `signer` - The OpenSSL signer to configure + fn configure_pkey_ctx(&self, pkey_ctx: &mut PkeyCtx) -> Result<(), CryptoError> { + pkey_ctx + .set_rsa_padding(self.padding) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + + if let Some(hash) = &self.hash { + pkey_ctx + .set_signature_md(hash.md()) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + + if self.padding == Padding::PKCS1_PSS { + pkey_ctx + .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(self.salt_len as i32)) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + pkey_ctx + .set_rsa_mgf1_md(hash.md()) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + } + } + + Ok(()) + } +} From 535cf486f09890d72934a646c628d7b513796a00 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Wed, 1 Jul 2026 01:10:41 +0200 Subject: [PATCH 02/10] fix(crypto): correct two OpenSSL 1.1.x backend behaviors - AES-ECB: move set_padding(false) after encrypt_init. EVP_*Init resets padding to its default (on); OpenSSL 1.1 honors that reset, so the pre-init call was discarded and a block-aligned input got a spurious PKCS#7 block (wrong, over-long ciphertext). Also fixes the AES-KW/KWP and RSA-AES-KW paths, which build on ECB. - HKDF: reject a zero-length output (HkdfDeriveError) and a mismatched Extract-mode length (HmacInvalidDerivedKeyLength), matching the 3.x / CNG backends; the 1.1 EVP_PKEY_HKDF path accepted both silently. The 1.1.x crypto test suite now passes 477/477 (was 458/19); 3.x is unchanged (477/477). Signed-off-by: Christian Walter --- crates/crypto/src/aes/ecb_ossl11.rs | 4 +++- crates/crypto/src/kdf/hkdf_ossl11.rs | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/crypto/src/aes/ecb_ossl11.rs b/crates/crypto/src/aes/ecb_ossl11.rs index 9d49e2bc8..8db35a2ac 100644 --- a/crates/crypto/src/aes/ecb_ossl11.rs +++ b/crates/crypto/src/aes/ecb_ossl11.rs @@ -120,9 +120,11 @@ impl EncryptOp for OsslAesEcbAlgo { return Err(CryptoError::AesBufferTooSmall); } let mut ctx = CipherCtx::new().map_err(|_| CryptoError::AesEncryptError)?; - ctx.set_padding(false); ctx.encrypt_init(Some(cipher), Some(key_bytes), None) .map_err(|_| CryptoError::AesEncryptError)?; + // After init: EVP_*Init resets padding to its default (on), which + // OpenSSL 1.1 honors, so disable padding last. + ctx.set_padding(false); let mut count = ctx .cipher_update(input, Some(output)) .map_err(|_| CryptoError::AesEncryptError)?; diff --git a/crates/crypto/src/kdf/hkdf_ossl11.rs b/crates/crypto/src/kdf/hkdf_ossl11.rs index e2ee62526..811714d02 100644 --- a/crates/crypto/src/kdf/hkdf_ossl11.rs +++ b/crates/crypto/src/kdf/hkdf_ossl11.rs @@ -156,6 +156,17 @@ impl<'a> DeriveOp for OsslHkdfAlgo<'a> { // Extract key bytes let key_bytes = key.to_vec()?; + // Reject a zero-length output (invalid per RFC 5869); the 3.x backend + // errors on this too. + if derive_len == 0 { + return Err(CryptoError::HkdfDeriveError); + } + // Extract-only yields the PRK (one digest block); reject a mismatched + // requested length so behavior matches the 3.x / CNG backends. + if matches!(self.mode, HkdfMode::Extract) && derive_len != self.md.size() { + return Err(CryptoError::HmacInvalidDerivedKeyLength); + } + // Create and configure HKDF context let mut ctx = openssl::pkey_ctx::PkeyCtx::new_id(openssl::pkey::Id::HKDF) .map_err(|_| CryptoError::HkdfError)?; From 22e2d04c355c97a7f848bdb36dc49474e1190a08 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Wed, 1 Jul 2026 14:35:54 +0200 Subject: [PATCH 03/10] ci(engine-matrix): run azihsm_crypto tests against OpenSSL 1.1.x The engine build only compile-checks the crate's 1.1.x crypto backends; run its test suite in the 1.1.1f cell so the 1.1 paths are exercised at runtime, not just compiled. Signed-off-by: Christian Walter --- .github/workflows/engine-matrix.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/engine-matrix.yml b/.github/workflows/engine-matrix.yml index d1c622432..255dfc5a3 100644 --- a/.github/workflows/engine-matrix.yml +++ b/.github/workflows/engine-matrix.yml @@ -108,5 +108,8 @@ jobs: - name: Test openssl-engine run: cargo test -p openssl-engine --features engine + - name: Test azihsm_crypto + run: cargo test -p azihsm_crypto + - name: Load engine run: openssl engine -t "$(pwd)/target/debug/libazihsm_engine.so" From 088c5ed9da134ced762ca3bc9364a39c90ce19a7 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Wed, 1 Jul 2026 22:08:50 +0200 Subject: [PATCH 04/10] fix(crypto): harden 1.1.x backends to match the 3.x guards The restored 1.1.x backends predate three defensive checks the 3.x backends already carry: - HMAC verify: compare with openssl::memcmp::eq after a length check instead of `==`, so verification is constant-time (the module docs already promise this). - AES-CBC: only advance the chaining IV when a full block was produced; an empty input with padding disabled yields count == 0 (encrypt) or a short input (decrypt), which would underflow the slice index and panic. - RSA-PSS: convert the salt length with i32::try_from and error on overflow instead of truncating with `as`. 3.x is unchanged. Signed-off-by: Christian Walter --- crates/crypto/src/aes/cbc_ossl11.rs | 12 ++++++++++-- crates/crypto/src/hmac/hmac_ossl11.rs | 4 ++-- crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs | 6 +++++- crates/crypto/src/rsa/rsa_sign_ossl11.rs | 6 +++++- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/crates/crypto/src/aes/cbc_ossl11.rs b/crates/crypto/src/aes/cbc_ossl11.rs index 93690b444..d7dc92a06 100644 --- a/crates/crypto/src/aes/cbc_ossl11.rs +++ b/crates/crypto/src/aes/cbc_ossl11.rs @@ -214,7 +214,11 @@ impl EncryptOp for OsslAesCbcAlgo { count += crypter .finalize(&mut output[count..]) .map_err(|_| CryptoError::AesEncryptError)?; - iv.copy_from_slice(&output[count - iv.len()..count]); + // Only advance the chaining IV when a full block was produced; + // empty input with padding off gives count == 0 and would underflow. + if count >= iv.len() { + iv.copy_from_slice(&output[count - iv.len()..count]); + } } else { // The required output buffer size for OpenSSL's `update` is // `input.len() + block_size` regardless of whether padding is enabled. @@ -474,7 +478,11 @@ impl DecryptOp for OsslAesCbcAlgo { count += crypter .finalize(&mut output[count..]) .map_err(|_| CryptoError::AesDecryptError)?; - iv.copy_from_slice(&input[input.len() - iv.len()..]); + // Only advance the chaining IV when there is a full block; an empty + // ciphertext would underflow input.len() - iv.len(). + if input.len() >= iv.len() { + iv.copy_from_slice(&input[input.len() - iv.len()..]); + } } else { count = input.len() + cipher.block_size(); } diff --git a/crates/crypto/src/hmac/hmac_ossl11.rs b/crates/crypto/src/hmac/hmac_ossl11.rs index 48eefcd33..b73207915 100644 --- a/crates/crypto/src/hmac/hmac_ossl11.rs +++ b/crates/crypto/src/hmac/hmac_ossl11.rs @@ -313,7 +313,7 @@ impl VerifyOp for OsslHmacAlgo { .sign_oneshot(&mut result, data) .map_err(|_| CryptoError::HmacVerifyError)?; - Ok(result == signature) + Ok(result.len() == signature.len() && openssl::memcmp::eq(&result, signature)) } } @@ -419,7 +419,7 @@ impl<'a> VerifyStreamingOpContext<'a> for OsslHmacAlgoVerifyContext<'a> { .sign(&mut result) .map_err(|_| CryptoError::HmacVerifyFinishError)?; - Ok(result == signature) + Ok(result.len() == signature.len() && openssl::memcmp::eq(&result, signature)) } /// Returns a reference to the underlying hash algorithm. diff --git a/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs index 8759e3944..a1038eb53 100644 --- a/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs @@ -416,7 +416,11 @@ impl OsslRsaHashSignAlgo { pkey.set_rsa_padding(self.padding) .map_err(|_| CryptoError::RsaSetPropertyError)?; if self.padding == Padding::PKCS1_PSS { - pkey.set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(self.salt_len as i32)) + // RsaPssSaltlen::custom takes an i32; reject lengths that don't fit + // rather than truncating with `as`. + let saltlen = + i32::try_from(self.salt_len).map_err(|_| CryptoError::RsaSetPropertyError)?; + pkey.set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(saltlen)) .map_err(|_| CryptoError::RsaSetPropertyError)?; pkey.set_rsa_mgf1_md(self.hash.md()) .map_err(|_| CryptoError::RsaSetPropertyError)?; diff --git a/crates/crypto/src/rsa/rsa_sign_ossl11.rs b/crates/crypto/src/rsa/rsa_sign_ossl11.rs index e2e659cfb..0c7f25a2e 100644 --- a/crates/crypto/src/rsa/rsa_sign_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_sign_ossl11.rs @@ -288,8 +288,12 @@ impl OsslRsaSignAlgo { .map_err(|_| CryptoError::RsaSetPropertyError)?; if self.padding == Padding::PKCS1_PSS { + // RsaPssSaltlen::custom takes an i32; reject lengths that don't + // fit rather than truncating with `as`. + let saltlen = + i32::try_from(self.salt_len).map_err(|_| CryptoError::RsaSetPropertyError)?; pkey_ctx - .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(self.salt_len as i32)) + .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(saltlen)) .map_err(|_| CryptoError::RsaSetPropertyError)?; pkey_ctx .set_rsa_mgf1_md(hash.md()) From 3df643e0ab5ad1e42a250127c4cf3f92d03155a1 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Thu, 2 Jul 2026 10:26:13 +0200 Subject: [PATCH 05/10] ci(engine-matrix): run 1.1.x tests via cargo nextest Install cargo-nextest from its prebuilt binary (building it from source is unreliable in the 20.04 cell) and run the openssl-engine and azihsm_crypto suites through `cargo nextest run`, matching the runner the 3.x CI already uses. nextest skips doctests, so run azihsm_crypto's doctests in a separate step. Signed-off-by: Christian Walter --- .github/workflows/engine-matrix.yml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/engine-matrix.yml b/.github/workflows/engine-matrix.yml index 255dfc5a3..b29b3c70a 100644 --- a/.github/workflows/engine-matrix.yml +++ b/.github/workflows/engine-matrix.yml @@ -102,14 +102,23 @@ jobs: cargo clippy -p azihsm_engine -p openssl-engine -p openssl-sys-engine \ --features engine --all-targets -- -D warnings + # cargo-nextest isn't in the base image and building it from source is + # unreliable here; fetch the prebuilt binary. + - name: Install cargo-nextest + run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C "${CARGO_HOME:-$HOME/.cargo}/bin" + # openssl-engine's unit tests (error queue, ex_data, set_destroy) depend # only on the 1.1 FFI, so they run here. The azihsm_engine mock lifecycle # tests pull the OpenSSL-3.x SDK and need separate handling (roadmap #19). - name: Test openssl-engine - run: cargo test -p openssl-engine --features engine + run: cargo nextest run -p openssl-engine --features engine - name: Test azihsm_crypto - run: cargo test -p azihsm_crypto + run: cargo nextest run -p azihsm_crypto + + # nextest does not run doctests; run azihsm_crypto's separately. + - name: Doctests + run: cargo test --doc -p azihsm_crypto - name: Load engine run: openssl engine -t "$(pwd)/target/debug/libazihsm_engine.so" From a35982127013decd24fa8ca65fb5d5d0fc486507 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Thu, 2 Jul 2026 20:22:34 +0200 Subject: [PATCH 06/10] fix(crypto): tighten three 1.1.x backend details - HKDF: error if OpenSSL returns fewer bytes than requested instead of returning a silently truncated key; the 3.x backend already yields the full requested length. - RSA-OAEP decrypt: map set_rsa_mgf1_md failure to RsaSetPropertyError to match the surrounding OAEP setup calls and the documented error. - Fix a with_pss_padding doc comment that named the parameter hash_algo instead of hash. Signed-off-by: Christian Walter --- crates/crypto/src/kdf/hkdf_ossl11.rs | 8 ++++++-- crates/crypto/src/rsa/rsa_enc_ossl11.rs | 2 +- crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/crypto/src/kdf/hkdf_ossl11.rs b/crates/crypto/src/kdf/hkdf_ossl11.rs index 811714d02..bb1ffdf93 100644 --- a/crates/crypto/src/kdf/hkdf_ossl11.rs +++ b/crates/crypto/src/kdf/hkdf_ossl11.rs @@ -182,8 +182,12 @@ impl<'a> DeriveOp for OsslHkdfAlgo<'a> { .derive(Some(&mut derived_key)) .map_err(|_| CryptoError::HkdfDeriveError)?; - // Return only the actual derived bytes - GenericSecretKey::from_bytes(&derived_key[..derived_size]) + // OpenSSL must fill the whole buffer; a short read would otherwise be + // returned as a silently truncated key. + if derived_size != derive_len { + return Err(CryptoError::HkdfDeriveError); + } + GenericSecretKey::from_bytes(&derived_key) } } diff --git a/crates/crypto/src/rsa/rsa_enc_ossl11.rs b/crates/crypto/src/rsa/rsa_enc_ossl11.rs index 40bc2dd19..bf64ec188 100644 --- a/crates/crypto/src/rsa/rsa_enc_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_enc_ossl11.rs @@ -327,7 +327,7 @@ impl<'a> OsslRsaEncryptAlgo<'a> { .map_err(|_| CryptoError::RsaSetPropertyError)?; decrypter .set_rsa_mgf1_md(hash.message_digest()) - .map_err(|_| CryptoError::RsaError)?; + .map_err(|_| CryptoError::RsaSetPropertyError)?; } if let Some(label) = self.label { decrypter diff --git a/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs index a1038eb53..ba7666343 100644 --- a/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs @@ -384,7 +384,7 @@ impl OsslRsaHashSignAlgo { /// /// # Arguments /// - /// * `hash_algo` - The hash algorithm to use (SHA-256 or stronger recommended) + /// * `hash` - The hash algorithm to use (SHA-256 or stronger recommended) /// * `salt_len` - The salt length in bytes (typically matches hash output length) /// /// # Returns From e2b63fb8b3739813dfa6ec0b4be08826ad13abf9 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Thu, 2 Jul 2026 20:43:15 +0200 Subject: [PATCH 07/10] docs(crypto): fix stale 1.1.x doc references; pin nextest install - Correct doc comments naming renamed types: OsslRsaSigning -> OsslRsaSignAlgo / OsslRsaHashSignAlgo, OsslHkdf -> OsslHkdfAlgo, and an HMAC module doc describing an OsslHmacKey wrapper / OsslHmacAlgo trait the backend doesn't use (it drives an OpenSSL Signer). - Drop the nonexistent `derived_length` argument from the HKDF new() docs and order the argument list to match the signature. - Pin the engine-matrix cargo-nextest download to 0.9.132 (the version xtask installs) rather than `latest`, for a deterministic install. Signed-off-by: Christian Walter --- .github/workflows/engine-matrix.yml | 5 +++-- crates/crypto/src/hmac/hmac_ossl11.rs | 6 +++--- crates/crypto/src/kdf/hkdf_ossl11.rs | 5 ++--- crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs | 4 ++-- crates/crypto/src/rsa/rsa_sign_ossl11.rs | 4 ++-- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/engine-matrix.yml b/.github/workflows/engine-matrix.yml index b29b3c70a..e43d71f65 100644 --- a/.github/workflows/engine-matrix.yml +++ b/.github/workflows/engine-matrix.yml @@ -103,9 +103,10 @@ jobs: --features engine --all-targets -- -D warnings # cargo-nextest isn't in the base image and building it from source is - # unreliable here; fetch the prebuilt binary. + # unreliable here; fetch the prebuilt binary, pinned to the version xtask + # installs elsewhere (xtask/src/setup.rs CARGO_NEXTEST_VERSION). - name: Install cargo-nextest - run: curl -LsSf https://get.nexte.st/latest/linux | tar zxf - -C "${CARGO_HOME:-$HOME/.cargo}/bin" + run: curl -LsSf https://get.nexte.st/0.9.132/linux | tar zxf - -C "${CARGO_HOME:-$HOME/.cargo}/bin" # openssl-engine's unit tests (error queue, ex_data, set_destroy) depend # only on the 1.1 FFI, so they run here. The azihsm_engine mock lifecycle diff --git a/crates/crypto/src/hmac/hmac_ossl11.rs b/crates/crypto/src/hmac/hmac_ossl11.rs index b73207915..ea463559a 100644 --- a/crates/crypto/src/hmac/hmac_ossl11.rs +++ b/crates/crypto/src/hmac/hmac_ossl11.rs @@ -16,9 +16,9 @@ //! //! # Implementation Strategy //! -//! The module uses a generic approach with the `OsslHmacKey` type that wraps -//! algorithm-specific implementations. Each algorithm implements the `OsslHmacAlgo` -//! trait to provide its OpenSSL `MessageDigest` configuration and key size limits. +//! The module provides the `OsslHmacAlgo` type, which drives an OpenSSL +//! `Signer` configured with the selected `MessageDigest` to compute and +//! verify HMACs. //! //! # Platform Integration //! diff --git a/crates/crypto/src/kdf/hkdf_ossl11.rs b/crates/crypto/src/kdf/hkdf_ossl11.rs index bb1ffdf93..b6a58797f 100644 --- a/crates/crypto/src/kdf/hkdf_ossl11.rs +++ b/crates/crypto/src/kdf/hkdf_ossl11.rs @@ -81,15 +81,14 @@ impl<'a> OsslHkdfAlgo<'a> { /// /// # Arguments /// - /// * `hash` - The hash instance specifying the algorithm to use for HKDF /// * `mode` - The HKDF mode (Extract, Expand, or ExtractAndExpand) + /// * `hash` - The hash instance specifying the algorithm to use for HKDF /// * `salt` - Optional salt for Extract phase (recommended for Extract modes) /// * `info` - Optional context/application-specific info for Expand phase - /// * `derived_length` - Desired output length (defaults to hash output size) /// /// # Returns /// - /// A new `OsslHkdf` instance configured for the specified parameters. + /// A new `OsslHkdfAlgo` instance configured for the specified parameters. pub fn new( mode: HkdfMode, hash: &'a HashAlgo, diff --git a/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs index ba7666343..f84b25818 100644 --- a/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs @@ -362,7 +362,7 @@ impl OsslRsaHashSignAlgo { /// /// # Returns /// - /// A new `OsslRsaSigning` instance configured for PKCS#1 v1.5 padding. + /// A new `OsslRsaHashSignAlgo` instance configured for PKCS#1 v1.5 padding. /// /// # Security Considerations /// @@ -389,7 +389,7 @@ impl OsslRsaHashSignAlgo { /// /// # Returns /// - /// A new `OsslRsaSigning` instance configured for PSS padding. + /// A new `OsslRsaHashSignAlgo` instance configured for PSS padding. /// /// # Security Considerations /// diff --git a/crates/crypto/src/rsa/rsa_sign_ossl11.rs b/crates/crypto/src/rsa/rsa_sign_ossl11.rs index 0c7f25a2e..fe7dc4be3 100644 --- a/crates/crypto/src/rsa/rsa_sign_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_sign_ossl11.rs @@ -227,7 +227,7 @@ impl OsslRsaSignAlgo { /// /// # Returns /// - /// A new `OsslRsaSigning` instance configured for PKCS#1 v1.5 padding. + /// A new `OsslRsaSignAlgo` instance configured for PKCS#1 v1.5 padding. /// /// # Security Considerations /// @@ -254,7 +254,7 @@ impl OsslRsaSignAlgo { /// /// # Returns /// - /// A new `OsslRsaSigning` instance configured for PSS padding. + /// A new `OsslRsaSignAlgo` instance configured for PSS padding. /// /// # Security Considerations /// From 8cb22f02cf135a872d46350679aa2ae741e9ea1b Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Mon, 6 Jul 2026 09:11:57 +0200 Subject: [PATCH 08/10] docs(crypto): correct two stale 1.1.x doc comments - rsa_enc: configure_decrypter docs listed MGF1 failure as RsaError, but it now maps to RsaSetPropertyError like the other OAEP setup calls. - cbc: reword the streaming-decrypt IV note; the advanced IV is retained in the context and retrievable via algo()/into_algo(), not lost. Signed-off-by: Christian Walter --- crates/crypto/src/aes/cbc_ossl11.rs | 4 ++-- crates/crypto/src/rsa/rsa_enc_ossl11.rs | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/crypto/src/aes/cbc_ossl11.rs b/crates/crypto/src/aes/cbc_ossl11.rs index d7dc92a06..0e16a0868 100644 --- a/crates/crypto/src/aes/cbc_ossl11.rs +++ b/crates/crypto/src/aes/cbc_ossl11.rs @@ -654,8 +654,8 @@ impl<'a> DecryptOpContext<'a> for OsslAesCbcDecryptContext { .crypter .finalize(&mut output[count..]) .map_err(|_| CryptoError::AesDecryptError)?; - // Note: IV is updated in the context but not propagated back to the caller - // For proper IV chaining in streaming mode, the IV should be the last block of ciphertext + // Advance the context IV to the last ciphertext block so the CBC + // chain continues on a follow-on call (retrievable via algo()/into_algo()). let iv_len = self.algo.iv.len(); if input.len() >= iv_len { self.algo.iv.copy_from_slice(&input[input.len() - iv_len..]); diff --git a/crates/crypto/src/rsa/rsa_enc_ossl11.rs b/crates/crypto/src/rsa/rsa_enc_ossl11.rs index bf64ec188..ddc4bbf7f 100644 --- a/crates/crypto/src/rsa/rsa_enc_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_enc_ossl11.rs @@ -309,8 +309,7 @@ impl<'a> OsslRsaEncryptAlgo<'a> { /// # Errors /// /// Returns an error if: - /// - `CryptoError::RsaSetPropertyError` - Setting padding, OAEP hash algorithm, or label fails - /// - `CryptoError::RsaError` - Setting MGF1 hash algorithm fails + /// - `CryptoError::RsaSetPropertyError` - Setting padding, OAEP hash algorithm, MGF1 hash algorithm, or label fails fn configure_decrypter<'b>( &mut self, decrypter: &mut openssl::encrypt::Decrypter<'b>, From d313064034f72dad7550614f6154f8b93f7f10f2 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Mon, 6 Jul 2026 19:46:03 +0200 Subject: [PATCH 09/10] fix(crypto): match 3.x OAEP empty-label handling; lint 1.1.x backends in CI - rsa_enc: filter empty OAEP labels in both encrypt and decrypt so None and Some(b"") behave identically, matching the 3.x backend. - engine-matrix: run `cargo clippy -p azihsm_crypto` on the 1.1.x cell. The *_ossl11.rs backends compile only under cfg(not(ossl300)), so the 3.x workspace clippy in rust.yml never lints them. Signed-off-by: Christian Walter --- .github/workflows/engine-matrix.yml | 5 +++++ crates/crypto/src/rsa/rsa_enc_ossl11.rs | 8 ++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/engine-matrix.yml b/.github/workflows/engine-matrix.yml index e43d71f65..28dc0fa7c 100644 --- a/.github/workflows/engine-matrix.yml +++ b/.github/workflows/engine-matrix.yml @@ -102,6 +102,11 @@ jobs: cargo clippy -p azihsm_engine -p openssl-engine -p openssl-sys-engine \ --features engine --all-targets -- -D warnings + # azihsm_crypto's *_ossl11.rs backends only compile under cfg(not(ossl300)), + # so the 3.x workspace clippy in rust.yml never lints them; lint them here. + - name: Clippy azihsm_crypto + run: cargo clippy -p azihsm_crypto --all-targets -- -D warnings + # cargo-nextest isn't in the base image and building it from source is # unreliable here; fetch the prebuilt binary, pinned to the version xtask # installs elsewhere (xtask/src/setup.rs CARGO_NEXTEST_VERSION). diff --git a/crates/crypto/src/rsa/rsa_enc_ossl11.rs b/crates/crypto/src/rsa/rsa_enc_ossl11.rs index ddc4bbf7f..22e09069c 100644 --- a/crates/crypto/src/rsa/rsa_enc_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_enc_ossl11.rs @@ -284,7 +284,9 @@ impl<'a> OsslRsaEncryptAlgo<'a> { .set_rsa_mgf1_md(hash.message_digest()) .map_err(|_| CryptoError::RsaSetPropertyError)?; } - if let Some(label) = self.label { + // An empty label is equivalent to no label (OAEP's default), so + // filter it out to keep None and Some(b"") equivalent (matches 3.x). + if let Some(label) = self.label.filter(|l| !l.is_empty()) { encrypter .set_rsa_oaep_label(label) .map_err(|_| CryptoError::RsaSetPropertyError)?; @@ -328,7 +330,9 @@ impl<'a> OsslRsaEncryptAlgo<'a> { .set_rsa_mgf1_md(hash.message_digest()) .map_err(|_| CryptoError::RsaSetPropertyError)?; } - if let Some(label) = self.label { + // An empty label is equivalent to no label (OAEP's default), so + // filter it out to keep None and Some(b"") equivalent (matches 3.x). + if let Some(label) = self.label.filter(|l| !l.is_empty()) { decrypter .set_rsa_oaep_label(label) .map_err(|_| CryptoError::RsaSetPropertyError)?; From c0dcdd692040300ba2e74f730fc36fb8d2ad4593 Mon Sep 17 00:00:00 2001 From: Christian Walter Date: Mon, 6 Jul 2026 19:59:04 +0200 Subject: [PATCH 10/10] fix(crypto): return actual RSA encrypt length, not the upper bound RSA encrypt returned encrypt_len() (an upper bound) even after writing to the output buffer, instead of the byte count encrypter.encrypt() actually wrote. Rebind to the real length like the decrypt path already does. For RSA the ciphertext is always modulus-sized so the value is unchanged in practice, but this stops reporting a length that could exceed what was written. Signed-off-by: Christian Walter --- crates/crypto/src/rsa/rsa_enc_ossl11.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/crypto/src/rsa/rsa_enc_ossl11.rs b/crates/crypto/src/rsa/rsa_enc_ossl11.rs index 22e09069c..c7eb7c195 100644 --- a/crates/crypto/src/rsa/rsa_enc_ossl11.rs +++ b/crates/crypto/src/rsa/rsa_enc_ossl11.rs @@ -100,14 +100,16 @@ impl EncryptOp for OsslRsaEncryptAlgo<'_> { let len = encrypter .encrypt_len(input) .map_err(|_| CryptoError::RsaError)?; - if let Some(output) = output { + let len = if let Some(output) = output { if output.len() < len { return Err(CryptoError::RsaBufferTooSmall); } encrypter .encrypt(input, output) - .map_err(|_| CryptoError::RsaEncryptError)?; - } + .map_err(|_| CryptoError::RsaEncryptError)? + } else { + len + }; Ok(len) } }