diff --git a/Cargo.toml b/Cargo.toml index 2b86625..05b91ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,13 @@ 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-aes-ccm"] +rustcrypto-encrypt = ["rustcrypto-aes-gcm", "rustcrypto-aes-ccm", "rustcrypto-chacha20-poly1305"] 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-chacha20-poly1305 = ["dep:chacha20poly1305", "dep:typenum", "dep:aead"] 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"] @@ -43,6 +44,7 @@ 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"] } +chacha20poly1305 = { version = "0.10.1", 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/encrypt.rs b/src/token/cose/crypto_impl/openssl/encrypt.rs index 2c99511..3fa54fe 100644 --- a/src/token/cose/crypto_impl/openssl/encrypt.rs +++ b/src/token/cose/crypto_impl/openssl/encrypt.rs @@ -11,7 +11,7 @@ use crate::error::CoseCipherError; use crate::token::cose::crypto_impl::openssl::OpensslContext; -use crate::token::cose::util::{aes_ccm_algorithm_tag_len, AES_GCM_TAG_LEN}; +use crate::token::cose::util::symmetric_algorithm_tag_len; use crate::token::cose::{crypto_impl, CoseSymmetricKey, EncryptCryptoBackend}; use alloc::vec::Vec; use coset::iana; @@ -27,6 +27,7 @@ impl EncryptCryptoBackend for OpensslContext { iv: &[u8], ) -> Result, CoseCipherError> { let cipher = crypto_impl::openssl::algorithm_to_cipher(algorithm)?; + let tag_len = symmetric_algorithm_tag_len(algorithm)?; 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. @@ -51,7 +52,7 @@ impl EncryptCryptoBackend for OpensslContext { // 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); + ciphertext.resize(ciphertext_len + tag_len, 0u8); ctx.tag(&mut ciphertext[ciphertext_len..])?; Ok(ciphertext) } @@ -65,8 +66,9 @@ impl EncryptCryptoBackend for OpensslContext { iv: &[u8], ) -> Result, CoseCipherError> { let cipher = crypto_impl::openssl::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)]; + let tag_len = symmetric_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_GCM_mode @@ -103,7 +105,7 @@ impl EncryptCryptoBackend for OpensslContext { iv: &[u8], ) -> Result, CoseCipherError> { let cipher = crypto_impl::openssl::algorithm_to_cipher(algorithm)?; - let tag_len = aes_ccm_algorithm_tag_len(algorithm)?; + let tag_len = symmetric_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. @@ -140,7 +142,7 @@ impl EncryptCryptoBackend for OpensslContext { iv: &[u8], ) -> Result, CoseCipherError> { let cipher = crypto_impl::openssl::algorithm_to_cipher(algorithm)?; - let tag_len = aes_ccm_algorithm_tag_len(algorithm)?; + let tag_len = symmetric_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)]; @@ -169,4 +171,69 @@ impl EncryptCryptoBackend for OpensslContext { Ok(plaintext) } + + fn encrypt_chacha20_poly1305( + &mut self, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + let cipher = openssl::cipher::Cipher::chacha20_poly1305(); + let tag_len = symmetric_algorithm_tag_len(iana::Algorithm::ChaCha20Poly1305)?; + let mut ctx = CipherCtx::new()?; + // Refer to https://docs.openssl.org/1.1.1/man3/EVP_EncryptInit/#chacha20-poly1305 for + // reference. + // 1. First, we set the cipher. + ctx.encrypt_init(Some(cipher), None, None)?; + // 2. We *must* set the IV length _before_ setting the IV. + 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![]; + // 4. Then, we set the AAD before setting the plaintext. + ctx.cipher_update(aad, None)?; + // 5. Finally, we provide the plaintext. + 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 + tag_len, 0u8); + ctx.tag(&mut ciphertext[ciphertext_len..])?; + Ok(ciphertext) + } + + fn decrypt_chacha20_poly1305( + &mut self, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + let cipher = openssl::cipher::Cipher::chacha20_poly1305(); + let tag_len = symmetric_algorithm_tag_len(iana::Algorithm::ChaCha20Poly1305)?; + 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://docs.openssl.org/1.1.1/man3/EVP_EncryptInit/#chacha20-poly1305 for + // reference. + // 1. First, we set the cipher. + ctx.decrypt_init(Some(cipher), None, None)?; + // 2. We *must* set the tag and IV length _before_ setting key and IV. + 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. Then, we set the AAD before setting the ciphertext. + ctx.cipher_update(aad, None)?; + // 5. Finally, we provide the ciphertext 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) + } } diff --git a/src/token/cose/crypto_impl/rustcrypto/encrypt/aead.rs b/src/token/cose/crypto_impl/rustcrypto/encrypt/aead.rs index 1af0cee..3d4e416 100644 --- a/src/token/cose/crypto_impl/rustcrypto/encrypt/aead.rs +++ b/src/token/cose/crypto_impl/rustcrypto/encrypt/aead.rs @@ -16,6 +16,7 @@ use crate::error::CoseCipherError; use crate::token::cose::{CoseSymmetricKey, CryptoBackend}; use super::RustCryptoContext; +use alloc::vec::Vec; impl RustCryptoContext { /// Perform an AEAD encryption operation on `plaintext` and the additional authenticated diff --git a/src/token/cose/crypto_impl/rustcrypto/encrypt/chacha_poly.rs b/src/token/cose/crypto_impl/rustcrypto/encrypt/chacha_poly.rs new file mode 100644 index 0000000..57e13b9 --- /dev/null +++ b/src/token/cose/crypto_impl/rustcrypto/encrypt/chacha_poly.rs @@ -0,0 +1,27 @@ +use crate::error::CoseCipherError; +use crate::token::cose::crypto_impl::rustcrypto::RustCryptoContext; +use crate::token::cose::{CoseSymmetricKey, CryptoBackend}; +use alloc::vec::Vec; +use chacha20poly1305::ChaCha20Poly1305; +use rand::CryptoRng; +use rand::RngCore; + +impl RustCryptoContext { + pub(super) fn encrypt_chacha20_poly1305( + key: &CoseSymmetricKey<'_, ::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError<::Error>> { + Self::encrypt_aead::(key, plaintext, aad, iv) + } + + pub(super) fn decrypt_chacha20_poly1305( + key: &CoseSymmetricKey<'_, ::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError<::Error>> { + Self::decrypt_aead::(key, ciphertext_with_tag, aad, iv) + } +} diff --git a/src/token/cose/crypto_impl/rustcrypto/encrypt/mod.rs b/src/token/cose/crypto_impl/rustcrypto/encrypt/mod.rs index a3b2070..eafe186 100644 --- a/src/token/cose/crypto_impl/rustcrypto/encrypt/mod.rs +++ b/src/token/cose/crypto_impl/rustcrypto/encrypt/mod.rs @@ -14,6 +14,7 @@ use rand::{CryptoRng, RngCore}; use crate::error::CoseCipherError; use crate::token::cose::crypto_impl::rustcrypto::RustCryptoContext; use crate::token::cose::{CoseSymmetricKey, EncryptCryptoBackend}; +use alloc::vec::Vec; #[cfg(feature = "rustcrypto-aes-gcm")] mod aes_gcm; @@ -21,7 +22,14 @@ mod aes_gcm; #[cfg(feature = "rustcrypto-aes-ccm")] mod aes_ccm; -#[cfg(any(feature = "rustcrypto-aes-gcm", feature = "rustcrypto-aes-ccm"))] +#[cfg(feature = "rustcrypto-chacha20-poly1305")] +mod chacha_poly; + +#[cfg(any( + feature = "rustcrypto-aes-gcm", + feature = "rustcrypto-aes-ccm", + feature = "rustcrypto-chacha20-poly1305" +))] mod aead; impl EncryptCryptoBackend for RustCryptoContext { @@ -72,4 +80,26 @@ impl EncryptCryptoBackend for RustCryptoContext { ) -> Result, CoseCipherError> { Self::decrypt_aes_ccm(algorithm, &key, ciphertext_with_tag, aad, iv) } + + #[cfg(feature = "rustcrypto-chacha20-poly1305")] + fn encrypt_chacha20_poly1305( + &mut self, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + Self::encrypt_chacha20_poly1305(&key, plaintext, aad, iv) + } + + #[cfg(feature = "rustcrypto-chacha20-poly1305")] + fn decrypt_chacha20_poly1305( + &mut self, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + Self::decrypt_chacha20_poly1305(&key, ciphertext_with_tag, aad, iv) + } } diff --git a/src/token/cose/crypto_impl/rustcrypto/mac/hmac.rs b/src/token/cose/crypto_impl/rustcrypto/mac/hmac.rs index 3c58f99..8bb7d17 100644 --- a/src/token/cose/crypto_impl/rustcrypto/mac/hmac.rs +++ b/src/token/cose/crypto_impl/rustcrypto/mac/hmac.rs @@ -17,6 +17,7 @@ use sha2::{Sha256, Sha384, Sha512}; use crate::error::CoseCipherError; use crate::token::cose::crypto_impl::rustcrypto::RustCryptoContext; use crate::token::cose::{CoseSymmetricKey, CryptoBackend}; +use alloc::vec::Vec; impl RustCryptoContext { /// Compute the HMAC of `payload` using the given `key` with the HMAC function diff --git a/src/token/cose/crypto_impl/rustcrypto/mac/mod.rs b/src/token/cose/crypto_impl/rustcrypto/mac/mod.rs index 03a89ca..fde865a 100644 --- a/src/token/cose/crypto_impl/rustcrypto/mac/mod.rs +++ b/src/token/cose/crypto_impl/rustcrypto/mac/mod.rs @@ -13,6 +13,7 @@ use crate::token::cose::crypto_impl::rustcrypto::CoseCipherError; use crate::token::cose::crypto_impl::rustcrypto::RustCryptoContext; use crate::token::cose::CoseSymmetricKey; use crate::token::cose::MacCryptoBackend; +use alloc::vec::Vec; use coset::iana; use rand::{CryptoRng, RngCore}; diff --git a/src/token/cose/encrypted/encrypt/tests.rs b/src/token/cose/encrypted/encrypt/tests.rs index 13d90d1..4ee1132 100644 --- a/src/token/cose/encrypted/encrypt/tests.rs +++ b/src/token/cose/encrypted/encrypt/tests.rs @@ -32,7 +32,11 @@ use crate::token::cose::{util::determine_header_param, CryptoBackend}; #[cfg(feature = "openssl")] use crate::token::cose::test_helper::openssl_ctx; #[cfg(all( - any(feature = "rustcrypto-aes-gcm", feature = "rustcrypto-aes-ccm"), + any( + feature = "rustcrypto-aes-gcm", + feature = "rustcrypto-aes-ccm", + feature = "rustcrypto-chacha20-poly1305" + ), feature = "rustcrypto-aes-kw" ))] use crate::token::cose::test_helper::rustcrypto_ctx; @@ -317,6 +321,42 @@ fn cose_examples_aes_ccm_self_signed(test_path, backend); } +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr( + all( + feature = "rustcrypto-aes-kw", + feature = "rustcrypto-chacha20-poly1305" + ), + case::rustcrypto(rustcrypto_ctx()) +)] +fn cose_examples_chacha20_poly1305_reference_output< + B: EncryptCryptoBackend + KeyDistributionCryptoBackend, +>( + #[files("tests/cose_examples/chacha-poly-examples/chacha-poly-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-chacha20-poly1305" + ), + case::rustcrypto(rustcrypto_ctx()) +)] +fn cose_examples_chacha20_poly1305_self_signed< + B: EncryptCryptoBackend + KeyDistributionCryptoBackend, +>( + #[files("tests/cose_examples/chacha-poly-examples/chacha-poly-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( @@ -355,3 +395,19 @@ fn aes_ccm_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-chacha20-poly1305" + ), + case::rustcrypto(rustcrypto_ctx()) +)] +fn chacha20_poly1305_tests( + #[files("tests/dcaf_cose_examples/chacha-poly/*.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 f2e48b6..f1fd541 100644 --- a/src/token/cose/encrypted/encrypt0/tests.rs +++ b/src/token/cose/encrypted/encrypt0/tests.rs @@ -24,7 +24,11 @@ use crate::token::cose::CryptoBackend; #[cfg(feature = "openssl")] use crate::token::cose::test_helper::openssl_ctx; -#[cfg(any(feature = "rustcrypto-aes-gcm", feature = "rustcrypto-aes-ccm"))] +#[cfg(any( + feature = "rustcrypto-aes-gcm", + feature = "rustcrypto-aes-ccm", + feature = "rustcrypto-chacha20-poly1305" +))] use crate::token::cose::test_helper::rustcrypto_ctx; impl CoseStructTestHelper for CoseEncrypt0 { @@ -188,3 +192,29 @@ fn cose_examples_aes_ccm_encrypt0_self_signed( ) { perform_cose_self_signed_test::(test_path, backend); } + +#[rstest] +#[cfg_attr(feature = "openssl", case::openssl(openssl_ctx()))] +#[cfg_attr( + feature = "rustcrypto-chacha20-poly1305", + case::rustcrypto(rustcrypto_ctx()) +)] +fn cose_examples_chacha20_poly1305_reference_output( + #[files("tests/cose_examples/chacha-poly-examples/chacha-poly-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-chacha20-poly1305", + case::rustcrypto(rustcrypto_ctx()) +)] +fn cose_examples_chacha20_poly1305_self_signed( + #[files("tests/cose_examples/chacha-poly-examples/chacha-poly-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 e348fc2..58f2e54 100644 --- a/src/token/cose/encrypted/mod.rs +++ b/src/token/cose/encrypted/mod.rs @@ -22,8 +22,7 @@ mod encrypt; mod encrypt0; use crate::token::cose::util::{ - aes_ccm_algorithm_tag_len, determine_and_check_aes_params, try_cose_crypto_operation, - AES_GCM_TAG_LEN, + determine_and_check_symmetric_params, symmetric_algorithm_tag_len, try_cose_crypto_operation, }; pub use encrypt::{CoseEncryptBuilderExt, CoseEncryptExt}; pub use encrypt0::{CoseEncrypt0BuilderExt, CoseEncrypt0Ext}; @@ -218,8 +217,8 @@ pub trait EncryptCryptoBackend: CryptoBackend { /// Decrypts the given `ciphertext_with_tag` 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`. + /// [RFC 9053, section 4.2](https://datatracker.ietf.org/doc/html/rfc9053#section-4.2), the + /// given `key`, and the provided `iv`. /// /// # Arguments /// @@ -280,6 +279,103 @@ pub trait EncryptCryptoBackend: CryptoBackend { algorithm, ))) } + + /// Encrypts the given `payload` using ChaCha20/Poly1305 using the parameters specified for it + /// in [RFC 9053, section 4.3](https://datatracker.ietf.org/doc/html/rfc9053#section-4.3), the + /// given `key`, and the provided `iv`. + /// + /// # Arguments + /// + /// * `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 ChaCha20/Poly1305 + /// and panic if this is not the case. + /// + /// # Returns + /// + /// It is expected that the return value is the computed output of ChaCha20/Poly1305 as + /// specified in [RFC 8439, Section 2.8](https://datatracker.ietf.org/doc/html/rfc8439#section-2.8). + /// + /// # 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 key or IV are not of the right length for + /// ChaCha20/Poly1305 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. + #[allow(unused_variables)] + fn encrypt_chacha20_poly1305( + &mut self, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + iana::Algorithm::ChaCha20Poly1305, + ))) + } + + /// Decrypts the given `ciphertext_with_tag` using ChaCha20/Poly1305 using the parameters specified for it + /// in [RFC 9053, section 4.3](https://datatracker.ietf.org/doc/html/rfc9053#section-4.3), the + /// given `key`, and the provided `iv`. + /// + /// # Arguments + /// + /// * `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 8439, Section 2.8](https://datatracker.ietf.org/doc/html/rfc8439#section-2.8)). + /// 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 decryption process. + /// Implementations may assume that `iv` has the correct length for ChaCha20/Poly1305 + /// 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 key or IV are not of the right length for + /// ChaCha20/Poly1305 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. + #[allow(unused_variables)] + fn decrypt_chacha20_poly1305( + &mut self, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + iana::Algorithm::ChaCha20Poly1305, + ))) + } } /// Attempts to perform a COSE encryption operation for a [`CoseEncrypt`](coset::CoseEncrypt) or @@ -308,12 +404,11 @@ fn try_encrypt( BTreeSet::from_iter(vec![KeyOperation::Assigned(iana::KeyOperation::Encrypt)]), |key, alg, protected, unprotected| { let parsed_key = CoseParsedKey::try_from(key)?; + // Check if this is a valid symmetric, determine IV. + let (symm_key, iv) = + determine_and_check_symmetric_params(alg, parsed_key, protected, unprotected)?; match alg { iana::Algorithm::A128GCM | iana::Algorithm::A192GCM | iana::Algorithm::A256GCM => { - // 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 @@ -324,12 +419,11 @@ fn try_encrypt( | 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) } + iana::Algorithm::ChaCha20Poly1305 => { + backend.encrypt_chacha20_poly1305(symm_key, plaintext, enc_structure, &iv) + } alg => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( alg, ))), @@ -364,18 +458,18 @@ pub(crate) fn try_decrypt( BTreeSet::from_iter(vec![KeyOperation::Assigned(iana::KeyOperation::Decrypt)]), |key, alg, protected, unprotected| { 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, determine IV. - let (symm_key, iv) = - determine_and_check_aes_params(alg, parsed_key, protected, unprotected)?; + // Check if this is a valid symmetric key, determine IV. + let (symm_key, iv) = + determine_and_check_symmetric_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. - if ciphertext.len() < AES_GCM_TAG_LEN { - return Err(CoseCipherError::VerificationFailure); - } + // 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. + if ciphertext.len() < symmetric_algorithm_tag_len(alg)? { + return Err(CoseCipherError::VerificationFailure); + } + match alg { + iana::Algorithm::A128GCM | iana::Algorithm::A192GCM | iana::Algorithm::A256GCM => { (*backend.borrow_mut()).decrypt_aes_gcm( alg, symm_key, @@ -391,23 +485,15 @@ pub(crate) fn try_decrypt( | 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, - ) - } + | iana::Algorithm::AES_CCM_64_128_256 => (*backend.borrow_mut()).decrypt_aes_ccm( + alg, + symm_key, + ciphertext, + enc_structure, + &iv, + ), + iana::Algorithm::ChaCha20Poly1305 => (*backend.borrow_mut()) + .decrypt_chacha20_poly1305(symm_key, ciphertext, enc_structure, &iv), alg => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( alg, ))), diff --git a/src/token/cose/header.rs b/src/token/cose/header.rs index d25e9ae..acb79ad 100644 --- a/src/token/cose/header.rs +++ b/src/token/cose/header.rs @@ -1,5 +1,5 @@ use crate::error::CoseCipherError; -use crate::token::cose::util::aes_algorithm_iv_len; +use crate::token::cose::util::symmetric_algorithm_iv_len; use crate::token::cose::{CryptoBackend, EncryptCryptoBackend}; use coset::{iana, HeaderBuilder}; @@ -25,7 +25,7 @@ impl HeaderBuilderExt for HeaderBuilder { backend: &mut B, alg: iana::Algorithm, ) -> Result> { - let iv_size = aes_algorithm_iv_len(alg)?; + let iv_size = symmetric_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/recipient/mod.rs b/src/token/cose/recipient/mod.rs index a19e00f..66a2c77 100644 --- a/src/token/cose/recipient/mod.rs +++ b/src/token/cose/recipient/mod.rs @@ -25,7 +25,7 @@ use crate::token::cose::aad::{AadProvider, InvertedAadProvider}; use crate::token::cose::header::HeaderParam; use crate::token::cose::key::{CoseParsedKey, KeyProvider}; use crate::token::cose::util::determine_header_param; -use crate::token::cose::util::ensure_valid_aes_key; +use crate::token::cose::util::ensure_valid_symmetric_key; use crate::token::cose::util::try_cose_crypto_operation; use crate::token::cose::util::{determine_algorithm, determine_key_candidates}; use crate::token::cose::{CoseSymmetricKey, CryptoBackend}; @@ -612,7 +612,7 @@ impl CoseRecipientBuilderExt for CoseRecipientBuilder { iana::Algorithm::A128KW | iana::Algorithm::A192KW | iana::Algorithm::A256KW => { - let symm_key = ensure_valid_aes_key(alg, parsed_key)?; + let symm_key = ensure_valid_symmetric_key(alg, parsed_key)?; if protected.is_some() && !protected.as_ref().unwrap().is_empty() { return Err(CoseCipherError::AadUnsupported); @@ -842,7 +842,7 @@ impl CoseRecipientExt for CoseRecipient { iana::Algorithm::A128KW | iana::Algorithm::A192KW | iana::Algorithm::A256KW => { - let symm_key = ensure_valid_aes_key(alg, parsed_key)?; + let symm_key = ensure_valid_symmetric_key(alg, parsed_key)?; if !self.protected.is_empty() { return Err(CoseCipherError::AadUnsupported); } diff --git a/src/token/cose/test_helper.rs b/src/token/cose/test_helper.rs index aac5a32..3d167e3 100644 --- a/src/token/cose/test_helper.rs +++ b/src/token/cose/test_helper.rs @@ -117,6 +117,7 @@ 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("ChaCha-Poly1305") => Ok(Some(iana::Algorithm::ChaCha20Poly1305)), 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)), diff --git a/src/token/cose/util/symm.rs b/src/token/cose/util/symm.rs index fbdd74a..c2769a4 100644 --- a/src/token/cose/util/symm.rs +++ b/src/token/cose/util/symm.rs @@ -31,7 +31,7 @@ pub(crate) fn generate_cek_for_alg( /// /// Performs the checks required for symmetric keys suitable for AES according to /// [RFC 9053, Section 4](https://datatracker.ietf.org/doc/html/rfc9053#section-4). -pub(crate) fn ensure_valid_aes_key( +pub(crate) fn ensure_valid_symmetric_key( algorithm: iana::Algorithm, parsed_key: CoseParsedKey, ) -> Result, CoseCipherError> { @@ -86,7 +86,8 @@ fn symmetric_key_size( | iana::Algorithm::AES_CCM_64_64_256 | iana::Algorithm::AES_CCM_16_128_256 | iana::Algorithm::AES_CCM_64_128_256 - | iana::Algorithm::A256KW => Ok(32), + | iana::Algorithm::A256KW + | iana::Algorithm::ChaCha20Poly1305 => Ok(32), _ => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( algorithm, ))), @@ -99,13 +100,17 @@ fn symmetric_key_size( /// /// Returns [CoseCipherError::UnsupportedAlgorithm] if the provided algorithm is not a supported /// AES algorithm. -pub fn aes_algorithm_iv_len( +pub const fn symmetric_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(12), - // AES-CCM: Nonce length is parameterized. + // ChaCha20/Poly1305: Nonce is fixed at 96 bits (RFC 9053, Section 4.3). + iana::Algorithm::A128GCM + | iana::Algorithm::A192GCM + | iana::Algorithm::A256GCM + | iana::Algorithm::ChaCha20Poly1305 => Ok(12), + // AES-CCM: Nonce length is parameterized (RFC 9053, Section 4.2). iana::Algorithm::AES_CCM_16_64_128 | iana::Algorithm::AES_CCM_16_128_128 | iana::Algorithm::AES_CCM_16_64_256 @@ -120,16 +125,17 @@ pub fn aes_algorithm_iv_len( } } -/// Returns the authentication tag length expected for the AES-CCM variant given as `alg`. +/// Returns the authentication tag length expected for the symmetric algorithm 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( +/// symmetric algorithm. +pub const fn symmetric_algorithm_tag_len( algorithm: iana::Algorithm, ) -> Result> { match algorithm { + // AES-CCM: Tag length is parameterized (RFC 9053, Section 4.2). iana::Algorithm::AES_CCM_16_64_128 | iana::Algorithm::AES_CCM_64_64_128 | iana::Algorithm::AES_CCM_16_64_256 @@ -137,7 +143,13 @@ pub fn aes_ccm_algorithm_tag_len( 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), + | iana::Algorithm::AES_CCM_64_128_128 + // AES-GCM: Tag length is fixed to 128 bits (RFC 9053, Section 4.1). + | iana::Algorithm::A128GCM + | iana::Algorithm::A192GCM + | iana::Algorithm::A256GCM + // ChaCha20/Poly1305: Tag length is fixed to 128 bits (RFC 9053, Section 4.3). + | iana::Algorithm::ChaCha20Poly1305 => Ok(16), v => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( v, ))), @@ -147,13 +159,13 @@ pub fn aes_ccm_algorithm_tag_len( /// Determines the key and IV for an AES AEAD operation using the provided `protected` and /// `unprotected` headers, ensuring that the provided `parsed_key` is a valid AES key in the /// process. -pub(crate) fn determine_and_check_aes_params<'a, BE: Display>( +pub(crate) fn determine_and_check_symmetric_params<'a, BE: Display>( alg: iana::Algorithm, parsed_key: CoseParsedKey<'a, BE>, protected: Option<&Header>, unprotected: Option<&Header>, ) -> Result<(CoseSymmetricKey<'a, BE>, Vec), CoseCipherError> { - let symm_key = ensure_valid_aes_key::(alg, parsed_key)?; + let symm_key = ensure_valid_symmetric_key::(alg, parsed_key)?; let iv = determine_header_param(protected, unprotected, |v| { (!v.iv.is_empty()).then_some(&v.iv) @@ -163,7 +175,7 @@ pub(crate) fn determine_and_check_aes_params<'a, BE: Display>( (!v.partial_iv.is_empty()).then_some(&v.partial_iv) }); - let expected_iv_len = aes_algorithm_iv_len(alg)?; + let expected_iv_len = symmetric_algorithm_iv_len(alg)?; let iv = match (iv, partial_iv) { // IV and partial IV must not be set at the same time. @@ -219,7 +231,3 @@ pub(crate) fn determine_and_check_aes_params<'a, BE: Display>( Ok((symm_key, iv)) } - -/// Authentication tag length to use for AES-GCM (fixed to 128 bits according to -/// [RFC 9053, section 4.1](https://datatracker.ietf.org/doc/html/rfc9053#section-4.1)). -pub const AES_GCM_TAG_LEN: usize = 16; diff --git a/src/token/tests.rs b/src/token/tests.rs index c67b131..a1a53d4 100644 --- a/src/token/tests.rs +++ b/src/token/tests.rs @@ -12,7 +12,7 @@ use super::*; use crate::common::test_helper::MockCipher; use crate::error::CoseCipherError; -use crate::token::cose::util::aes_algorithm_iv_len; +use crate::token::cose::util::symmetric_algorithm_iv_len; use alloc::vec::Vec; use alloc::{string::ToString, vec}; use base64::Engine; @@ -88,7 +88,7 @@ fn example_headers(alg: Algorithm, generate_iv: bool) -> (Header, Header) { let mut iv = vec![ 0x63, 0x68, 0x98, 0x99, 0x4F, 0xF0, 0xEC, 0x7B, 0xFC, 0xF6, 0xD3, 0xF9, 0x5B, ]; - iv.truncate(aes_algorithm_iv_len::(alg).expect("invalid algorithm")); + iv.truncate(symmetric_algorithm_iv_len::(alg).expect("invalid algorithm")); unprotected_header_builder = unprotected_header_builder.iv(iv); } let unprotected_header = unprotected_header_builder.build(); diff --git a/tests/dcaf_cose_examples/chacha-poly/empty_payload.json b/tests/dcaf_cose_examples/chacha-poly/empty_payload.json new file mode 100644 index 0000000..a201dee --- /dev/null +++ b/tests/dcaf_cose_examples/chacha-poly/empty_payload.json @@ -0,0 +1,36 @@ +{ + "title": "ChaCha-Poly1305-01: Encryption with empty payload ", + "input": { + "plaintext": "", + "enveloped": { + "protected": { + "alg": "ChaCha-Poly1305" + }, + "recipients": [ + { + "key": { + "kty": "oct", + "kid": "sec-256", + "use": "enc", + "k": "Dx4tPEtaaXiHlqW0w9Lh8B8uPUxbanmIl6a1xNPi8QA" + }, + "unprotected": { + "alg": "direct", + "kid": "sec-256" + } + } + ] + }, + "rng_stream": [ + "26682306D4FB28CA01B43B80" + ] + }, + "intermediates": { + "AAD_hex": "8367456E637279707444A101181840", + "CEK_hex": "0F1E2D3C4B5A69788796A5B4C3D2E1F01F2E3D4C5B6A798897A6B5C4D3E2F100", + "recipients": [ + { + } + ] + } +}