diff --git a/.github/workflows/engine-matrix.yml b/.github/workflows/engine-matrix.yml index d1c622432..28dc0fa7c 100644 --- a/.github/workflows/engine-matrix.yml +++ b/.github/workflows/engine-matrix.yml @@ -102,11 +102,29 @@ 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). + - name: Install cargo-nextest + 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 # 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 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" 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..0e16a0868 --- /dev/null +++ b/crates/crypto/src/aes/cbc_ossl11.rs @@ -0,0 +1,696 @@ +// 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)?; + // 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. + 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)?; + // 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(); + } + 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)?; + // 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..]); + } + 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..8db35a2ac --- /dev/null +++ b/crates/crypto/src/aes/ecb_ossl11.rs @@ -0,0 +1,211 @@ +// 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.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)?; + 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..35e3b6995 --- /dev/null +++ b/crates/crypto/src/hash/hash_ossl11.rs @@ -0,0 +1,368 @@ +// 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 Reuse + /// + /// This method takes `&mut self` and finalizes the digest; the context + /// must not be updated or finalized again afterwards. + /// + /// # 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..ea463559a --- /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 provides the `OsslHmacAlgo` type, which drives an OpenSSL +//! `Signer` configured with the selected `MessageDigest` to compute and +//! verify HMACs. +//! +//! # 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.len() == signature.len() && openssl::memcmp::eq(&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.len() == signature.len() && openssl::memcmp::eq(&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..11086c280 --- /dev/null +++ b/crates/crypto/src/kdf/hkdf_ossl11.rs @@ -0,0 +1,246 @@ +// 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 + /// + /// * `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 + /// + /// # Returns + /// + /// A new `OsslHkdfAlgo` 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::HkdfSetPropertyError` - Setting HKDF properties or the key fails + /// - `CryptoError::HkdfDeriveError` - Derivation fails or a zero-length output is requested + /// - `CryptoError::HmacInvalidDerivedKeyLength` - Extract-mode length differs from the digest size + fn derive(&self, key: &Self::Key, derive_len: usize) -> Result { + // 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)?; + 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)?; + + // 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) + } +} + +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..c7eb7c195 --- /dev/null +++ b/crates/crypto/src/rsa/rsa_enc_ossl11.rs @@ -0,0 +1,345 @@ +// 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)?; + let len = if let Some(output) = output { + if output.len() < len { + return Err(CryptoError::RsaBufferTooSmall); + } + encrypter + .encrypt(input, output) + .map_err(|_| CryptoError::RsaEncryptError)? + } else { + len + }; + 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)?; + } + // 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)?; + } + } + 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, MGF1 hash algorithm, or label 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::RsaSetPropertyError)?; + } + // 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)?; + } + } + 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..f84b25818 --- /dev/null +++ b/crates/crypto/src/rsa/rsa_hash_sign_ossl11.rs @@ -0,0 +1,430 @@ +// 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 `OsslRsaHashSignAlgo` 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` - 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 `OsslRsaHashSignAlgo` 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 { + // 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)?; + } + 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..fe7dc4be3 --- /dev/null +++ b/crates/crypto/src/rsa/rsa_sign_ossl11.rs @@ -0,0 +1,306 @@ +// 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 `OsslRsaSignAlgo` 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 `OsslRsaSignAlgo` 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 { + // 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(saltlen)) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + pkey_ctx + .set_rsa_mgf1_md(hash.md()) + .map_err(|_| CryptoError::RsaSetPropertyError)?; + } + } + + Ok(()) + } +}