diff --git a/Cargo.toml b/Cargo.toml index edd6084..2b86625 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,11 +17,12 @@ exclude = [ default = ["std", "openssl"] std = ["serde/std", "ciborium/std", "serde_bytes/std", "erased-serde/std", "derive_builder/std"] rustcrypto = ["rustcrypto-aes-gcm", "rustcrypto-aes-kw", "rustcrypto-ecdsa", "rustcrypto-hmac"] -rustcrypto-encrypt = ["rustcrypto-aes-gcm"] +rustcrypto-encrypt = ["rustcrypto-aes-gcm", "rustcrypto-aes-ccm"] rustcrypto-sign = ["rustcrypto-ecdsa"] rustcrypto-key-distribution = ["rustcrypto-aes-kw"] rustcrypto-mac = ["rustcrypto-hmac"] rustcrypto-aes-gcm = ["dep:aes-gcm", "dep:typenum", "dep:aead", "dep:aes"] +rustcrypto-aes-ccm = ["dep:ccm", "dep:typenum", "dep:aead", "dep:aes"] rustcrypto-aes-kw = ["dep:aes-kw", "dep:aes", "dep:typenum", "dep:crypto-common"] rustcrypto-ecdsa = ["dep:ecdsa", "dep:p256", "dep:p384", "dep:digest", "dep:sha2", "dep:elliptic-curve"] rustcrypto-hmac = ["dep:hmac", "dep:digest", "dep:sha2"] @@ -41,6 +42,7 @@ rand = { version = "^0.8", default-features = false } openssl = { version = "^0.10", optional = true } lazy_static = "1.4.0" aes-gcm = { version = "0.10.3", optional = true, default-features = false, features = ["alloc", "aes"] } +ccm = { version = "0.5.0", optional = true, default-features = false, features = ["alloc"] } typenum = { version = "1.17.0", optional = true, default-features = false, features = ["const-generics"] } crypto-common = { version = "0.1.6", optional = true, default-features = false } aead = { version = "0.5.2", optional = true, default-features = false } diff --git a/src/token/cose/crypto_impl/openssl.rs b/src/token/cose/crypto_impl/openssl.rs index 5b7ad4b..31662e5 100644 --- a/src/token/cose/crypto_impl/openssl.rs +++ b/src/token/cose/crypto_impl/openssl.rs @@ -10,11 +10,11 @@ */ use alloc::vec::Vec; - use ciborium::value::Value; use coset::{iana, Algorithm}; use openssl::aes::{unwrap_key, wrap_key, AesKey}; use openssl::bn::BigNum; +use openssl::cipher_ctx::CipherCtx; use openssl::ec::{EcGroup, EcKey}; use openssl::ecdsa::EcdsaSig; use openssl::error::ErrorStack; @@ -22,7 +22,6 @@ use openssl::hash::MessageDigest; use openssl::nid::Nid; use openssl::pkey::{PKey, Private, Public}; use openssl::sign::{Signer, Verifier}; -use openssl::symm::{decrypt_aead, encrypt_aead, Cipher}; use strum_macros::Display; use crate::error::CoseCipherError; @@ -32,7 +31,7 @@ use crate::token::cose::key::{CoseEc2Key, CoseSymmetricKey, EllipticCurve}; use crate::token::cose::maced::MacCryptoBackend; use crate::token::cose::recipient::KeyDistributionCryptoBackend; use crate::token::cose::signed::SignCryptoBackend; -use crate::token::cose::CryptoBackend; +use crate::token::cose::{aes_ccm_algorithm_tag_len, CryptoBackend}; /// Represents an error caused by the OpenSSL cryptographic backend. #[derive(Debug, Display)] @@ -102,15 +101,15 @@ impl From for CoseCipherError { /// - [x] A128GCM /// - [x] A192GCM /// - [x] A256GCM -/// - [ ] AES-CCM -/// - [ ] AES-CCM-16-64-128 -/// - [ ] AES-CCM-16-64-256 -/// - [ ] AES-CCM-64-64-128 -/// - [ ] AES-CCM-64-64-256 -/// - [ ] AES-CCM-16-128-128 -/// - [ ] AES-CCM-16-128-256 -/// - [ ] AES-CCM-64-128-128 -/// - [ ] AES-CCM-64-128-256 +/// - [x] AES-CCM +/// - [x] AES-CCM-16-64-128 +/// - [x] AES-CCM-16-64-256 +/// - [x] AES-CCM-64-64-128 +/// - [x] AES-CCM-64-64-256 +/// - [x] AES-CCM-16-128-128 +/// - [x] AES-CCM-16-128-256 +/// - [x] AES-CCM-64-128-128 +/// - [x] AES-CCM-64-128-256 /// - [ ] ChaCha20/Poly1305 /// - Content Key Distribution Methods (for COSE_Recipients) /// - Direct Encryption @@ -371,11 +370,32 @@ impl EncryptCryptoBackend for OpensslContext { iv: &[u8], ) -> Result, CoseCipherError> { let cipher = algorithm_to_cipher(algorithm)?; - let mut auth_tag = vec![0; AES_GCM_TAG_LEN]; - let mut ciphertext = encrypt_aead(cipher, key.k, Some(iv), aad, plaintext, &mut auth_tag) - .map_err(CoseCipherError::from)?; - - ciphertext.append(&mut auth_tag); + let mut ctx = CipherCtx::new()?; + // So, apparently OpenSSL requires a very specific order of operations which differs + // slightly for AES-GCM and AES-CCM in order to work. + // It would have just been too easy if you could just generalize and reuse the code for + // AES-CCM and AES-GCM, right? + + // Refer to https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Encryption_using_GCM_mode + // for reference. + // 1. First, we set the cipher. + ctx.encrypt_init(Some(cipher), None, None)?; + // 2. For GCM, we set the IV length _before_ setting key and IV. + // We do not set the tag length, as it is fixed for AES-GCM. + ctx.set_iv_length(iv.len())?; + // 3. Now we can set key and IV. + ctx.encrypt_init(None, Some(key.k), Some(iv))?; + let mut ciphertext = vec![]; + // Unlike for CCM, we *must not* set the data length here, otherwise encryption *will fail*. + // 4. Then, we *must* set the AAD _before_ setting the plaintext. + ctx.cipher_update(aad, None)?; + // 5. Finally, we must provide all plaintext in a single call. + ctx.cipher_update_vec(plaintext, &mut ciphertext)?; + // 6. Then, we can finish the operation. + ctx.cipher_final_vec(&mut ciphertext)?; + let ciphertext_len = ciphertext.len(); + ciphertext.resize(ciphertext_len + AES_GCM_TAG_LEN, 0u8); + ctx.tag(&mut ciphertext[ciphertext_len..])?; Ok(ciphertext) } @@ -388,27 +408,133 @@ impl EncryptCryptoBackend for OpensslContext { iv: &[u8], ) -> Result, CoseCipherError> { let cipher = algorithm_to_cipher(algorithm)?; - let auth_tag = &ciphertext_with_tag[(ciphertext_with_tag.len() - AES_GCM_TAG_LEN)..]; let ciphertext = &ciphertext_with_tag[..(ciphertext_with_tag.len() - AES_GCM_TAG_LEN)]; - decrypt_aead(cipher, key.k, Some(iv), aad, ciphertext, auth_tag) - .map_err(|_e| CoseCipherError::VerificationFailure) + let mut ctx = CipherCtx::new()?; + // Refer to https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Decryption_using_GCM_mode + // for reference. + // 1. First, we set the cipher. + ctx.decrypt_init(Some(cipher), None, None)?; + // 2. For GCM, we set the IV length _before_ setting key and IV. + // We do not set the tag length, as it is fixed for AES-GCM. + ctx.set_iv_length(iv.len())?; + // 3. Now we can set key and IV. + ctx.decrypt_init(None, Some(key.k), Some(iv))?; + // Unlike for CCM, we *must not* set the data length here, otherwise decryption *will fail*. + // 5. Then, we *must* set the AAD _before_ setting the ciphertext. + ctx.cipher_update(aad, None)?; + // 6. After that, we provide the ciphertext in a single call for decryption. + let mut plaintext = vec![0; ciphertext.len()]; + let mut plaintext_size = ctx.cipher_update(ciphertext, Some(&mut plaintext))?; + // 7. For GCM, we must set the tag value right before the finalization call. + ctx.set_tag(auth_tag)?; + // 8. Now we can finalize decryption. + plaintext_size += ctx.cipher_final_vec(&mut plaintext)?; + + plaintext.truncate(plaintext_size); + + Ok(plaintext) + } + + fn encrypt_aes_ccm( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + let cipher = algorithm_to_cipher(algorithm)?; + let tag_len = aes_ccm_algorithm_tag_len(algorithm)?; + let mut ctx = CipherCtx::new()?; + // Refer to https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Encryption_using_CCM_mode + // for reference. + // 1. First, we set the cipher. + ctx.encrypt_init(Some(cipher), None, None)?; + // 2. At least for CCM, we *must* set the tag and IV length _before_ setting key and IV. + // (https://github.com/sfackler/rust-openssl/pull/1594#issue-1105067105) + ctx.set_iv_length(iv.len())?; + ctx.set_tag_length(tag_len)?; + // 3. Now we can set key and IV. + ctx.encrypt_init(None, Some(key.k), Some(iv))?; + let mut ciphertext = vec![]; + // 4. For CCM, we *must* then inform OpenSSL about the size of the plaintext data _before_ + // setting the AAD. + ctx.set_data_len(plaintext.len())?; + // 5. Then, we *must* set the AAD _before_ setting the plaintext. + ctx.cipher_update(aad, None)?; + // 6. Finally, we must provide all plaintext in a single call. + ctx.cipher_update_vec(plaintext, &mut ciphertext)?; + // 7. Then, we can finish the operation. + ctx.cipher_final_vec(&mut ciphertext)?; + let ciphertext_len = ciphertext.len(); + ciphertext.resize(ciphertext_len + tag_len, 0u8); + ctx.tag(&mut ciphertext[ciphertext_len..])?; + Ok(ciphertext) + } + + fn decrypt_aes_ccm( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + let cipher = algorithm_to_cipher(algorithm)?; + let tag_len = aes_ccm_algorithm_tag_len(algorithm)?; + let auth_tag = &ciphertext_with_tag[(ciphertext_with_tag.len() - tag_len)..]; + let ciphertext = &ciphertext_with_tag[..(ciphertext_with_tag.len() - tag_len)]; + + let mut ctx = CipherCtx::new()?; + // Refer to https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Decryption_using_CCM_mode + // for reference. + // 1. First, we set the cipher. + ctx.decrypt_init(Some(cipher), None, None)?; + // 2. At least for CCM, we *must* set the tag and IV length _before_ setting key and IV. + // (https://github.com/sfackler/rust-openssl/pull/1594#issue-1105067105) + ctx.set_iv_length(iv.len())?; + ctx.set_tag(auth_tag)?; + // 3. Now we can set key and IV. + ctx.decrypt_init(None, Some(key.k), Some(iv))?; + // 4. For CCM, we *must* then inform OpenSSL about the size of the ciphertext data _before_ + // setting the AAD. + // (https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption#Authenticated_Decryption_using_CCM_mode) + ctx.set_data_len(ciphertext.len())?; + // 5. Then, we *must* set the AAD _before_ setting the ciphertext. + ctx.cipher_update(aad, None)?; + // 6. Finally, we must provide all ciphertext in a single call for decryption. + let mut plaintext = vec![0; ciphertext.len()]; + let plaintext_len = ctx.cipher_update(ciphertext, Some(&mut plaintext))?; + plaintext.truncate(plaintext_len); + // No call to cipher_final() here, I guess? + // The official examples in the OpenSSL wiki don't finalize, so we won't either. + + Ok(plaintext) } } /// Converts the provided [`iana::Algorithm`] to an OpenSSL [`Cipher`] that can be used for a -/// symmetric [`Crypter`]. +/// symmetric [`CipherCtx`]. fn algorithm_to_cipher( algorithm: iana::Algorithm, -) -> Result> { +) -> Result<&'static openssl::cipher::CipherRef, CoseCipherError> { match algorithm { - iana::Algorithm::A128GCM => Ok(Cipher::aes_128_gcm()), - iana::Algorithm::A192GCM => Ok(Cipher::aes_192_gcm()), - iana::Algorithm::A256GCM => Ok(Cipher::aes_256_gcm()), - iana::Algorithm::A128KW => Ok(Cipher::aes_128_ecb()), - iana::Algorithm::A192KW => Ok(Cipher::aes_192_ecb()), - iana::Algorithm::A256KW => Ok(Cipher::aes_256_ecb()), + iana::Algorithm::A128GCM => Ok(openssl::cipher::Cipher::aes_128_gcm()), + iana::Algorithm::A192GCM => Ok(openssl::cipher::Cipher::aes_192_gcm()), + iana::Algorithm::A256GCM => Ok(openssl::cipher::Cipher::aes_256_gcm()), + iana::Algorithm::A128KW => Ok(openssl::cipher::Cipher::aes_128_ecb()), + iana::Algorithm::A192KW => Ok(openssl::cipher::Cipher::aes_192_ecb()), + iana::Algorithm::A256KW => Ok(openssl::cipher::Cipher::aes_256_ecb()), + iana::Algorithm::AES_CCM_16_64_128 + | iana::Algorithm::AES_CCM_64_64_128 + | iana::Algorithm::AES_CCM_16_128_128 + | iana::Algorithm::AES_CCM_64_128_128 => Ok(openssl::cipher::Cipher::aes_128_ccm()), + iana::Algorithm::AES_CCM_16_64_256 + | iana::Algorithm::AES_CCM_64_64_256 + | iana::Algorithm::AES_CCM_16_128_256 + | iana::Algorithm::AES_CCM_64_128_256 => Ok(openssl::cipher::Cipher::aes_256_ccm()), v => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( v, ))), diff --git a/src/token/cose/crypto_impl/rustcrypto/encrypt/aead.rs b/src/token/cose/crypto_impl/rustcrypto/encrypt/aead.rs new file mode 100644 index 0000000..1af0cee --- /dev/null +++ b/src/token/cose/crypto_impl/rustcrypto/encrypt/aead.rs @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2024 The NAMIB Project Developers. + * Licensed under the Apache License, Version 2.0 or the MIT license + * , at your + * option. This file may not be copied, modified, or distributed + * except according to those terms. + * + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ +use aead::{Aead, AeadCore, Key, KeyInit, Nonce, Payload}; +use rand::CryptoRng; +use rand::RngCore; + +use crate::error::CoseCipherError; +use crate::token::cose::{CoseSymmetricKey, CryptoBackend}; + +use super::RustCryptoContext; + +impl RustCryptoContext { + /// Perform an AEAD encryption operation on `plaintext` and the additional authenticated + /// data `aad` using the given `iv` and `key`. + pub(super) fn encrypt_aead( + key: &CoseSymmetricKey<'_, as CryptoBackend>::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError<::Error>> { + let aes_key = Key::::from_slice(key.k); + let cipher = AEAD::new(aes_key); + let nonce = Nonce::::from_slice(iv); + let payload = Payload { + msg: plaintext, + aad, + }; + cipher + .encrypt(nonce, payload) + .map_err(CoseCipherError::from) + } + + /// Perform an AEAD decryption operation on `ciphertext` and the additional authenticated + /// data `aad` using the given `iv` and `key`. + pub(super) fn decrypt_aead( + key: &CoseSymmetricKey<'_, as CryptoBackend>::Error>, + ciphertext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError<::Error>> { + let aes_key = Key::::from_slice(key.k); + let cipher = AEAD::new(aes_key); + let nonce = Nonce::::from_slice(iv); + let payload = Payload { + msg: ciphertext, + aad, + }; + cipher + .decrypt(nonce, payload) + .map_err(CoseCipherError::from) + } +} diff --git a/src/token/cose/crypto_impl/rustcrypto/encrypt/aes_ccm.rs b/src/token/cose/crypto_impl/rustcrypto/encrypt/aes_ccm.rs new file mode 100644 index 0000000..1d7e180 --- /dev/null +++ b/src/token/cose/crypto_impl/rustcrypto/encrypt/aes_ccm.rs @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2024 The NAMIB Project Developers. + * Licensed under the Apache License, Version 2.0 or the MIT license + * , at your + * option. This file may not be copied, modified, or distributed + * except according to those terms. + * + * SPDX-License-Identifier: MIT OR Apache-2.0 + */ +use aes::{Aes128, Aes256}; +use ccm::Ccm; +use coset::{iana, Algorithm}; +use rand::CryptoRng; +use rand::RngCore; +use typenum::consts::{U13, U16, U7, U8}; + +use crate::error::CoseCipherError; +use crate::token::cose::{CoseSymmetricKey, CryptoBackend}; + +use super::RustCryptoContext; + +impl RustCryptoContext { + /// Perform an AES-GCM encryption operation on `plaintext` and the additional authenticated + /// data `aad` using the given `iv` and `key` with the given `algorithm` variant of AES-GCM. + pub(super) fn encrypt_aes_ccm( + algorithm: iana::Algorithm, + key: &CoseSymmetricKey<'_, ::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError<::Error>> { + match algorithm { + iana::Algorithm::AES_CCM_16_64_128 => { + Self::encrypt_aead::>(key, plaintext, aad, iv) + } + iana::Algorithm::AES_CCM_16_64_256 => { + Self::encrypt_aead::>(key, plaintext, aad, iv) + } + iana::Algorithm::AES_CCM_64_64_128 => { + Self::encrypt_aead::>(key, plaintext, aad, iv) + } + iana::Algorithm::AES_CCM_64_64_256 => { + Self::encrypt_aead::>(key, plaintext, aad, iv) + } + iana::Algorithm::AES_CCM_16_128_128 => { + Self::encrypt_aead::>(key, plaintext, aad, iv) + } + iana::Algorithm::AES_CCM_16_128_256 => { + Self::encrypt_aead::>(key, plaintext, aad, iv) + } + iana::Algorithm::AES_CCM_64_128_128 => { + Self::encrypt_aead::>(key, plaintext, aad, iv) + } + iana::Algorithm::AES_CCM_64_128_256 => { + Self::encrypt_aead::>(key, plaintext, aad, iv) + } + a => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + a, + ))), + } + } + + /// Perform an AES-GCM decryption operation on `ciphertext` and the additional authenticated + /// data `aad` using the given `iv` and `key` with the given `algorithm` variant of AES-GCM. + pub(super) fn decrypt_aes_ccm( + algorithm: iana::Algorithm, + key: &CoseSymmetricKey<'_, ::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError<::Error>> { + match algorithm { + iana::Algorithm::AES_CCM_16_64_128 => { + Self::decrypt_aead::>(key, ciphertext_with_tag, aad, iv) + } + iana::Algorithm::AES_CCM_16_64_256 => { + Self::decrypt_aead::>(key, ciphertext_with_tag, aad, iv) + } + iana::Algorithm::AES_CCM_64_64_128 => { + Self::decrypt_aead::>(key, ciphertext_with_tag, aad, iv) + } + iana::Algorithm::AES_CCM_64_64_256 => { + Self::decrypt_aead::>(key, ciphertext_with_tag, aad, iv) + } + iana::Algorithm::AES_CCM_16_128_128 => { + Self::decrypt_aead::>(key, ciphertext_with_tag, aad, iv) + } + iana::Algorithm::AES_CCM_16_128_256 => { + Self::decrypt_aead::>(key, ciphertext_with_tag, aad, iv) + } + iana::Algorithm::AES_CCM_64_128_128 => { + Self::decrypt_aead::>(key, ciphertext_with_tag, aad, iv) + } + iana::Algorithm::AES_CCM_64_128_256 => { + Self::decrypt_aead::>(key, ciphertext_with_tag, aad, iv) + } + a => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + a, + ))), + } + } +} diff --git a/src/token/cose/crypto_impl/rustcrypto/encrypt/aes_gcm.rs b/src/token/cose/crypto_impl/rustcrypto/encrypt/aes_gcm.rs index 7a01124..c18d8f2 100644 --- a/src/token/cose/crypto_impl/rustcrypto/encrypt/aes_gcm.rs +++ b/src/token/cose/crypto_impl/rustcrypto/encrypt/aes_gcm.rs @@ -8,7 +8,6 @@ * * SPDX-License-Identifier: MIT OR Apache-2.0 */ -use aead::{Aead, AeadCore, Key, KeyInit, Nonce, Payload}; use aes::Aes192; use aes_gcm::{Aes128Gcm, Aes256Gcm, AesGcm}; use coset::{iana, Algorithm}; @@ -22,46 +21,6 @@ use crate::token::cose::{CoseSymmetricKey, CryptoBackend}; use super::RustCryptoContext; impl RustCryptoContext { - /// Perform an AEAD encryption operation on `plaintext` and the additional authenticated - /// data `aad` using the given `iv` and `key`. - fn encrypt_aead( - key: &CoseSymmetricKey<'_, as CryptoBackend>::Error>, - plaintext: &[u8], - aad: &[u8], - iv: &[u8], - ) -> Result, CoseCipherError<::Error>> { - let aes_key = Key::::from_slice(key.k); - let cipher = AEAD::new(aes_key); - let nonce = Nonce::::from_slice(iv); - let payload = Payload { - msg: plaintext, - aad, - }; - cipher - .encrypt(nonce, payload) - .map_err(CoseCipherError::from) - } - - /// Perform an AEAD decryption operation on `ciphertext` and the additional authenticated - /// data `aad` using the given `iv` and `key`. - fn decrypt_aead( - key: &CoseSymmetricKey<'_, as CryptoBackend>::Error>, - ciphertext: &[u8], - aad: &[u8], - iv: &[u8], - ) -> Result, CoseCipherError<::Error>> { - let aes_key = Key::::from_slice(key.k); - let cipher = AEAD::new(aes_key); - let nonce = Nonce::::from_slice(iv); - let payload = Payload { - msg: ciphertext, - aad, - }; - cipher - .decrypt(nonce, payload) - .map_err(CoseCipherError::from) - } - /// Perform an AES-GCM encryption operation on `plaintext` and the additional authenticated /// data `aad` using the given `iv` and `key` with the given `algorithm` variant of AES-GCM. pub(super) fn encrypt_aes_gcm( diff --git a/src/token/cose/crypto_impl/rustcrypto/encrypt/mod.rs b/src/token/cose/crypto_impl/rustcrypto/encrypt/mod.rs index daa2910..a3b2070 100644 --- a/src/token/cose/crypto_impl/rustcrypto/encrypt/mod.rs +++ b/src/token/cose/crypto_impl/rustcrypto/encrypt/mod.rs @@ -18,6 +18,12 @@ use crate::token::cose::{CoseSymmetricKey, EncryptCryptoBackend}; #[cfg(feature = "rustcrypto-aes-gcm")] mod aes_gcm; +#[cfg(feature = "rustcrypto-aes-ccm")] +mod aes_ccm; + +#[cfg(any(feature = "rustcrypto-aes-gcm", feature = "rustcrypto-aes-ccm"))] +mod aead; + impl EncryptCryptoBackend for RustCryptoContext { #[cfg(feature = "rustcrypto-aes-gcm")] fn encrypt_aes_gcm( @@ -42,4 +48,28 @@ impl EncryptCryptoBackend for RustCryptoContext { ) -> Result, CoseCipherError> { Self::decrypt_aes_gcm(algorithm, &key, ciphertext_with_tag, aad, iv) } + + #[cfg(feature = "rustcrypto-aes-ccm")] + fn encrypt_aes_ccm( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + Self::encrypt_aes_ccm(algorithm, &key, plaintext, aad, iv) + } + + #[cfg(feature = "rustcrypto-aes-ccm")] + fn decrypt_aes_ccm( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + Self::decrypt_aes_ccm(algorithm, &key, ciphertext_with_tag, aad, iv) + } } diff --git a/src/token/cose/crypto_impl/rustcrypto/mod.rs b/src/token/cose/crypto_impl/rustcrypto/mod.rs index 7f7ffd4..089f2d9 100644 --- a/src/token/cose/crypto_impl/rustcrypto/mod.rs +++ b/src/token/cose/crypto_impl/rustcrypto/mod.rs @@ -138,15 +138,15 @@ impl From for CoseCipherError { /// - [x] A128GCM /// - [x] A192GCM /// - [x] A256GCM -/// - [ ] AES-CCM -/// - [ ] AES-CCM-16-64-128 -/// - [ ] AES-CCM-16-64-256 -/// - [ ] AES-CCM-64-64-128 -/// - [ ] AES-CCM-64-64-256 -/// - [ ] AES-CCM-16-128-128 -/// - [ ] AES-CCM-16-128-256 -/// - [ ] AES-CCM-64-128-128 -/// - [ ] AES-CCM-64-128-256 +/// - [x] AES-CCM +/// - [x] AES-CCM-16-64-128 +/// - [x] AES-CCM-16-64-256 +/// - [x] AES-CCM-64-64-128 +/// - [x] AES-CCM-64-64-256 +/// - [x] AES-CCM-16-128-128 +/// - [x] AES-CCM-16-128-256 +/// - [x] AES-CCM-64-128-128 +/// - [x] AES-CCM-64-128-256 /// - [ ] ChaCha20/Poly1305 /// - Content Key Distribution Methods (for COSE_Recipients) /// - Direct Encryption diff --git a/src/token/cose/encrypted/encrypt/tests.rs b/src/token/cose/encrypted/encrypt/tests.rs index f298980..49a805c 100644 --- a/src/token/cose/encrypted/encrypt/tests.rs +++ b/src/token/cose/encrypted/encrypt/tests.rs @@ -30,7 +30,10 @@ use crate::token::cose::CryptoBackend; #[cfg(feature = "openssl")] use crate::token::cose::test_helper::openssl_ctx; -#[cfg(all(feature = "rustcrypto-aes-gcm", feature = "rustcrypto-aes-kw"))] +#[cfg(all( + any(feature = "rustcrypto-aes-gcm", feature = "rustcrypto-aes-ccm"), + feature = "rustcrypto-aes-kw" +))] use crate::token::cose::test_helper::rustcrypto_ctx; impl CoseStructTestHelper @@ -228,6 +231,62 @@ fn cose_examples_aes_wrap_self_signed(test_path, backend); } +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr( + all(feature = "rustcrypto-aes-kw", feature = "rustcrypto-aes-gcm"), + case::rustcrypto(rustcrypto_ctx()) +)] +fn cose_examples_aes_gcm_reference_output< + B: EncryptCryptoBackend + KeyDistributionCryptoBackend, +>( + #[files("tests/cose_examples/aes-gcm-examples/aes-gcm-0[0-9].json")] test_path: PathBuf, + #[case] backend: B, +) { + perform_cose_reference_output_test::(test_path, backend); +} + +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr( + all(feature = "rustcrypto-aes-kw", feature = "rustcrypto-aes-gcm"), + case::rustcrypto(rustcrypto_ctx()) +)] +fn cose_examples_aes_gcm_self_signed( + #[files("tests/cose_examples/aes-gcm-examples/aes-gcm-0[0-9].json")] test_path: PathBuf, + #[case] backend: B, +) { + perform_cose_self_signed_test::(test_path, backend); +} + +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr( + all(feature = "rustcrypto-aes-kw", feature = "rustcrypto-aes-ccm"), + case::rustcrypto(rustcrypto_ctx()) +)] +fn cose_examples_aes_ccm_reference_output< + B: EncryptCryptoBackend + KeyDistributionCryptoBackend, +>( + #[files("tests/cose_examples/aes-ccm-examples/aes-ccm-0[0-9].json")] test_path: PathBuf, + #[case] backend: B, +) { + perform_cose_reference_output_test::(test_path, backend); +} + +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr( + all(feature = "rustcrypto-aes-kw", feature = "rustcrypto-aes-ccm"), + case::rustcrypto(rustcrypto_ctx()) +)] +fn cose_examples_aes_ccm_self_signed( + #[files("tests/cose_examples/aes-ccm-examples/aes-ccm-0[0-9].json")] test_path: PathBuf, + #[case] backend: B, +) { + perform_cose_self_signed_test::(test_path, backend); +} + #[rstest] #[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] #[cfg_attr( @@ -253,3 +312,16 @@ fn aes_gcm_tests( ) { perform_cose_self_signed_test::(test_path, backend); } + +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr( + all(feature = "rustcrypto-aes-kw", feature = "rustcrypto-aes-ccm"), + case::rustcrypto(rustcrypto_ctx()) +)] +fn aes_ccm_tests( + #[files("tests/dcaf_cose_examples/aes-ccm/*.json")] test_path: PathBuf, + #[case] backend: B, +) { + perform_cose_self_signed_test::(test_path, backend); +} diff --git a/src/token/cose/encrypted/encrypt0/tests.rs b/src/token/cose/encrypted/encrypt0/tests.rs index 27e6fe8..3a39e53 100644 --- a/src/token/cose/encrypted/encrypt0/tests.rs +++ b/src/token/cose/encrypted/encrypt0/tests.rs @@ -24,7 +24,7 @@ use crate::token::cose::CryptoBackend; #[cfg(feature = "openssl")] use crate::token::cose::test_helper::openssl_ctx; -#[cfg(feature = "rustcrypto-aes-gcm")] +#[cfg(any(feature = "rustcrypto-aes-gcm", feature = "rustcrypto-aes-ccm"))] use crate::token::cose::test_helper::rustcrypto_ctx; impl CoseStructTestHelper for CoseEncrypt0 { @@ -168,3 +168,23 @@ fn cose_examples_aes_gcm_encrypt0_self_signed( ) { perform_cose_self_signed_test::(test_path, backend); } + +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr(feature = "rustcrypto-aes-ccm", case::rustcrypto(rustcrypto_ctx()))] +fn cose_examples_aes_ccm_encrypt0_reference_output( + #[files("tests/cose_examples/aes-ccm-examples/aes-ccm-enc-*.json")] test_path: PathBuf, + #[case] backend: B, +) { + perform_cose_reference_output_test::(test_path, backend); +} + +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr(feature = "rustcrypto-aes-ccm", case::rustcrypto(rustcrypto_ctx()))] +fn cose_examples_aes_ccm_encrypt0_self_signed( + #[files("tests/cose_examples/aes-ccm-examples/aes-ccm-enc-*.json")] test_path: PathBuf, + #[case] backend: B, +) { + perform_cose_self_signed_test::(test_path, backend); +} diff --git a/src/token/cose/encrypted/mod.rs b/src/token/cose/encrypted/mod.rs index a98174c..6f3c804 100644 --- a/src/token/cose/encrypted/mod.rs +++ b/src/token/cose/encrypted/mod.rs @@ -11,7 +11,9 @@ use alloc::collections::BTreeSet; use alloc::rc::Rc; use alloc::vec::Vec; +use ciborium::Value; use core::cell::RefCell; +use core::fmt::Display; use coset::{iana, Algorithm, Header, KeyOperation}; use crate::error::CoseCipherError; @@ -59,6 +61,8 @@ pub trait EncryptCryptoBackend: CryptoBackend { /// * `aad` - additional authenticated data that should be included in the calculation of the /// authentication tag, but not encrypted. /// * `iv` - Initialization vector that should be used for the encryption process. + /// Implementations may assume that `iv` has the correct length for the given AES-GCM + /// variant and panic if this is not the case. /// /// # Returns /// @@ -95,8 +99,8 @@ pub trait EncryptCryptoBackend: CryptoBackend { ))) } - /// Decrypts the given `payload` using the AES-GCM variant provided as `algorithm` and the given - /// `key`. + /// Decrypts the given `payload` using the AES-GCM variant provided as `algorithm`, and the given + /// `key` and `iv`. /// /// Note that for all AES-GCM variants defined in RFC 9053, Section 4.1, the authentication tag /// should be 128 bits/16 bytes long. @@ -120,6 +124,8 @@ pub trait EncryptCryptoBackend: CryptoBackend { /// * `aad` - additional authenticated data that should be included in the calculation of the /// authentication tag, but not encrypted. /// * `iv` - Initialization vector that should be used for the encryption process. + /// Implementations may assume that `iv` has the correct length for the given AES-GCM + /// variant and panic if this is not the case. /// /// # Returns /// @@ -156,6 +162,209 @@ pub trait EncryptCryptoBackend: CryptoBackend { algorithm, ))) } + + /// Encrypts the given `payload` using AES-CCM with the parameters L (size of length field) + /// and M (size of authentication tag) specified for the given `algorithm` in + /// [RFC 9053, section 4.2](https://datatracker.ietf.org/doc/html/rfc9053#section-4.2), the + /// given `key`, and the provided `iv`. + /// + /// # Arguments + /// + /// * `algorithm` - The AES-CCM variant to use. + /// If unsupported by the backend, a [`CoseCipherError::UnsupportedAlgorithm`] + /// should be returned. + /// If the given algorithm is an IANA-assigned value that is unknown, the + /// implementation should return [`CoseCipherError::UnsupportedAlgorithm`] (in case + /// additional variants of AES-CCM are ever added). + /// If the algorithm is not an AES-CCM algorithm, the implementation may return + /// [`CoseCipherError::UnsupportedAlgorithm`] or panic. + /// * `key` - Symmetric key that should be used. + /// Implementations may assume that the provided key has the right length for the + /// provided algorithm, and panic if this is not the case. + /// * `plaintext` - Data that should be encrypted. + /// * `aad` - additional authenticated data that should be included in the calculation of the + /// authentication tag, but not encrypted. + /// * `iv` - Initialization vector that should be used for the encryption process. + /// Implementations may assume that `iv` has the correct length for the given AES-CCM + /// variant and panic if this is not the case. + /// + /// # Returns + /// + /// It is expected that the return value is the computed output of AES-CCM as specified in + /// [RFC 3610, Section 2.4](https://datatracker.ietf.org/doc/html/rfc3610#section-2.4). + /// + /// # Errors + /// + /// In case of errors, the implementation may return any valid [`CoseCipherError`]. + /// For backend-specific errors, [`CoseCipherError::Other`] may be used to convey a + /// backend-specific error. + /// + /// # Panics + /// + /// Implementations may panic if the provided algorithm is not an AES-CCM algorithm, the + /// provided key or IV are not of the right length for the provided algorithm or if an + /// unrecoverable backend error occurs that necessitates a panic (at their own discretion). + /// In the last of the above cases, additional panics should be documented on the backend level. + /// + /// For unknown algorithms or key curves, however, the implementation must not panic and return + /// [`CoseCipherError::UnsupportedAlgorithm`] instead (in case new AES-CCM variants are ever + /// defined). + #[allow(unused_variables)] + fn encrypt_aes_ccm( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + algorithm, + ))) + } + + /// Decrypts the given `payload` using AES-CCM with the parameters L (size of length field) + /// and M (size of authentication tag) specified for the given `algorithm` in + /// [RFC 9053, section 4.2](https://datatracker.ietf.org/doc/html/rfc9053#section-4.2) and the + /// given `key`. + /// + /// # Arguments + /// + /// * `algorithm` - The AES-CCM variant to use. + /// If unsupported by the backend, a [`CoseCipherError::UnsupportedAlgorithm`] error + /// should be returned. + /// If the given algorithm is an IANA-assigned value that is unknown, the + /// implementation should return [`CoseCipherError::UnsupportedAlgorithm`] (in case + /// additional variants of AES-CCM are ever added). + /// If the algorithm is not an AES-CCM algorithm, the implementation may return + /// [`CoseCipherError::UnsupportedAlgorithm`] or panic. + /// * `key` - Symmetric key that should be used. + /// Implementations may assume that the provided key has the right length for the + /// provided algorithm, and panic if this is not the case. + /// * `ciphertext_with_tag` - The ciphertext that should be decrypted concatenated with the + /// authentication tag that should be verified (if valid, should be the output of a + /// previous encryption as specified in + /// [RFC 3610, Section 2.4](https://datatracker.ietf.org/doc/html/rfc3610#section-2.4)). + /// Is guaranteed to be at least as long as the authentication tag should be. + /// * `aad` - additional authenticated data that should be included in the calculation of the + /// authentication tag, but not encrypted. + /// * `iv` - Initialization vector that should be used for the encryption process. + /// Implementations may assume that `iv` has the correct length for the given AES-CCM + /// variant and panic if this is not the case. + /// + /// # Returns + /// + /// It is expected that the return value is either the computed plaintext if decryption and + /// authentication are successful, or a [`CoseCipherError::VerificationFailure`] if one of these + /// steps fails even though the input is well-formed. + /// + /// # Errors + /// + /// In case of errors, the implementation may return any valid [`CoseCipherError`]. + /// For backend-specific errors, [`CoseCipherError::Other`] may be used to convey a + /// backend-specific error. + /// + /// # Panics + /// + /// Implementations may panic if the provided algorithm is not an AES-CCM algorithm, the + /// provided key or IV are not of the right length for the provided algorithm or if an + /// unrecoverable backend error occurs that necessitates a panic (at their own discretion). + /// In the last of the above cases, additional panics should be documented on the backend level. + /// + /// For unknown algorithms or key curves, however, the implementation must not panic and return + /// [`CoseCipherError::UnsupportedAlgorithm`] instead (in case new AES-CCM variants are ever + /// defined). + #[allow(unused_variables)] + fn decrypt_aes_ccm( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + algorithm, + ))) + } +} + +/// Returns the IV length expected for the AES variant given as `alg`. +/// +/// # Errors +/// +/// Returns [CoseCipherError::UnsupportedAlgorithm] if the provided algorithm is not a supported +/// AES algorithm. +pub fn aes_algorithm_iv_len( + alg: iana::Algorithm, +) -> Result> { + match alg { + // AES-GCM: Nonce is fixed at 96 bits (RFC 9053, Section 4.1) + iana::Algorithm::A128GCM | iana::Algorithm::A192GCM | iana::Algorithm::A256GCM => { + Ok(AES_GCM_NONCE_SIZE) + } + // AES-CCM: Nonce length is parameterized. + iana::Algorithm::AES_CCM_16_64_128 + | iana::Algorithm::AES_CCM_16_128_128 + | iana::Algorithm::AES_CCM_16_64_256 + | iana::Algorithm::AES_CCM_16_128_256 => Ok(13), + iana::Algorithm::AES_CCM_64_64_128 + | iana::Algorithm::AES_CCM_64_128_128 + | iana::Algorithm::AES_CCM_64_64_256 + | iana::Algorithm::AES_CCM_64_128_256 => Ok(7), + v => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + v, + ))), + } +} + +/// Returns the authentication tag length expected for the AES-CCM variant given as `alg`. +/// +/// # Errors +/// +/// Returns [CoseCipherError::UnsupportedAlgorithm] if the provided algorithm is not a supported +/// variant of AES-CCM. +pub fn aes_ccm_algorithm_tag_len( + algorithm: iana::Algorithm, +) -> Result> { + match algorithm { + iana::Algorithm::AES_CCM_16_64_128 + | iana::Algorithm::AES_CCM_64_64_128 + | iana::Algorithm::AES_CCM_16_64_256 + | iana::Algorithm::AES_CCM_64_64_256 => Ok(8), + iana::Algorithm::AES_CCM_16_128_256 + | iana::Algorithm::AES_CCM_64_128_256 + | iana::Algorithm::AES_CCM_16_128_128 + | iana::Algorithm::AES_CCM_64_128_128 => Ok(16), + v => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + v, + ))), + } +} + +fn determine_and_check_aes_params<'a, 'b, BE: Display>( + alg: iana::Algorithm, + parsed_key: CoseParsedKey<'a, BE>, + protected: Option<&'b Header>, + unprotected: Option<&'b Header>, +) -> Result<(CoseSymmetricKey<'a, BE>, &'b Vec), CoseCipherError> { + let symm_key = key::ensure_valid_aes_key::(alg, parsed_key)?; + + let iv = header_util::determine_header_param(protected, unprotected, |v| { + (!v.iv.is_empty()).then_some(&v.iv) + }) + .ok_or(CoseCipherError::MissingHeaderParam(HeaderParam::Generic( + iana::HeaderParameter::Iv, + )))?; + + if iv.len() != aes_algorithm_iv_len(alg)? { + return Err(CoseCipherError::InvalidHeaderParam( + HeaderParam::Generic(iana::HeaderParameter::Iv), + Value::Bytes(iv.clone()), + )); + } + + Ok((symm_key, iv)) } /// Attempts to perform a COSE encryption operation for a [`CoseEncrypt`](coset::CoseEncrypt) or @@ -186,21 +395,26 @@ fn try_encrypt( let parsed_key = CoseParsedKey::try_from(key)?; match alg { iana::Algorithm::A128GCM | iana::Algorithm::A192GCM | iana::Algorithm::A256GCM => { - // Check if this is a valid AES key. - let symm_key = key::ensure_valid_aes_key::(alg, parsed_key)?; - - let iv = protected - .into_iter() - .chain(unprotected.into_iter()) - .filter(|x| !x.iv.is_empty()) - .map(|x| x.iv.as_ref()) - .next() - .ok_or(CoseCipherError::MissingHeaderParam(HeaderParam::Generic( - iana::HeaderParameter::Iv, - )))?; + // Check if this is a valid AES key, determine IV. + let (symm_key, iv) = + determine_and_check_aes_params(alg, parsed_key, protected, unprotected)?; backend.encrypt_aes_gcm(alg, symm_key, plaintext, enc_structure, iv) } + iana::Algorithm::AES_CCM_16_64_128 + | iana::Algorithm::AES_CCM_64_64_128 + | iana::Algorithm::AES_CCM_16_128_128 + | iana::Algorithm::AES_CCM_64_128_128 + | iana::Algorithm::AES_CCM_16_64_256 + | iana::Algorithm::AES_CCM_64_64_256 + | iana::Algorithm::AES_CCM_16_128_256 + | iana::Algorithm::AES_CCM_64_128_256 => { + // Check if this is a valid AES key, determine IV. + let (symm_key, iv) = + determine_and_check_aes_params(alg, parsed_key, protected, unprotected)?; + + backend.encrypt_aes_ccm(alg, symm_key, plaintext, enc_structure, iv) + } alg => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( alg, ))), @@ -237,18 +451,9 @@ pub(crate) fn try_decrypt( let parsed_key = CoseParsedKey::try_from(key)?; match alg { iana::Algorithm::A128GCM | iana::Algorithm::A192GCM | iana::Algorithm::A256GCM => { - // Check if this is a valid AES key. - let symm_key = key::ensure_valid_aes_key::(alg, parsed_key)?; - - let iv = protected - .into_iter() - .chain(unprotected.into_iter()) - .filter(|x| !x.iv.is_empty()) - .map(|x| x.iv.as_ref()) - .next() - .ok_or(CoseCipherError::MissingHeaderParam(HeaderParam::Generic( - iana::HeaderParameter::Iv, - )))?; + // Check if this is a valid AES key, determine IV. + let (symm_key, iv) = + determine_and_check_aes_params(alg, parsed_key, protected, unprotected)?; // Authentication tag is 16 bytes long and should be included in the ciphertext. // Empty payloads are allowed, therefore we check for ciphertext.len() < 16, not <= 16. @@ -264,6 +469,30 @@ pub(crate) fn try_decrypt( iv, ) } + iana::Algorithm::AES_CCM_16_64_128 + | iana::Algorithm::AES_CCM_64_64_128 + | iana::Algorithm::AES_CCM_16_128_128 + | iana::Algorithm::AES_CCM_64_128_128 + | iana::Algorithm::AES_CCM_16_64_256 + | iana::Algorithm::AES_CCM_64_64_256 + | iana::Algorithm::AES_CCM_16_128_256 + | iana::Algorithm::AES_CCM_64_128_256 => { + // Check if this is a valid AES key, determine IV. + let (symm_key, iv) = + determine_and_check_aes_params(alg, parsed_key, protected, unprotected)?; + + if ciphertext.len() < aes_ccm_algorithm_tag_len(alg)? { + return Err(CoseCipherError::VerificationFailure); + } + + (*backend.borrow_mut()).decrypt_aes_ccm( + alg, + symm_key, + ciphertext, + enc_structure, + iv, + ) + } alg => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( alg, ))), diff --git a/src/token/cose/header_util.rs b/src/token/cose/header_util.rs index 66f7833..937a682 100644 --- a/src/token/cose/header_util.rs +++ b/src/token/cose/header_util.rs @@ -18,9 +18,8 @@ use coset::iana::EnumI64; use coset::{iana, Algorithm, CoseKey, Header, HeaderBuilder, KeyOperation, Label}; use crate::error::CoseCipherError; -use crate::token::cose::encrypted::AES_GCM_NONCE_SIZE; use crate::token::cose::key::KeyProvider; -use crate::token::cose::{CryptoBackend, EncryptCryptoBackend}; +use crate::token::cose::{aes_algorithm_iv_len, CryptoBackend, EncryptCryptoBackend}; /// A header parameter that can be used in a COSE header. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -97,9 +96,9 @@ pub(crate) fn check_for_duplicate_headers( /// Determines the value of a header param based on the provided `protected` and `unprotected` /// header buckets and the `accessor` function that determines the header parameter from a header /// reference. -pub(crate) fn determine_header_param Option, T>( - protected_header: Option<&Header>, - unprotected_header: Option<&Header>, +pub(crate) fn determine_header_param<'a, F: Fn(&'a Header) -> Option, T: 'a>( + protected_header: Option<&'a Header>, + unprotected_header: Option<&'a Header>, accessor: F, ) -> Option { protected_header @@ -199,17 +198,7 @@ impl HeaderBuilderExt for HeaderBuilder { backend: &mut B, alg: iana::Algorithm, ) -> Result> { - let iv_size = match alg { - // AES-GCM: Nonce is fixed at 96 bits (RFC 9053, Section 4.1) - iana::Algorithm::A128GCM | iana::Algorithm::A192GCM | iana::Algorithm::A256GCM => { - AES_GCM_NONCE_SIZE - } - v => { - return Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( - v, - ))) - } - }; + let iv_size = aes_algorithm_iv_len(alg)?; let mut iv = vec![0; iv_size]; backend.generate_rand(&mut iv)?; Ok(self.iv(iv)) diff --git a/src/token/cose/test_helper.rs b/src/token/cose/test_helper.rs index 9fd028b..11502a0 100644 --- a/src/token/cose/test_helper.rs +++ b/src/token/cose/test_helper.rs @@ -117,6 +117,14 @@ fn string_to_algorithm<'de, D: Deserializer<'de>>( Some("A128GCM") => Ok(Some(iana::Algorithm::A128GCM)), Some("A192GCM") => Ok(Some(iana::Algorithm::A192GCM)), Some("A256GCM") => Ok(Some(iana::Algorithm::A256GCM)), + Some("AES-CCM-16-128/64") => Ok(Some(iana::Algorithm::AES_CCM_16_64_128)), + Some("AES-CCM-16-256/64") => Ok(Some(iana::Algorithm::AES_CCM_16_64_256)), + Some("AES-CCM-64-128/64") => Ok(Some(iana::Algorithm::AES_CCM_64_64_128)), + Some("AES-CCM-64-256/64") => Ok(Some(iana::Algorithm::AES_CCM_64_64_256)), + Some("AES-CCM-16-128/128") => Ok(Some(iana::Algorithm::AES_CCM_16_128_128)), + Some("AES-CCM-16-256/128") => Ok(Some(iana::Algorithm::AES_CCM_16_128_256)), + Some("AES-CCM-64-128/128") => Ok(Some(iana::Algorithm::AES_CCM_64_128_128)), + Some("AES-CCM-64-256/128") => Ok(Some(iana::Algorithm::AES_CCM_64_128_256)), Some("A128KW") => Ok(Some(iana::Algorithm::A128KW)), Some("A192KW") => Ok(Some(iana::Algorithm::A192KW)), Some("A256KW") => Ok(Some(iana::Algorithm::A256KW)), diff --git a/src/token/tests.rs b/src/token/tests.rs index 825adfb..e49b415 100644 --- a/src/token/tests.rs +++ b/src/token/tests.rs @@ -84,7 +84,7 @@ fn example_ec_key_two(alg: Algorithm) -> CoseKey { fn example_headers(alg: Algorithm) -> (Header, Header) { let unprotected_header = HeaderBuilder::new() .iv(vec![ - 0x63, 0x68, 0x98, 0x99, 0x4F, 0xF0, 0xEC, 0x7B, 0xFC, 0xF6, 0xD3, 0xF9, 0x5B, + 0x63, 0x68, 0x98, 0x99, 0x4F, 0xF0, 0xEC, 0x7B, 0xFC, 0xF6, 0xD3, 0xF9, ]) .algorithm(alg) .value(47, Value::Null) diff --git a/tests/dcaf_cose_examples/aes-ccm/empty_payload.json b/tests/dcaf_cose_examples/aes-ccm/empty_payload.json new file mode 100644 index 0000000..5ed474b --- /dev/null +++ b/tests/dcaf_cose_examples/aes-ccm/empty_payload.json @@ -0,0 +1,36 @@ +{ + "title": "AES-GCM-01: Encryption with empty payload", + "input": { + "plaintext": "", + "enveloped": { + "protected": { + "alg": "AES-CCM-16-128/64" + }, + "recipients": [ + { + "key": { + "kty": "oct", + "kid": "our-secret", + "use": "enc", + "k": "hJtXIZ2uSN5kbQfbtTNWbg" + }, + "unprotected": { + "alg": "direct", + "kid": "our-secret" + } + } + ] + }, + "rng_stream": [ + "02D1F7E6F26C43D4868D87CEDC" + ] + }, + "intermediates": { + "AAD_hex": "8367456E637279707443A1010140", + "CEK_hex": "849B57219DAE48DE646D07DBB533566E", + "recipients": [ + { + } + ] + } +}