From 56b8037b9a2c15dc2e3eb2558f97bfccb401657c Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Tue, 5 May 2026 10:55:18 +0300 Subject: [PATCH 1/4] =?UTF-8?q?ossl/aes:=20add=20CKM=5FAES=5FKEY=5FWRAP=5F?= =?UTF-8?q?PAD=20mechanism=20(PKCS#11=20v3.0=20=C2=A72.16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement CKM_AES_KEY_WRAP_PAD as defined in PKCS#11 v3.0 section 2.16. This mechanism applies PKCS#7 padding (block size 8) to input before wrapping with the standard AES Key Wrap algorithm (RFC 3394 / NIST SP 800-38F Section 6.2), using the same OpenSSL cipher as CKM_AES_KEY_WRAP. - Register CKM_AES_KEY_WRAP_PAD with encrypt/decrypt/wrap/unwrap flags - Accept optional 8-byte IV parameter (same as CKM_AES_KEY_WRAP) - Buffer input in encrypt_update; apply PKCS#7 padding and encrypt in encrypt_final to correctly handle multi-part operations - Buffer ciphertext in decrypt_update; unwrap and strip PKCS#7 padding in decrypt_final using an allocation-free constant-time XOR loop - Update rustdoc for wrap/unwrap/encrypt_update/decrypt_update methods and add module-level SP 800-38F reference This is needed for FreeIPA migration from SoftHSMv2 to Kryoptic, where existing wrapped key material uses this mechanism. Signed-off-by: Alexander Bokovoy --- src/aes.rs | 10 ++++- src/ossl/aes.rs | 97 ++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 100 insertions(+), 7 deletions(-) diff --git a/src/aes.rs b/src/aes.rs index d51d0ae0..ffce9ee0 100644 --- a/src/aes.rs +++ b/src/aes.rs @@ -8,7 +8,9 @@ //! _Recommendation for Block Cipher Modes of Operation: Methods and //! Techniques_, [SP 800-38D](https://doi.org/10.6028/NIST.SP.800-38D): //! _Recommendation for Block Cipher Modes of Operation: Galois/Counter Mode -//! (GCM) and GMAC_). +//! (GCM) and GMAC_, [SP 800-38F](https://doi.org/10.6028/NIST.SP.800-38F): +//! _Recommendation for Block Cipher Modes of Operation: Methods for +//! Key Wrapping_). use std::fmt::Debug; use std::sync::LazyLock; @@ -320,6 +322,9 @@ impl Mechanism for AesMechanism { /// Implements the AES Key wrap operation (Wrap) /// + /// Supports CKM_AES_KEY_WRAP (RFC 3394), CKM_AES_KEY_WRAP_PAD + /// (PKCS#7 padding + RFC 3394), and CKM_AES_KEY_WRAP_KWP (RFC 5649). + /// /// [AES Key Wrap](https://docs.oasis-open.org/pkcs11/pkcs11-spec/v3.1/os/pkcs11-spec-v3.1-os.html#_Toc111203510) /// (Version 3.1) @@ -344,6 +349,9 @@ impl Mechanism for AesMechanism { /// Implements the AES Key wrap operation (Unwrap) /// + /// Supports CKM_AES_KEY_WRAP (RFC 3394), CKM_AES_KEY_WRAP_PAD + /// (PKCS#7 padding + RFC 3394), and CKM_AES_KEY_WRAP_KWP (RFC 5649). + /// /// [AES Key Wrap](https://docs.oasis-open.org/pkcs11/pkcs11-spec/v3.1/os/pkcs11-spec-v3.1-os.html#_Toc111203510) /// (Version 3.1) diff --git a/src/ossl/aes.rs b/src/ossl/aes.rs index e11f0553..166f688c 100644 --- a/src/ossl/aes.rs +++ b/src/ossl/aes.rs @@ -182,6 +182,7 @@ impl AesOperation { CKM_AES_CTR, CKM_AES_CTS, CKM_AES_KEY_WRAP, + CKM_AES_KEY_WRAP_PAD, CKM_AES_KEY_WRAP_KWP, ] { mechs.add_mechanism(*ckm, &(*AES_MECHS)[0]); @@ -375,7 +376,7 @@ impl AesOperation { taglen: 0, }) } - CKM_AES_KEY_WRAP => { + CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_PAD => { let iv = match mech.ulParameterLen { 0 => AesIvData::none()?, 8 => AesIvData::simple(bytes_to_vec( @@ -464,7 +465,7 @@ impl AesOperation { CKM_AES_CFB128 => EncAlg::AesCfb128(size), #[cfg(not(feature = "fips"))] CKM_AES_OFB => EncAlg::AesOfb(size), - CKM_AES_KEY_WRAP => EncAlg::AesWrap(size), + CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_PAD => EncAlg::AesWrap(size), CKM_AES_KEY_WRAP_KWP => EncAlg::AesWrapPad(size), _ => return Err(CKR_MECHANISM_INVALID)?, }) @@ -670,6 +671,10 @@ impl AesOperation { } /// Instantiates a new AES Key-Wrap Operation + /// + /// Supports CKM_AES_KEY_WRAP, CKM_AES_KEY_WRAP_PAD, and + /// CKM_AES_KEY_WRAP_KWP. For CKM_AES_KEY_WRAP_PAD, PKCS#7 + /// padding is applied during encryption via [AesOperation::encrypt]. pub fn wrap( mech: &CK_MECHANISM, wrapping_key: &Object, @@ -719,6 +724,10 @@ impl AesOperation { } /// Instantiates a new AES Key-Unwrap Operation + /// + /// Supports CKM_AES_KEY_WRAP, CKM_AES_KEY_WRAP_PAD, and + /// CKM_AES_KEY_WRAP_KWP. For CKM_AES_KEY_WRAP_PAD, PKCS#7 + /// padding is stripped during decryption via [AesOperation::decrypt]. pub fn unwrap( mech: &CK_MECHANISM, wrapping_key: &Object, @@ -1196,7 +1205,10 @@ impl Encryption for AesOperation { } /// Calls the underlying OpenSSL function to encrypt the plaintext buffer - /// provided, according to the configured mode + /// provided, according to the configured mode. + /// + /// For CKM_AES_KEY_WRAP_PAD, input is buffered and the actual PKCS#7 + /// padding and encryption is deferred to [AesOperation::encrypt_final]. fn encrypt_update( &mut self, plain: &[u8], @@ -1255,6 +1267,9 @@ impl Encryption for AesOperation { return Err(self.op_err(CKR_DATA_LEN_RANGE)); } } + CKM_AES_KEY_WRAP_PAD => { + outlen = 0; + } _ => (), } if cipher.len() < outlen { @@ -1344,6 +1359,10 @@ impl Encryption for AesOperation { plain_offset = plain_end; } } + CKM_AES_KEY_WRAP_PAD => { + self.buffer.extend_from_slice(plain); + plain_offset = plain_end; + } _ => (), } @@ -1453,6 +1472,19 @@ impl Encryption for AesOperation { } } CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_KWP => (), + CKM_AES_KEY_WRAP_PAD => { + let buf_len = self.buffer.len(); + let pad_len = AES_KWP_BLOCK - (buf_len % AES_KWP_BLOCK); + self.buffer.resize(buf_len + pad_len, pad_len as u8); + outlen = ctx.update(self.buffer.as_slice(), cipher).or_else( + |_| { + self.finalized = true; + Err(CKR_DEVICE_ERROR) + }, + )?; + zeromem(self.buffer.as_mut_slice()); + self.buffer.clear(); + } _ => { self.finalized = true; return Err(CKR_GENERAL_ERROR)?; @@ -1505,6 +1537,9 @@ impl Encryption for AesOperation { data_len + AES_KWP_BLOCK } } + CKM_AES_KEY_WRAP_PAD => { + ((data_len / AES_KWP_BLOCK) + 2) * AES_KWP_BLOCK + } CKM_AES_KEY_WRAP_KWP => { ((data_len + AES_BLOCK_SIZE - 1) / AES_KWP_BLOCK) * AES_KWP_BLOCK @@ -1535,6 +1570,7 @@ impl Encryption for AesOperation { data_len + AES_KWP_BLOCK } } + CKM_AES_KEY_WRAP_PAD => 0, CKM_AES_KEY_WRAP_KWP => { ((data_len + AES_BLOCK_SIZE - 1) / AES_KWP_BLOCK) * AES_KWP_BLOCK @@ -1563,7 +1599,11 @@ impl Decryption for AesOperation { } /// Calls the underlying OpenSSL function to decrypt the ciphertext buffer - /// provided, according to the configured mode + /// provided, according to the configured mode. + /// + /// For CKM_AES_KEY_WRAP_PAD, ciphertext is buffered and the actual + /// unwrapping and PKCS#7 padding removal is deferred to + /// [AesOperation::decrypt_final]. fn decrypt_update( &mut self, cipher: &[u8], @@ -1593,7 +1633,7 @@ impl Decryption for AesOperation { return Err(self.op_err(CKR_DATA_LEN_RANGE)); } } - CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_KWP => { + CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_PAD | CKM_AES_KEY_WRAP_KWP => { if cipher.len() % AES_KWP_BLOCK != 0 { return Err(self.op_err(CKR_DATA_LEN_RANGE)); } @@ -1647,6 +1687,7 @@ impl Decryption for AesOperation { * AES_BLOCK_SIZE } CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_KWP => cipher.len(), + CKM_AES_KEY_WRAP_PAD => 0, _ => cipher.len(), }; if plain.len() < outlen { @@ -1798,6 +1839,10 @@ impl Decryption for AesOperation { cipher_offset = cipher_end; } } + CKM_AES_KEY_WRAP_PAD => { + self.buffer.extend_from_slice(cipher); + cipher_offset = cipher_end; + } _ => (), } @@ -1899,6 +1944,44 @@ impl Decryption for AesOperation { #[cfg(not(feature = "fips"))] CKM_AES_CFB8 | CKM_AES_CFB1 | CKM_AES_CFB128 | CKM_AES_OFB => (), CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_KWP => (), + CKM_AES_KEY_WRAP_PAD => { + if self.buffer.len() == 0 { + return Err(CKR_ENCRYPTED_DATA_LEN_RANGE)?; + } + outlen = ctx.update(self.buffer.as_slice(), plain).or_else( + |_| { + self.finalized = true; + Err(CKR_DEVICE_ERROR) + }, + )?; + zeromem(self.buffer.as_mut_slice()); + self.buffer.clear(); + + if outlen < AES_KWP_BLOCK { + return Err(CKR_ENCRYPTED_DATA_INVALID)?; + } + let pad_byte = plain[outlen - 1]; + let pad_len = pad_byte as usize; + if pad_len == 0 || pad_len > AES_KWP_BLOCK { + zeromem(&mut plain[..outlen]); + return Err(CKR_ENCRYPTED_DATA_INVALID)?; + } + if pad_len > outlen { + zeromem(&mut plain[..outlen]); + return Err(CKR_ENCRYPTED_DATA_INVALID)?; + } + let pad_start = outlen - pad_len; + let mut bad = 0u8; + for b in &plain[pad_start..outlen] { + bad |= b ^ pad_byte; + } + if bad != 0 { + zeromem(&mut plain[..outlen]); + return Err(CKR_ENCRYPTED_DATA_INVALID)?; + } + zeromem(&mut plain[pad_start..outlen]); + outlen = pad_start; + } _ => return Err(CKR_GENERAL_ERROR)?, } @@ -1947,7 +2030,8 @@ impl Decryption for AesOperation { } self.buffer.len() + data_len } - CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_KWP => { + CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_PAD + | CKM_AES_KEY_WRAP_KWP => { if data_len % AES_KWP_BLOCK != 0 { return Err(self.op_err(CKR_ENCRYPTED_DATA_LEN_RANGE)); } @@ -1995,6 +2079,7 @@ impl Decryption for AesOperation { (data_len / AES_KWP_BLOCK) * AES_KWP_BLOCK } } + CKM_AES_KEY_WRAP_PAD => 0, _ => return Err(self.op_err(CKR_GENERAL_ERROR)), } }; From c0ea894b3f4d5616f3b6dc29687dd64fcff902b2 Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Tue, 5 May 2026 10:55:21 +0300 Subject: [PATCH 2/4] fips: add FIPS indicator entry for CKM_AES_KEY_WRAP_PAD Register CKM_AES_KEY_WRAP_PAD in the FIPS approved mechanism table with encrypt/decrypt/wrap/unwrap operations permitted. Signed-off-by: Alexander Bokovoy --- src/fips/indicators.rs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/fips/indicators.rs b/src/fips/indicators.rs index d78ee99e..3bfad623 100644 --- a/src/fips/indicators.rs +++ b/src/fips/indicators.rs @@ -283,7 +283,7 @@ struct FipsMechanism { /// Struct that holds FIPS properties for keys and mechanisms struct FipsChecks { keys: [FipsKeyType; 17], - mechs: [FipsMechanism; 93], + mechs: [FipsMechanism; 94], } /// A constant instantiation of FIPS properties with a list @@ -638,6 +638,18 @@ const FIPS_CHECKS: FipsChecks = FipsChecks { | CKF_UNWRAP | CKF_DERIVE, }, + FipsMechanism { + mechanism: CKM_AES_KEY_WRAP_PAD, + operations: CKF_ENCRYPT | CKF_DECRYPT | CKF_WRAP | CKF_UNWRAP, + restrictions: [restrict!(KRY_UNSPEC), restrict!()], + genflags: CKF_SIGN + | CKF_VERIFY + | CKF_ENCRYPT + | CKF_DECRYPT + | CKF_WRAP + | CKF_UNWRAP + | CKF_DERIVE, + }, FipsMechanism { mechanism: CKM_AES_KEY_WRAP_KWP, operations: CKF_ENCRYPT | CKF_DECRYPT | CKF_WRAP | CKF_UNWRAP, From 1a1234b7a6ed815e2b895f47b0b6c03d3f63f46d Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Tue, 5 May 2026 10:55:24 +0300 Subject: [PATCH 3/4] tests/aes: add CKM_AES_KEY_WRAP_PAD test coverage Extend the existing wrap/unwrap mechanism loop to include CKM_AES_KEY_WRAP_PAD, and add a dedicated test for non-aligned input sizes (8, 9, 13, 15 bytes) to verify correct PKCS#7 padding round-trips. Signed-off-by: Alexander Bokovoy --- src/tests/aes.rs | 100 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 2 deletions(-) diff --git a/src/tests/aes.rs b/src/tests/aes.rs index df57c50a..48bb58b6 100644 --- a/src/tests/aes.rs +++ b/src/tests/aes.rs @@ -711,7 +711,7 @@ fn test_aes_operations() { assert_eq!(&enc, &ciphertext); } - for mech in [CKM_AES_KEY_WRAP, CKM_AES_KEY_WRAP_KWP] { + for mech in [CKM_AES_KEY_WRAP, CKM_AES_KEY_WRAP_PAD, CKM_AES_KEY_WRAP_KWP] { /* AES KEY WRAP */ /* encryption and key wrapping operations should give the same @@ -720,7 +720,7 @@ fn test_aes_operations() { let data = [0x55u8; AES_BLOCK_SIZE]; let iv = [0xCCu8; 8]; let iv_len = match mech { - CKM_AES_KEY_WRAP => 8, + CKM_AES_KEY_WRAP | CKM_AES_KEY_WRAP_PAD => 8, CKM_AES_KEY_WRAP_KWP => 4, _ => panic!("uh?"), }; @@ -814,6 +814,102 @@ fn test_aes_operations() { assert_eq!(value, data); } + { + /* CKM_AES_KEY_WRAP_PAD with non-aligned input sizes. + * AES Key Wrap (RFC 3394) requires at least 2 semiblocks + * (16 bytes) of input, so minimum pre-pad input is 8 bytes. */ + for data_len in [8usize, 9, 13, 15] { + let data: Vec = (0..data_len).map(|i| (i + 1) as u8).collect(); + let mut mechanism = CK_MECHANISM { + mechanism: CKM_AES_KEY_WRAP_PAD, + pParameter: std::ptr::null_mut(), + ulParameterLen: 0, + }; + + let enc = ret_or_panic!(encrypt( + session, + handle, + data.as_slice(), + &mechanism, + )); + let dec = ret_or_panic!(decrypt( + session, + handle, + enc.as_slice(), + &mechanism, + )); + assert_eq!(data, dec); + + /* Also test via wrap/unwrap API using CKK_GENERIC_SECRET */ + let wp_handle = ret_or_panic!(import_object( + session, + CKO_SECRET_KEY, + &[(CKA_KEY_TYPE, CKK_GENERIC_SECRET)], + &[(CKA_VALUE, data.as_slice())], + &[(CKA_EXTRACTABLE, true)], + )); + + let mut wraplen = 0; + let ret = fn_wrap_key( + session, + &mut mechanism, + handle, + wp_handle, + std::ptr::null_mut(), + &mut wraplen, + ); + assert_eq!(ret, CKR_OK); + + let mut wrapped = vec![0u8; wraplen as usize]; + let ret = fn_wrap_key( + session, + &mut mechanism, + handle, + wp_handle, + wrapped.as_mut_ptr(), + &mut wraplen, + ); + assert_eq!(ret, CKR_OK); + + let mut template = make_attr_template( + &[ + (CKA_CLASS, CKO_SECRET_KEY), + (CKA_KEY_TYPE, CKK_GENERIC_SECRET), + (CKA_VALUE_LEN, data_len as CK_ULONG), + ], + &[], + &[(CKA_SENSITIVE, false), (CKA_EXTRACTABLE, true)], + ); + let mut wp_handle2 = CK_INVALID_HANDLE; + let ret = fn_unwrap_key( + session, + &mut mechanism, + handle, + wrapped.as_mut_ptr(), + wraplen, + template.as_mut_ptr(), + template.len() as CK_ULONG, + &mut wp_handle2, + ); + assert_eq!(ret, CKR_OK); + + let mut recovered = vec![0u8; data_len]; + let mut extract_template = make_ptrs_template(&[( + CKA_VALUE, + void_ptr!(recovered.as_mut_ptr()), + data_len, + )]); + let ret = fn_get_attribute_value( + session, + wp_handle2, + extract_template.as_mut_ptr(), + extract_template.len() as CK_ULONG, + ); + assert_eq!(ret, CKR_OK); + assert_eq!(data, recovered); + } + } + { /* GCM via AEAD MessageEncrypt/MessageDecrypt API */ let mut mechanism: CK_MECHANISM = CK_MECHANISM { From 395cb2fc35a271f14335326c56f345f66b964e0e Mon Sep 17 00:00:00 2001 From: Alexander Bokovoy Date: Tue, 5 May 2026 10:55:27 +0300 Subject: [PATCH 4/4] CHANGELOG: document CKM_AES_KEY_WRAP_PAD mechanism addition Signed-off-by: Alexander Bokovoy --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47acb81d..e3afdedd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,8 @@ All notable changes to this project should be documented in this file. ### What Changed --- +* Added support for CKM_AES_KEY_WRAP_PAD mechanism (PKCS#7 padding + AES Key + Wrap per PKCS#11 v3.0 section 2.16) ## [1.5.0] ## 2026-03-04