diff --git a/src/token/cose/aad.rs b/src/token/cose/aad.rs index ec55647..f184862 100644 --- a/src/token/cose/aad.rs +++ b/src/token/cose/aad.rs @@ -1,4 +1,4 @@ -use crate::token::cose::determine_header_param; +use crate::token::cose::util::determine_header_param; use alloc::collections::BTreeMap; use coset::{EncryptionContext, Header}; diff --git a/src/token/cose/crypto_impl/openssl.rs b/src/token/cose/crypto_impl/openssl.rs deleted file mode 100644 index 7057bda..0000000 --- a/src/token/cose/crypto_impl/openssl.rs +++ /dev/null @@ -1,625 +0,0 @@ -/* - * Copyright (c) 2022-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 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::CipherRef; -use openssl::cipher_ctx::CipherCtx; -use openssl::ec::{EcGroup, EcKey}; -use openssl::ecdsa::EcdsaSig; -use openssl::error::ErrorStack; -use openssl::hash::MessageDigest; -use openssl::nid::Nid; -use openssl::pkey::{PKey, Private, Public}; -use openssl::sign::{Signer, Verifier}; -use strum_macros::Display; - -use crate::error::CoseCipherError; -use crate::token::cose::encrypted::{EncryptCryptoBackend, AES_GCM_TAG_LEN}; -use crate::token::cose::header_util::HeaderParam; -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::{aes_ccm_algorithm_tag_len, CryptoBackend}; - -/// Represents an error caused by the OpenSSL cryptographic backend. -#[derive(Debug, Display)] -#[non_exhaustive] -pub enum CoseOpensslCipherError { - /// Standard OpenSSL error (represented as an [`ErrorStack`] in the openssl library crate). - OpensslError(ErrorStack), - /// AES key error. - AesKeyError(openssl::aes::KeyError), - /// Other error (error message is provided as a string). - Other(&'static str), -} - -impl From for CoseOpensslCipherError { - fn from(value: ErrorStack) -> Self { - CoseOpensslCipherError::OpensslError(value) - } -} - -impl From for CoseOpensslCipherError { - fn from(value: openssl::aes::KeyError) -> Self { - CoseOpensslCipherError::AesKeyError(value) - } -} - -impl From for CoseCipherError { - fn from(value: ErrorStack) -> Self { - CoseCipherError::Other(value.into()) - } -} - -impl From for CoseCipherError { - fn from(value: openssl::aes::KeyError) -> Self { - CoseCipherError::Other(value.into()) - } -} - -/// Context for the OpenSSL cryptographic backend. -/// -/// Can be used as a [`CryptoBackend`] for COSE operations. -/// -/// Generic properties of this backend: -/// - [ ] Can derive EC public key components if only the private component (d) is present. -/// - [ ] Can work with compressed EC public keys (EC keys using point compression) -/// -/// Algorithm support: -/// - Signature Algorithms (for COSE_Sign and COSE_Sign1) -/// - [x] ECDSA -/// - [x] ES256 -/// - [x] ES384 -/// - [x] ES512 -/// - [ ] ES256K -/// - [ ] EdDSA -/// - Message Authentication Code Algorithms (for COSE_Mac and COSE_Mac0) -/// - [x] HMAC -/// - [ ] HMAC 256/64 -/// - [x] HMAC 256/256 -/// - [x] HMAC 384/384 -/// - [x] HMAC 512/512 -/// - [ ] AES-CBC-MAC -/// - [ ] AES-MAC 128/64 -/// - [ ] AES-MAC 256/64 -/// - [ ] AES-MAC 128/128 -/// - [ ] AES-MAC 256/128 -/// - Content Encryption Algorithms (for COSE_Encrypt and COSE_Encrypt0) -/// - [x] AES-GCM -/// - [x] A128GCM -/// - [x] A192GCM -/// - [x] A256GCM -/// - [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 -/// - [ ] Direct Key with KDF -/// - [ ] direct+HKDF-SHA-256 -/// - [ ] direct+HKDF-SHA-512 -/// - [ ] direct+HKDF-AES-128 -/// - [ ] direct+HKDF-AES-256 -/// - Key Wrap -/// - [x] AES Key Wrap -/// - [x] A128KW -/// - [x] A192KW -/// - [x] A256KW -/// - Direct Key Agreement -/// - [ ] Direct ECDH -/// - [ ] ECDH-ES + HKDF-256 -/// - [ ] ECDH-ES + HKDF-512 -/// - [ ] ECDH-SS + HKDF-256 -/// - [ ] ECDH-SS + HKDF-512 -/// - Key Agreement with Key Wrap -/// - [ ] ECDH with Key Wrap -/// - [ ] ECDH-ES + A128KW -/// - [ ] ECDH-ES + A192KW -/// - [ ] ECDH-ES + A256KW -/// - [ ] ECDH-SS + A128KW -/// - [ ] ECDH-SS + A192KW -/// - [ ] ECDH-SS + A256KW -/// -/// Elliptic Curve support (for EC algorithms): -/// - ES256/ES384/ES512 [^1] -/// - [x] P-256 -/// - [x] P-384 -/// - [x] P-521 -/// - ES256K -/// - [ ] secp256k1 -/// - EdDSA -/// - [ ] Ed448 -/// - [ ] Ed25519 -/// - ECDH -/// - [ ] X448 -/// - [ ] X25519 -/// -/// [^1]: RFC 9053, Section 2.1 suggests using ES256 only with curve P-256, ES384 with curve P-384 -/// and ES512 only with curve P-521. -#[derive(Default)] -pub struct OpensslContext {} - -impl OpensslContext { - /// Creates a new OpenSSL context for use with COSE algorithms. - #[must_use] - pub fn new() -> OpensslContext { - OpensslContext {} - } -} - -impl CryptoBackend for OpensslContext { - type Error = CoseOpensslCipherError; - - fn generate_rand(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> { - openssl::rand::rand_bytes(buf).map_err(CoseOpensslCipherError::from) - } -} - -impl SignCryptoBackend for OpensslContext { - fn sign_ecdsa( - &mut self, - alg: iana::Algorithm, - key: &CoseEc2Key<'_, Self::Error>, - target: &[u8], - ) -> Result, CoseCipherError> { - let (pad_size, group) = get_ecdsa_group_params(key)?; - let hash = get_algorithm_hash_function(alg)?; - - // Possible truncation is fine, the key size will never exceed the size of an i32. - #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] - sign_ecdsa(&group, pad_size as i32, hash, key, target) - } - - fn verify_ecdsa( - &mut self, - alg: iana::Algorithm, - key: &CoseEc2Key<'_, Self::Error>, - signature: &[u8], - target: &[u8], - ) -> Result<(), CoseCipherError> { - let (pad_size, group) = get_ecdsa_group_params(key)?; - let hash = get_algorithm_hash_function(alg)?; - - verify_ecdsa(&group, pad_size, hash, key, signature, target) - } -} - -/// Determine the openssl [`EcGroup`] instance and coordinate size that should be used for the given -/// ECDSA key (based on its curve). -fn get_ecdsa_group_params( - key: &CoseEc2Key<'_, CoseOpensslCipherError>, -) -> Result<(usize, EcGroup), CoseCipherError> { - match &key.crv { - EllipticCurve::Assigned(iana::EllipticCurve::P_256) => { - // ECDSA using P-256 curve, coordinates are padded to 256 bits - Ok((32, EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap())) - } - EllipticCurve::Assigned(iana::EllipticCurve::P_384) => { - // ECDSA using P-384 curve, coordinates are padded to 384 bits - Ok((48, EcGroup::from_curve_name(Nid::SECP384R1).unwrap())) - } - EllipticCurve::Assigned(iana::EllipticCurve::P_521) => { - // ECDSA using P-521 curve, coordinates are padded to 528 bits (521 bits rounded up - // to the nearest full bytes). - Ok((66, EcGroup::from_curve_name(Nid::SECP521R1).unwrap())) - } - v => Err(CoseCipherError::UnsupportedCurve(v.clone())), - } -} - -/// Determine the hash function (represented in OpenSSL as a [`MessageDigest`]) that should be used -/// for a given [`iana::Algorithm`]. -fn get_algorithm_hash_function( - alg: iana::Algorithm, -) -> Result> { - match alg { - iana::Algorithm::ES256 | iana::Algorithm::HMAC_256_256 => Ok(MessageDigest::sha256()), - iana::Algorithm::ES384 | iana::Algorithm::HMAC_384_384 => Ok(MessageDigest::sha384()), - iana::Algorithm::ES512 | iana::Algorithm::HMAC_512_512 => Ok(MessageDigest::sha512()), - v => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( - v, - ))), - } -} - -/// Perform an ECDSA signature operation with the given parameters. -fn sign_ecdsa( - group: &EcGroup, - pad_size: i32, - hash: MessageDigest, - key: &CoseEc2Key<'_, CoseOpensslCipherError>, - target: &[u8], -) -> Result, CoseCipherError> { - let private_key = cose_ec2_to_ec_private_key(key, group).map_err(CoseCipherError::from)?; - - let mut signer = Signer::new( - hash, - &*PKey::from_ec_key(private_key).map_err(CoseOpensslCipherError::from)?, - ) - .map_err(CoseOpensslCipherError::from)?; - - // generated signature is of DER format, need to convert it to COSE key format - let der_signature = signer - .sign_oneshot_to_vec(target) - .map_err(CoseOpensslCipherError::from) - .map_err(CoseCipherError::from)?; - - let ecdsa_sig = EcdsaSig::from_der(der_signature.as_slice()) - .map_err(CoseOpensslCipherError::from) - .map_err(CoseCipherError::from)?; - - // See RFC 8152, section 8.1 - let mut sig = ecdsa_sig - .r() - .to_vec_padded(pad_size) - .map_err(CoseOpensslCipherError::from) - .map_err(CoseCipherError::from)?; - let mut s_vec = ecdsa_sig - .s() - .to_vec_padded(pad_size) - .map_err(CoseOpensslCipherError::from) - .map_err(CoseCipherError::from)?; - sig.append(&mut s_vec); - - Ok(sig) -} - -/// Perform an ECDSA verification operation with the given parameters. -fn verify_ecdsa( - group: &EcGroup, - pad_size: usize, - hash: MessageDigest, - key: &CoseEc2Key<'_, CoseOpensslCipherError>, - signature: &[u8], - signed_data: &[u8], -) -> Result<(), CoseCipherError> { - let public_key = cose_ec2_to_ec_public_key(key, group).map_err(CoseCipherError::from)?; - let pkey = PKey::from_ec_key(public_key).map_err(CoseOpensslCipherError::from)?; - - let mut verifier = Verifier::new(hash, &pkey).map_err(CoseOpensslCipherError::from)?; - - // signature is in COSE format, need to convert to DER format. - let r = BigNum::from_slice(&signature[..pad_size]).map_err(CoseOpensslCipherError::from)?; - let s = BigNum::from_slice(&signature[pad_size..]).map_err(CoseOpensslCipherError::from)?; - let signature = - EcdsaSig::from_private_components(r, s).map_err(CoseOpensslCipherError::from)?; - // Note: EcdsaSig has its own "verify" method, but it is deprecated since OpenSSL - // 3.0, which is why it's not used here. - let der_signature = signature.to_der().map_err(CoseOpensslCipherError::from)?; - - verifier - .verify_oneshot(der_signature.as_slice(), signed_data) - .map_err(CoseOpensslCipherError::from) - .map_err(CoseCipherError::from) - .and_then(|verification_successful| { - verification_successful - .then_some(()) - .ok_or(CoseCipherError::VerificationFailure) - }) -} - -/// Converts a private [`CoseEc2Key`] instance to its corresponding representation as an [`EcKey`] -/// in `openssl`. -fn cose_ec2_to_ec_private_key( - key: &CoseEc2Key<'_, CoseOpensslCipherError>, - group: &EcGroup, -) -> Result, CoseCipherError> { - let public_key = cose_ec2_to_ec_public_key(key, group)?; - - EcKey::::from_private_components( - group, - &*BigNum::from_slice( - // According to the contract of the trait, this should be ensured by the caller, so it's - // fine to panic here. - key.d - .expect("key provided to backend has no private component"), - ) - .map_err(CoseCipherError::::from)?, - public_key.public_key(), - ) - .map_err(CoseCipherError::::from) -} - -/// Converts a public [`CoseEc2Key`] instance to its corresponding representation as an [`EcKey`] -/// in `openssl`. -fn cose_ec2_to_ec_public_key( - key: &CoseEc2Key<'_, CoseOpensslCipherError>, - group: &EcGroup, -) -> Result, CoseCipherError> { - // TODO X and Y can be recomputed and are not strictly required if D is known - // (RFC 8152, Section 13.1.1) - if key.x.is_none() || key.y.is_none() { - return Err(CoseCipherError::UnsupportedKeyDerivation); - } - - EcKey::::from_public_key_affine_coordinates( - group, - &*BigNum::from_slice(key.x.unwrap()) - .map_err(CoseCipherError::::from)?, - &*BigNum::from_slice(key.y.unwrap()) - .map_err(CoseCipherError::::from)?, - ) - .map_err(CoseCipherError::::from) -} - -impl EncryptCryptoBackend for OpensslContext { - fn encrypt_aes_gcm( - &mut self, - algorithm: iana::Algorithm, - key: CoseSymmetricKey<'_, Self::Error>, - plaintext: &[u8], - aad: &[u8], - iv: &[u8], - ) -> Result, CoseCipherError> { - let cipher = algorithm_to_cipher(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. - // 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) - } - - fn decrypt_aes_gcm( - &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 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 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*. - // 4. Then, we *must* set the AAD _before_ setting the ciphertext. - ctx.cipher_update(aad, None)?; - // 5. 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))?; - // 6. For GCM, we must set the tag value right before the finalization call. - ctx.set_tag(auth_tag)?; - // 7. 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. - 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 [`CipherRef`] that can be used for a -/// symmetric [`CipherCtx`]. -fn algorithm_to_cipher( - algorithm: iana::Algorithm, -) -> Result<&'static CipherRef, CoseCipherError> { - match algorithm { - 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, - ))), - } -} - -impl KeyDistributionCryptoBackend for OpensslContext { - fn aes_key_wrap( - &mut self, - _algorithm: iana::Algorithm, - key: CoseSymmetricKey<'_, Self::Error>, - plaintext: &[u8], - iv: &[u8], - ) -> Result, CoseCipherError> { - let key = AesKey::new_encrypt(key.k)?; - let iv: [u8; 8] = iv.try_into().map_err(|_e| { - CoseCipherError::InvalidHeaderParam( - HeaderParam::Generic(iana::HeaderParameter::Iv), - Value::Bytes(iv.to_vec()), - ) - })?; - let mut output = vec![0u8; plaintext.len() + 8]; - let output_len = wrap_key(&key, Some(iv), output.as_mut_slice(), plaintext)?; - output.truncate(output_len); - Ok(output) - } - - fn aes_key_unwrap( - &mut self, - _algorithm: iana::Algorithm, - key: CoseSymmetricKey<'_, Self::Error>, - ciphertext: &[u8], - iv: &[u8], - ) -> Result, CoseCipherError> { - let key = AesKey::new_decrypt(key.k)?; - let iv: [u8; 8] = iv.try_into().map_err(|_e| { - CoseCipherError::InvalidHeaderParam( - HeaderParam::Generic(iana::HeaderParameter::Iv), - Value::Bytes(iv.to_vec()), - ) - })?; - let mut output = vec![0u8; ciphertext.len() - 8]; - let output_len = unwrap_key(&key, Some(iv), output.as_mut_slice(), ciphertext)?; - output.truncate(output_len); - Ok(output) - } -} - -/// Computes an HMAC for `input` using the given `algorithm` and `key`. -fn compute_hmac( - algorithm: iana::Algorithm, - key: &CoseSymmetricKey<'_, CoseOpensslCipherError>, - input: &[u8], -) -> Result, CoseCipherError> { - let hash = get_algorithm_hash_function(algorithm)?; - let hmac_key = PKey::hmac(key.k)?; - let mut signer = Signer::new(hash, &hmac_key)?; - signer - .sign_oneshot_to_vec(input) - .map_err(CoseCipherError::from) -} - -impl MacCryptoBackend for OpensslContext { - fn compute_hmac( - &mut self, - algorithm: iana::Algorithm, - key: CoseSymmetricKey<'_, Self::Error>, - data: &[u8], - ) -> Result, CoseCipherError> { - compute_hmac(algorithm, &key, data) - } - - fn verify_hmac( - &mut self, - algorithm: iana::Algorithm, - key: CoseSymmetricKey<'_, Self::Error>, - tag: &[u8], - data: &[u8], - ) -> Result<(), CoseCipherError> { - let hmac = compute_hmac(algorithm, &key, data)?; - // Use openssl::memcmp::eq to prevent timing attacks. - if openssl::memcmp::eq(hmac.as_slice(), tag) { - Ok(()) - } else { - Err(CoseCipherError::VerificationFailure) - } - } -} diff --git a/src/token/cose/crypto_impl/openssl/encrypt.rs b/src/token/cose/crypto_impl/openssl/encrypt.rs new file mode 100644 index 0000000..2c99511 --- /dev/null +++ b/src/token/cose/crypto_impl/openssl/encrypt.rs @@ -0,0 +1,172 @@ +/* + * Copyright (c) 2022-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 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::{crypto_impl, CoseSymmetricKey, EncryptCryptoBackend}; +use alloc::vec::Vec; +use coset::iana; +use openssl::cipher_ctx::CipherCtx; + +impl EncryptCryptoBackend for OpensslContext { + fn encrypt_aes_gcm( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + let cipher = crypto_impl::openssl::algorithm_to_cipher(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. + // 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) + } + + fn decrypt_aes_gcm( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + 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 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*. + // 4. Then, we *must* set the AAD _before_ setting the ciphertext. + ctx.cipher_update(aad, None)?; + // 5. 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))?; + // 6. For GCM, we must set the tag value right before the finalization call. + ctx.set_tag(auth_tag)?; + // 7. 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 = crypto_impl::openssl::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 = crypto_impl::openssl::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. + 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) + } +} diff --git a/src/token/cose/crypto_impl/openssl/key_distribution.rs b/src/token/cose/crypto_impl/openssl/key_distribution.rs new file mode 100644 index 0000000..7b643df --- /dev/null +++ b/src/token/cose/crypto_impl/openssl/key_distribution.rs @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2022-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 crate::error::CoseCipherError; +use crate::token::cose::crypto_impl::openssl::OpensslContext; +use crate::token::cose::{CoseSymmetricKey, HeaderParam, KeyDistributionCryptoBackend}; +use alloc::vec::Vec; +use ciborium::Value; +use coset::iana; +use openssl::aes::{unwrap_key, wrap_key, AesKey}; + +impl KeyDistributionCryptoBackend for OpensslContext { + fn aes_key_wrap( + &mut self, + _algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + let key = AesKey::new_encrypt(key.k)?; + let iv: [u8; 8] = iv.try_into().map_err(|_e| { + CoseCipherError::InvalidHeaderParam( + HeaderParam::Generic(iana::HeaderParameter::Iv), + Value::Bytes(iv.to_vec()), + ) + })?; + let mut output = vec![0u8; plaintext.len() + 8]; + let output_len = wrap_key(&key, Some(iv), output.as_mut_slice(), plaintext)?; + output.truncate(output_len); + Ok(output) + } + + fn aes_key_unwrap( + &mut self, + _algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + let key = AesKey::new_decrypt(key.k)?; + let iv: [u8; 8] = iv.try_into().map_err(|_e| { + CoseCipherError::InvalidHeaderParam( + HeaderParam::Generic(iana::HeaderParameter::Iv), + Value::Bytes(iv.to_vec()), + ) + })?; + let mut output = vec![0u8; ciphertext.len() - 8]; + let output_len = unwrap_key(&key, Some(iv), output.as_mut_slice(), ciphertext)?; + output.truncate(output_len); + Ok(output) + } +} diff --git a/src/token/cose/crypto_impl/openssl/mac.rs b/src/token/cose/crypto_impl/openssl/mac.rs new file mode 100644 index 0000000..ce16013 --- /dev/null +++ b/src/token/cose/crypto_impl/openssl/mac.rs @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022-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 crate::error::CoseCipherError; +use crate::token::cose::crypto_impl::openssl::{CoseOpensslCipherError, OpensslContext}; +use crate::token::cose::{CoseSymmetricKey, MacCryptoBackend}; +use alloc::vec::Vec; +use coset::iana; +use openssl::pkey::PKey; +use openssl::sign::Signer; + +/// Computes an HMAC for `input` using the given `algorithm` and `key`. +fn compute_hmac( + algorithm: iana::Algorithm, + key: &CoseSymmetricKey<'_, CoseOpensslCipherError>, + input: &[u8], +) -> Result, CoseCipherError> { + let hash = super::get_algorithm_hash_function(algorithm)?; + let hmac_key = PKey::hmac(key.k)?; + let mut signer = Signer::new(hash, &hmac_key)?; + signer + .sign_oneshot_to_vec(input) + .map_err(CoseCipherError::from) +} + +impl MacCryptoBackend for OpensslContext { + fn compute_hmac( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + data: &[u8], + ) -> Result, CoseCipherError> { + compute_hmac(algorithm, &key, data) + } + + fn verify_hmac( + &mut self, + algorithm: iana::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + tag: &[u8], + data: &[u8], + ) -> Result<(), CoseCipherError> { + let hmac = compute_hmac(algorithm, &key, data)?; + // Use openssl::memcmp::eq to prevent timing attacks. + if openssl::memcmp::eq(hmac.as_slice(), tag) { + Ok(()) + } else { + Err(CoseCipherError::VerificationFailure) + } + } +} diff --git a/src/token/cose/crypto_impl/openssl/mod.rs b/src/token/cose/crypto_impl/openssl/mod.rs new file mode 100644 index 0000000..c577c3b --- /dev/null +++ b/src/token/cose/crypto_impl/openssl/mod.rs @@ -0,0 +1,202 @@ +/* + * Copyright (c) 2022-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 + */ +mod encrypt; +mod key_distribution; +mod mac; +mod sign; + +use crate::error::CoseCipherError; +use crate::token::cose::CryptoBackend; +use coset::{iana, Algorithm}; +use openssl::cipher::CipherRef; +use openssl::error::ErrorStack; +use openssl::hash::MessageDigest; +use strum_macros::Display; +/// Represents an error caused by the OpenSSL cryptographic backend. +#[derive(Debug, Display)] +#[non_exhaustive] +pub enum CoseOpensslCipherError { + /// Standard OpenSSL error (represented as an [`ErrorStack`] in the openssl library crate). + OpensslError(ErrorStack), + /// AES key error. + AesKeyError(openssl::aes::KeyError), + /// Other error (error message is provided as a string). + Other(&'static str), +} + +impl From for CoseOpensslCipherError { + fn from(value: ErrorStack) -> Self { + CoseOpensslCipherError::OpensslError(value) + } +} + +impl From for CoseOpensslCipherError { + fn from(value: openssl::aes::KeyError) -> Self { + CoseOpensslCipherError::AesKeyError(value) + } +} + +impl From for CoseCipherError { + fn from(value: ErrorStack) -> Self { + CoseCipherError::Other(value.into()) + } +} + +impl From for CoseCipherError { + fn from(value: openssl::aes::KeyError) -> Self { + CoseCipherError::Other(value.into()) + } +} + +/// Context for the OpenSSL cryptographic backend. +/// +/// Can be used as a [`CryptoBackend`] for COSE operations. +/// +/// Generic properties of this backend: +/// - [ ] Can derive EC public key components if only the private component (d) is present. +/// - [ ] Can work with compressed EC public keys (EC keys using point compression) +/// +/// Algorithm support: +/// - Signature Algorithms (for COSE_Sign and COSE_Sign1) +/// - [x] ECDSA +/// - [x] ES256 +/// - [x] ES384 +/// - [x] ES512 +/// - [ ] ES256K +/// - [ ] EdDSA +/// - Message Authentication Code Algorithms (for COSE_Mac and COSE_Mac0) +/// - [x] HMAC +/// - [ ] HMAC 256/64 +/// - [x] HMAC 256/256 +/// - [x] HMAC 384/384 +/// - [x] HMAC 512/512 +/// - [ ] AES-CBC-MAC +/// - [ ] AES-MAC 128/64 +/// - [ ] AES-MAC 256/64 +/// - [ ] AES-MAC 128/128 +/// - [ ] AES-MAC 256/128 +/// - Content Encryption Algorithms (for COSE_Encrypt and COSE_Encrypt0) +/// - [x] AES-GCM +/// - [x] A128GCM +/// - [x] A192GCM +/// - [x] A256GCM +/// - [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 +/// - [ ] Direct Key with KDF +/// - [ ] direct+HKDF-SHA-256 +/// - [ ] direct+HKDF-SHA-512 +/// - [ ] direct+HKDF-AES-128 +/// - [ ] direct+HKDF-AES-256 +/// - Key Wrap +/// - [x] AES Key Wrap +/// - [x] A128KW +/// - [x] A192KW +/// - [x] A256KW +/// - Direct Key Agreement +/// - [ ] Direct ECDH +/// - [ ] ECDH-ES + HKDF-256 +/// - [ ] ECDH-ES + HKDF-512 +/// - [ ] ECDH-SS + HKDF-256 +/// - [ ] ECDH-SS + HKDF-512 +/// - Key Agreement with Key Wrap +/// - [ ] ECDH with Key Wrap +/// - [ ] ECDH-ES + A128KW +/// - [ ] ECDH-ES + A192KW +/// - [ ] ECDH-ES + A256KW +/// - [ ] ECDH-SS + A128KW +/// - [ ] ECDH-SS + A192KW +/// - [ ] ECDH-SS + A256KW +/// +/// Elliptic Curve support (for EC algorithms): +/// - ES256/ES384/ES512 [^1] +/// - [x] P-256 +/// - [x] P-384 +/// - [x] P-521 +/// - ES256K +/// - [ ] secp256k1 +/// - EdDSA +/// - [ ] Ed448 +/// - [ ] Ed25519 +/// - ECDH +/// - [ ] X448 +/// - [ ] X25519 +/// +/// [^1]: RFC 9053, Section 2.1 suggests using ES256 only with curve P-256, ES384 with curve P-384 +/// and ES512 only with curve P-521. +#[derive(Default)] +pub struct OpensslContext {} + +impl OpensslContext { + /// Creates a new OpenSSL context for use with COSE algorithms. + #[must_use] + pub fn new() -> OpensslContext { + OpensslContext {} + } +} + +impl CryptoBackend for OpensslContext { + type Error = CoseOpensslCipherError; + + fn generate_rand(&mut self, buf: &mut [u8]) -> Result<(), Self::Error> { + openssl::rand::rand_bytes(buf).map_err(CoseOpensslCipherError::from) + } +} + +/// Converts the provided [`iana::Algorithm`] to an OpenSSL [`CipherRef`] that can be used for a +/// symmetric [`CipherCtx`](openssl::cipher_ctx::CipherCtx). +fn algorithm_to_cipher( + algorithm: iana::Algorithm, +) -> Result<&'static CipherRef, CoseCipherError> { + match algorithm { + 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, + ))), + } +} + +/// Determine the hash function (represented in OpenSSL as a [`MessageDigest`]) that should be used +/// for a given [`iana::Algorithm`]. +fn get_algorithm_hash_function( + alg: iana::Algorithm, +) -> Result> { + match alg { + iana::Algorithm::ES256 | iana::Algorithm::HMAC_256_256 => Ok(MessageDigest::sha256()), + iana::Algorithm::ES384 | iana::Algorithm::HMAC_384_384 => Ok(MessageDigest::sha384()), + iana::Algorithm::ES512 | iana::Algorithm::HMAC_512_512 => Ok(MessageDigest::sha512()), + v => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( + v, + ))), + } +} diff --git a/src/token/cose/crypto_impl/openssl/sign.rs b/src/token/cose/crypto_impl/openssl/sign.rs new file mode 100644 index 0000000..e45d6ef --- /dev/null +++ b/src/token/cose/crypto_impl/openssl/sign.rs @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2022-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 crate::error::CoseCipherError; +use crate::token::cose::crypto_impl::openssl::{CoseOpensslCipherError, OpensslContext}; +use crate::token::cose::{crypto_impl, CoseEc2Key, EllipticCurve}; +use crate::token::SignCryptoBackend; +use alloc::vec::Vec; +use coset::iana; +use openssl::bn::BigNum; +use openssl::ec::{EcGroup, EcKey}; +use openssl::ecdsa::EcdsaSig; +use openssl::hash::MessageDigest; +use openssl::nid::Nid; +use openssl::pkey::{PKey, Private, Public}; +use openssl::sign::{Signer, Verifier}; + +impl SignCryptoBackend for OpensslContext { + fn sign_ecdsa( + &mut self, + alg: iana::Algorithm, + key: &CoseEc2Key<'_, Self::Error>, + target: &[u8], + ) -> Result, CoseCipherError> { + let (pad_size, group) = get_ecdsa_group_params(key)?; + let hash = super::get_algorithm_hash_function(alg)?; + + // Possible truncation is fine, the key size will never exceed the size of an i32. + #[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] + sign_ecdsa(&group, pad_size as i32, hash, key, target) + } + + fn verify_ecdsa( + &mut self, + alg: iana::Algorithm, + key: &CoseEc2Key<'_, Self::Error>, + signature: &[u8], + target: &[u8], + ) -> Result<(), CoseCipherError> { + let (pad_size, group) = get_ecdsa_group_params(key)?; + let hash = crypto_impl::openssl::get_algorithm_hash_function(alg)?; + + verify_ecdsa(&group, pad_size, hash, key, signature, target) + } +} + +/// Determine the openssl [`EcGroup`] instance and coordinate size that should be used for the given +/// ECDSA key (based on its curve). +fn get_ecdsa_group_params( + key: &CoseEc2Key<'_, CoseOpensslCipherError>, +) -> Result<(usize, EcGroup), CoseCipherError> { + match &key.crv { + EllipticCurve::Assigned(iana::EllipticCurve::P_256) => { + // ECDSA using P-256 curve, coordinates are padded to 256 bits + Ok((32, EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap())) + } + EllipticCurve::Assigned(iana::EllipticCurve::P_384) => { + // ECDSA using P-384 curve, coordinates are padded to 384 bits + Ok((48, EcGroup::from_curve_name(Nid::SECP384R1).unwrap())) + } + EllipticCurve::Assigned(iana::EllipticCurve::P_521) => { + // ECDSA using P-521 curve, coordinates are padded to 528 bits (521 bits rounded up + // to the nearest full bytes). + Ok((66, EcGroup::from_curve_name(Nid::SECP521R1).unwrap())) + } + v => Err(CoseCipherError::UnsupportedCurve(v.clone())), + } +} + +/// Perform an ECDSA signature operation with the given parameters. +fn sign_ecdsa( + group: &EcGroup, + pad_size: i32, + hash: MessageDigest, + key: &CoseEc2Key<'_, CoseOpensslCipherError>, + target: &[u8], +) -> Result, CoseCipherError> { + let private_key = cose_ec2_to_ec_private_key(key, group).map_err(CoseCipherError::from)?; + + let mut signer = Signer::new( + hash, + &*PKey::from_ec_key(private_key).map_err(CoseOpensslCipherError::from)?, + ) + .map_err(CoseOpensslCipherError::from)?; + + // generated signature is of DER format, need to convert it to COSE key format + let der_signature = signer + .sign_oneshot_to_vec(target) + .map_err(CoseOpensslCipherError::from) + .map_err(CoseCipherError::from)?; + + let ecdsa_sig = EcdsaSig::from_der(der_signature.as_slice()) + .map_err(CoseOpensslCipherError::from) + .map_err(CoseCipherError::from)?; + + // See RFC 8152, section 8.1 + let mut sig = ecdsa_sig + .r() + .to_vec_padded(pad_size) + .map_err(CoseOpensslCipherError::from) + .map_err(CoseCipherError::from)?; + let mut s_vec = ecdsa_sig + .s() + .to_vec_padded(pad_size) + .map_err(CoseOpensslCipherError::from) + .map_err(CoseCipherError::from)?; + sig.append(&mut s_vec); + + Ok(sig) +} + +/// Perform an ECDSA verification operation with the given parameters. +fn verify_ecdsa( + group: &EcGroup, + pad_size: usize, + hash: MessageDigest, + key: &CoseEc2Key<'_, CoseOpensslCipherError>, + signature: &[u8], + signed_data: &[u8], +) -> Result<(), CoseCipherError> { + let public_key = cose_ec2_to_ec_public_key(key, group).map_err(CoseCipherError::from)?; + let pkey = PKey::from_ec_key(public_key).map_err(CoseOpensslCipherError::from)?; + + let mut verifier = Verifier::new(hash, &pkey).map_err(CoseOpensslCipherError::from)?; + + // signature is in COSE format, need to convert to DER format. + let r = BigNum::from_slice(&signature[..pad_size]).map_err(CoseOpensslCipherError::from)?; + let s = BigNum::from_slice(&signature[pad_size..]).map_err(CoseOpensslCipherError::from)?; + let signature = + EcdsaSig::from_private_components(r, s).map_err(CoseOpensslCipherError::from)?; + // Note: EcdsaSig has its own "verify" method, but it is deprecated since OpenSSL + // 3.0, which is why it's not used here. + let der_signature = signature.to_der().map_err(CoseOpensslCipherError::from)?; + + verifier + .verify_oneshot(der_signature.as_slice(), signed_data) + .map_err(CoseOpensslCipherError::from) + .map_err(CoseCipherError::from) + .and_then(|verification_successful| { + verification_successful + .then_some(()) + .ok_or(CoseCipherError::VerificationFailure) + }) +} + +/// Converts a private [`CoseEc2Key`] instance to its corresponding representation as an [`EcKey`] +/// in `openssl`. +fn cose_ec2_to_ec_private_key( + key: &CoseEc2Key<'_, CoseOpensslCipherError>, + group: &EcGroup, +) -> Result, CoseCipherError> { + let public_key = cose_ec2_to_ec_public_key(key, group)?; + + EcKey::::from_private_components( + group, + &*BigNum::from_slice( + // According to the contract of the trait, this should be ensured by the caller, so it's + // fine to panic here. + key.d + .expect("key provided to backend has no private component"), + ) + .map_err(CoseCipherError::::from)?, + public_key.public_key(), + ) + .map_err(CoseCipherError::::from) +} + +/// Converts a public [`CoseEc2Key`] instance to its corresponding representation as an [`EcKey`] +/// in `openssl`. +fn cose_ec2_to_ec_public_key( + key: &CoseEc2Key<'_, CoseOpensslCipherError>, + group: &EcGroup, +) -> Result, CoseCipherError> { + // TODO X and Y can be recomputed and are not strictly required if D is known + // (RFC 8152, Section 13.1.1) + if key.x.is_none() || key.y.is_none() { + return Err(CoseCipherError::UnsupportedKeyDerivation); + } + + EcKey::::from_public_key_affine_coordinates( + group, + &*BigNum::from_slice(key.x.unwrap()) + .map_err(CoseCipherError::::from)?, + &*BigNum::from_slice(key.y.unwrap()) + .map_err(CoseCipherError::::from)?, + ) + .map_err(CoseCipherError::::from) +} diff --git a/src/token/cose/crypto_impl/rustcrypto/mac/mod.rs b/src/token/cose/crypto_impl/rustcrypto/mac/mod.rs index d0e3805..03a89ca 100644 --- a/src/token/cose/crypto_impl/rustcrypto/mac/mod.rs +++ b/src/token/cose/crypto_impl/rustcrypto/mac/mod.rs @@ -8,11 +8,12 @@ * * SPDX-License-Identifier: MIT OR Apache-2.0 */ -use coset::iana::Algorithm; -use crate::error::CoseCipherError; +use crate::token::cose::crypto_impl::rustcrypto::CoseCipherError; use crate::token::cose::crypto_impl::rustcrypto::RustCryptoContext; -use crate::token::cose::{CoseSymmetricKey, MacCryptoBackend}; +use crate::token::cose::CoseSymmetricKey; +use crate::token::cose::MacCryptoBackend; +use coset::iana; use rand::{CryptoRng, RngCore}; #[cfg(feature = "rustcrypto-hmac")] @@ -22,7 +23,7 @@ impl MacCryptoBackend for RustCryptoContext { #[cfg(feature = "rustcrypto-hmac")] fn compute_hmac( &mut self, - algorithm: Algorithm, + algorithm: iana::Algorithm, key: CoseSymmetricKey<'_, Self::Error>, payload: &[u8], ) -> Result, CoseCipherError> { @@ -32,7 +33,7 @@ impl MacCryptoBackend for RustCryptoContext { #[cfg(feature = "rustcrypto-hmac")] fn verify_hmac( &mut self, - algorithm: Algorithm, + algorithm: iana::Algorithm, key: CoseSymmetricKey<'_, Self::Error>, tag: &[u8], payload: &[u8], diff --git a/src/token/cose/crypto_impl/rustcrypto/sign/mod.rs b/src/token/cose/crypto_impl/rustcrypto/sign/mod.rs index 46b5aec..311a9c3 100644 --- a/src/token/cose/crypto_impl/rustcrypto/sign/mod.rs +++ b/src/token/cose/crypto_impl/rustcrypto/sign/mod.rs @@ -8,12 +8,12 @@ * * SPDX-License-Identifier: MIT OR Apache-2.0 */ +use crate::token::cose::crypto_impl::rustcrypto::CoseCipherError; +use crate::token::cose::CoseEc2Key; use coset::iana; use rand::{CryptoRng, RngCore}; -use crate::error::CoseCipherError; use crate::token::cose::crypto_impl::rustcrypto::RustCryptoContext; -use crate::token::cose::CoseEc2Key; use crate::token::SignCryptoBackend; #[cfg(feature = "rustcrypto-ecdsa")] diff --git a/src/token/cose/encrypted/encrypt/mod.rs b/src/token/cose/encrypted/encrypt/mod.rs index d68734b..edee572 100644 --- a/src/token/cose/encrypted/encrypt/mod.rs +++ b/src/token/cose/encrypted/encrypt/mod.rs @@ -46,7 +46,7 @@ pub trait CoseEncryptBuilderExt: Sized { /// override headers previously set using /// [`CoseEncryptBuilder::unprotected`](CoseEncryptBuilder). /// - `payload` - payload which should be added to the resulting - /// [`CoseEncrypt`](coset::CoseEncrypt) instance in encrypted form. + /// [`CoseEncrypt`](CoseEncrypt) instance in encrypted form. /// Will override a payload previously set using /// [`CoseEncryptBuilder::payload`](CoseEncryptBuilder). /// - `external_aad` - provider of additional authenticated data that should be included in the diff --git a/src/token/cose/encrypted/encrypt/tests.rs b/src/token/cose/encrypted/encrypt/tests.rs index f0f3266..13d90d1 100644 --- a/src/token/cose/encrypted/encrypt/tests.rs +++ b/src/token/cose/encrypted/encrypt/tests.rs @@ -19,7 +19,7 @@ use rstest::rstest; use crate::token::cose::encrypted::encrypt::{CoseEncryptBuilderExt, CoseEncryptExt}; use crate::token::cose::encrypted::EncryptCryptoBackend; -use crate::token::cose::header_util::{determine_algorithm, HeaderBuilderExt}; +use crate::token::cose::header::HeaderBuilderExt; use crate::token::cose::key::CoseSymmetricKey; use crate::token::cose::recipient::{CoseRecipientBuilderExt, KeyDistributionCryptoBackend}; use crate::token::cose::test_helper::{ @@ -27,7 +27,7 @@ use crate::token::cose::test_helper::{ perform_cose_reference_output_test, perform_cose_self_signed_test, serialize_cose_with_failures, CoseStructTestHelper, TestCase, }; -use crate::token::cose::{determine_header_param, CryptoBackend}; +use crate::token::cose::{util::determine_header_param, CryptoBackend}; #[cfg(feature = "openssl")] use crate::token::cose::test_helper::openssl_ctx; @@ -36,6 +36,7 @@ use crate::token::cose::test_helper::openssl_ctx; feature = "rustcrypto-aes-kw" ))] use crate::token::cose::test_helper::rustcrypto_ctx; +use crate::token::cose::util::determine_algorithm; impl CoseStructTestHelper for CoseEncrypt diff --git a/src/token/cose/encrypted/encrypt0/tests.rs b/src/token/cose/encrypted/encrypt0/tests.rs index 3a39e53..f2e48b6 100644 --- a/src/token/cose/encrypted/encrypt0/tests.rs +++ b/src/token/cose/encrypted/encrypt0/tests.rs @@ -15,7 +15,7 @@ use rstest::rstest; use crate::token::cose::encrypted::encrypt0::{CoseEncrypt0BuilderExt, CoseEncrypt0Ext}; use crate::token::cose::encrypted::EncryptCryptoBackend; -use crate::token::cose::header_util::HeaderBuilderExt; +use crate::token::cose::header::HeaderBuilderExt; use crate::token::cose::test_helper::{ apply_attribute_failures, apply_header_failures, perform_cose_reference_output_test, perform_cose_self_signed_test, serialize_cose_with_failures, CoseStructTestHelper, TestCase, diff --git a/src/token/cose/encrypted/mod.rs b/src/token/cose/encrypted/mod.rs index 9e022e5..e348fc2 100644 --- a/src/token/cose/encrypted/mod.rs +++ b/src/token/cose/encrypted/mod.rs @@ -11,26 +11,23 @@ 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; -use crate::token::cose::header_util::HeaderParam; use crate::token::cose::key::{CoseParsedKey, CoseSymmetricKey, KeyProvider}; -use crate::token::cose::{header_util, key, CryptoBackend, KeyParam}; +use crate::token::cose::CryptoBackend; 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, +}; pub use encrypt::{CoseEncryptBuilderExt, CoseEncryptExt}; pub use encrypt0::{CoseEncrypt0BuilderExt, CoseEncrypt0Ext}; -/// 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; - /// Trait for cryptographic backends that can perform encryption and decryption operations for /// algorithms used for COSE. pub trait EncryptCryptoBackend: CryptoBackend { @@ -285,133 +282,6 @@ pub trait EncryptCryptoBackend: CryptoBackend { } } -/// 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(12), - // 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, - ))), - } -} - -/// 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. -fn determine_and_check_aes_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 = 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) - }); - - let partial_iv = header_util::determine_header_param(protected, unprotected, |v| { - (!v.partial_iv.is_empty()).then_some(&v.partial_iv) - }); - - let expected_iv_len = aes_algorithm_iv_len(alg)?; - - let iv = match (iv, partial_iv) { - // IV and partial IV must not be set at the same time. - (Some(_iv), Some(partial_iv)) => Err(CoseCipherError::InvalidHeaderParam( - HeaderParam::Generic(iana::HeaderParameter::PartialIv), - Value::Bytes(partial_iv.clone()), - )), - (Some(iv), None) => Ok(iv.clone()), - // See https://datatracker.ietf.org/doc/html/rfc9052#section-3.1 - (None, Some(partial_iv)) => { - let context_iv = (!symm_key.as_ref().base_iv.is_empty()) - .then(|| &symm_key.as_ref().base_iv) - .ok_or(CoseCipherError::MissingKeyParam(vec![KeyParam::Common( - iana::KeyParameter::BaseIv, - )]))?; - - if partial_iv.len() > expected_iv_len { - return Err(CoseCipherError::InvalidHeaderParam( - HeaderParam::Generic(iana::HeaderParameter::PartialIv), - Value::Bytes(partial_iv.clone()), - )); - } - - if context_iv.len() > expected_iv_len { - return Err(CoseCipherError::InvalidKeyParam( - KeyParam::Common(iana::KeyParameter::BaseIv), - Value::Bytes(context_iv.clone()), - )); - } - - let mut message_iv = vec![0u8; expected_iv_len]; - - // Left-pad the Partial IV with zeros to the length of IV - message_iv[(expected_iv_len - partial_iv.len())..].copy_from_slice(partial_iv); - // XOR the padded Partial IV with the Context IV. - message_iv - .iter_mut() - .zip(context_iv.iter().chain(core::iter::repeat(&0u8))) - .for_each(|(b1, b2)| *b1 ^= *b2); - Ok(message_iv) - } - (None, None) => Err(CoseCipherError::MissingHeaderParam(HeaderParam::Generic( - iana::HeaderParameter::Iv, - ))), - }?; - - if iv.len() != expected_iv_len { - 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 /// [`CoseEncrypt0`](coset::CoseEncrypt0) structure with the given `protected` and `unprotected` /// headers, `plaintext` and `enc_structure` using the given `backend` and `key_provider`. @@ -431,7 +301,7 @@ fn try_encrypt( // (RFC 9052, Section 5.3). enc_structure: &[u8], ) -> Result, CoseCipherError> { - header_util::try_cose_crypto_operation( + try_cose_crypto_operation( key_provider, protected, unprotected, @@ -487,7 +357,7 @@ pub(crate) fn try_decrypt( // (RFC 9052, Section 5.3). enc_structure: &[u8], ) -> Result, CoseCipherError> { - header_util::try_cose_crypto_operation( + try_cose_crypto_operation( key_provider, Some(protected), Some(unprotected), diff --git a/src/token/cose/header.rs b/src/token/cose/header.rs new file mode 100644 index 0000000..d25e9ae --- /dev/null +++ b/src/token/cose/header.rs @@ -0,0 +1,54 @@ +use crate::error::CoseCipherError; +use crate::token::cose::util::aes_algorithm_iv_len; +use crate::token::cose::{CryptoBackend, EncryptCryptoBackend}; +use coset::{iana, HeaderBuilder}; + +/// Extensions to the [`HeaderBuilder`] type that enable usage of cryptographic backends. +pub trait HeaderBuilderExt: Sized { + /// Generate an initialization vector for the given `algorithm` using the given + /// cryptographic `backend`. + /// + /// # Errors + /// + /// Returns an error if the `algorithm` is unsupported/unknown or the cryptographic backend + /// returns an error. + fn gen_iv( + self, + backend: &mut B, + algorithm: iana::Algorithm, + ) -> Result>; +} + +impl HeaderBuilderExt for HeaderBuilder { + fn gen_iv( + self, + backend: &mut B, + alg: iana::Algorithm, + ) -> Result> { + let iv_size = aes_algorithm_iv_len(alg)?; + let mut iv = vec![0; iv_size]; + backend.generate_rand(&mut iv)?; + Ok(self.iv(iv)) + } +} + +/// A header parameter that can be used in a COSE header. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HeaderParam { + /// Generic header parameter applicable to all algorithms. + Generic(iana::HeaderParameter), + /// Header parameter that is specific for a set of algorithms. + Algorithm(iana::HeaderAlgorithmParameter), +} + +impl From for HeaderParam { + fn from(value: iana::HeaderParameter) -> Self { + HeaderParam::Generic(value) + } +} + +impl From for HeaderParam { + fn from(value: iana::HeaderAlgorithmParameter) -> Self { + HeaderParam::Algorithm(value) + } +} diff --git a/src/token/cose/key.rs b/src/token/cose/key.rs index df4fbbb..9209b06 100644 --- a/src/token/cose/key.rs +++ b/src/token/cose/key.rs @@ -14,10 +14,9 @@ use core::borrow::Borrow; use core::fmt::Display; use core::marker::PhantomData; use coset::iana::EnumI64; -use coset::{iana, Algorithm, AsCborValue, CoseKey, KeyType, Label, RegisteredLabelWithPrivate}; +use coset::{iana, AsCborValue, CoseKey, KeyType, Label, RegisteredLabelWithPrivate}; use crate::error::CoseCipherError; -use crate::token::cose::CryptoBackend; /// Finds a key parameter by its label. fn find_param_by_label<'a>(label: &Label, param_vec: &'a [(Label, Value)]) -> Option<&'a Value> { @@ -475,186 +474,3 @@ impl KeyProvider for &T { (*self).lookup_key(key_id) } } - -/// Determines the key size that a key for the given `algorithm` should have. -fn symmetric_key_size( - algorithm: iana::Algorithm, -) -> Result> { - match algorithm { - iana::Algorithm::A128GCM - | 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::A128KW => Ok(16), - iana::Algorithm::A192GCM | iana::Algorithm::A192KW => Ok(24), - iana::Algorithm::A256GCM - | 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 - | iana::Algorithm::A256KW => Ok(32), - _ => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( - algorithm, - ))), - } -} - -/// Attempts to parse the given `parsed_key` as an AES symmetric key. -/// -/// 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( - algorithm: iana::Algorithm, - parsed_key: CoseParsedKey, -) -> Result, CoseCipherError> { - // Checks according to RFC 9053, Section 4.1 and 4.2. - - // Key type must be symmetric. - let symm_key = if let CoseParsedKey::Symmetric(symm_key) = parsed_key { - symm_key - } else { - return Err(CoseCipherError::KeyTypeAlgorithmMismatch( - parsed_key.as_ref().kty.clone(), - Algorithm::Assigned(algorithm), - )); - }; - - // Algorithm in key must match algorithm to use. - if let Some(key_alg) = &symm_key.as_ref().alg { - if key_alg != &Algorithm::Assigned(algorithm) { - return Err(CoseCipherError::KeyAlgorithmMismatch( - key_alg.clone(), - Algorithm::Assigned(algorithm), - )); - } - } - - // For algorithms that we know, check the key length (would lead to a cipher error later on). - let key_len = symmetric_key_size(algorithm)?; - if symm_key.k.len() != key_len { - return Err(CoseCipherError::InvalidKeyParam( - KeyParam::Symmetric(iana::SymmetricKeyParameter::K), - Value::Bytes(symm_key.k.to_vec()), - )); - } - - Ok(symm_key) -} - -/// Generates a random content encryption key for the given `algorithm` using the given `backend`. -pub(crate) fn generate_cek_for_alg( - backend: &mut B, - algorithm: iana::Algorithm, -) -> Result, CoseCipherError> { - let key_len = symmetric_key_size(algorithm)?; - let mut key = vec![0u8; key_len]; - backend.generate_rand(key.as_mut_slice())?; - Ok(key) -} - -/// Attempts to parse the given `parsed_key` as an ECDSA key. -/// -/// Performs the checks required for ECDSA keys according to -/// [RFC 9053, Section 2.1](https://datatracker.ietf.org/doc/html/rfc9053#section-2.1) and/or -/// [RFC 8812, Section 3.2](https://datatracker.ietf.org/doc/html/rfc8812#section-3.2). -pub(crate) fn ensure_valid_ecdsa_key( - algorithm: iana::Algorithm, - parsed_key: CoseParsedKey, - key_should_be_private: bool, -) -> Result, CoseCipherError> { - // Checks according to RFC 9053, Section 2.1 or RFC 8812, Section 3.2. - - // Key type must be EC2 - let ec2_key = if let CoseParsedKey::Ec2(ec2_key) = parsed_key { - ec2_key - } else { - return Err(CoseCipherError::KeyTypeAlgorithmMismatch( - parsed_key.as_ref().kty.clone(), - Algorithm::Assigned(algorithm), - )); - }; - - // If algorithm in key is set, it must match our algorithm - if let Some(key_alg) = &ec2_key.as_ref().alg { - if key_alg != &Algorithm::Assigned(algorithm) { - return Err(CoseCipherError::KeyAlgorithmMismatch( - key_alg.clone(), - Algorithm::Assigned(algorithm), - )); - } - } - - // Key must contain private key information to perform signature, and either D or X and Y to - // verify a signature. - if key_should_be_private && ec2_key.d.is_none() { - return Err(CoseCipherError::MissingKeyParam(vec![ - iana::Ec2KeyParameter::D.into(), - ])); - } else if !key_should_be_private && ec2_key.d.is_none() { - if ec2_key.x.is_none() { - return Err(CoseCipherError::MissingKeyParam(vec![ - iana::Ec2KeyParameter::X.into(), - iana::Ec2KeyParameter::D.into(), - ])); - } - if ec2_key.y.is_none() { - return Err(CoseCipherError::MissingKeyParam(vec![ - iana::Ec2KeyParameter::Y.into(), - iana::Ec2KeyParameter::D.into(), - ])); - } - } - - Ok(ec2_key) -} - -/// Attempts to parse the given `parsed_key` as an HMAC key. -/// -/// Performs the checks required for symmetric keys suitable for HMAC according to -/// [RFC 9053, Section 3.1](https://datatracker.ietf.org/doc/html/rfc9053#section-3.1). -pub(crate) fn ensure_valid_hmac_key( - algorithm: iana::Algorithm, - parsed_key: CoseParsedKey, -) -> Result, CoseCipherError> { - // Checks according to RFC 9053, Section 3.1. - - // Key type must be symmetric. - let symm_key = if let CoseParsedKey::Symmetric(symm_key) = parsed_key { - symm_key - } else { - return Err(CoseCipherError::KeyTypeAlgorithmMismatch( - parsed_key.as_ref().kty.clone(), - Algorithm::Assigned(algorithm), - )); - }; - - // Algorithm in key must match algorithm to use. - if let Some(key_alg) = &symm_key.as_ref().alg { - if key_alg != &Algorithm::Assigned(algorithm) { - return Err(CoseCipherError::KeyAlgorithmMismatch( - key_alg.clone(), - Algorithm::Assigned(algorithm), - )); - } - } - - // For algorithms that we know, check the key length (would lead to a cipher error later on). - let key_len = match algorithm { - iana::Algorithm::HMAC_256_256 | iana::Algorithm::HMAC_256_64 => Some(32), - iana::Algorithm::HMAC_384_384 => Some(48), - iana::Algorithm::HMAC_512_512 => Some(64), - _ => None, - }; - - if let Some(key_len) = key_len { - if symm_key.k.len() != key_len { - return Err(CoseCipherError::InvalidKeyParam( - KeyParam::Symmetric(iana::SymmetricKeyParameter::K), - Value::Bytes(symm_key.k.to_vec()), - )); - } - } - - Ok(symm_key) -} diff --git a/src/token/cose/maced/mac/tests.rs b/src/token/cose/maced/mac/tests.rs index c2fbc09..f574403 100644 --- a/src/token/cose/maced/mac/tests.rs +++ b/src/token/cose/maced/mac/tests.rs @@ -18,7 +18,6 @@ use coset::{ }; use rstest::rstest; -use crate::token::cose::header_util::determine_algorithm; use crate::token::cose::key::CoseSymmetricKey; use crate::token::cose::maced::mac::{CoseMacBuilderExt, CoseMacExt}; use crate::token::cose::maced::MacCryptoBackend; @@ -28,6 +27,7 @@ use crate::token::cose::test_helper::{ apply_attribute_failures, apply_header_failures, serialize_cose_with_failures, CoseStructTestHelper, TestCase, }; +use crate::token::cose::util::determine_algorithm; use crate::token::cose::{test_helper, CryptoBackend}; #[cfg(feature = "openssl")] diff --git a/src/token/cose/maced/mac0/tests.rs b/src/token/cose/maced/mac0/tests.rs index 6a349cb..3162522 100644 --- a/src/token/cose/maced/mac0/tests.rs +++ b/src/token/cose/maced/mac0/tests.rs @@ -15,13 +15,13 @@ use coset::iana::Algorithm; use coset::{CoseError, CoseKey, CoseKeyBuilder, CoseMac0, CoseMac0Builder, Header}; use rstest::rstest; -use crate::token::cose::header_util::determine_algorithm; use crate::token::cose::maced::mac0::{CoseMac0BuilderExt, CoseMac0Ext}; use crate::token::cose::maced::MacCryptoBackend; use crate::token::cose::test_helper::{ apply_attribute_failures, apply_header_failures, serialize_cose_with_failures, CoseStructTestHelper, TestCase, }; +use crate::token::cose::util::determine_algorithm; use crate::token::cose::{test_helper, CryptoBackend}; #[cfg(feature = "openssl")] diff --git a/src/token/cose/maced/mod.rs b/src/token/cose/maced/mod.rs index 05efd78..ac137b8 100644 --- a/src/token/cose/maced/mod.rs +++ b/src/token/cose/maced/mod.rs @@ -20,7 +20,8 @@ pub use mac0::{CoseMac0BuilderExt, CoseMac0Ext}; use crate::error::CoseCipherError; use crate::token::cose::key::{CoseParsedKey, CoseSymmetricKey, KeyProvider}; -use crate::token::cose::{header_util, key, CryptoBackend}; +use crate::token::cose::util::{ensure_valid_hmac_key, try_cose_crypto_operation}; +use crate::token::cose::CryptoBackend; mod mac; mod mac0; @@ -157,7 +158,7 @@ fn try_compute( unprotected: Option<&Header>, payload: &[u8], ) -> Result, CoseCipherError> { - header_util::try_cose_crypto_operation( + try_cose_crypto_operation( key_provider, protected, unprotected, @@ -169,7 +170,7 @@ fn try_compute( iana::Algorithm::HMAC_256_256 | iana::Algorithm::HMAC_384_384 | iana::Algorithm::HMAC_512_512 => { - let symm_key = key::ensure_valid_hmac_key(alg, parsed_key)?; + let symm_key = ensure_valid_hmac_key(alg, parsed_key)?; backend.compute_hmac(alg, symm_key, payload) } alg => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( @@ -197,7 +198,7 @@ pub(crate) fn try_verify( tag: &[u8], payload: &[u8], ) -> Result<(), CoseCipherError> { - header_util::try_cose_crypto_operation( + try_cose_crypto_operation( key_provider, Some(protected), Some(unprotected), @@ -209,7 +210,7 @@ pub(crate) fn try_verify( iana::Algorithm::HMAC_256_256 | iana::Algorithm::HMAC_384_384 | iana::Algorithm::HMAC_512_512 => { - let symm_key = key::ensure_valid_hmac_key(alg, parsed_key)?; + let symm_key = ensure_valid_hmac_key(alg, parsed_key)?; (*backend.borrow_mut()).verify_hmac(alg, symm_key, tag, payload) } alg => Err(CoseCipherError::UnsupportedAlgorithm(Algorithm::Assigned( diff --git a/src/token/cose/mod.rs b/src/token/cose/mod.rs index 8fd7a79..0325a1b 100644 --- a/src/token/cose/mod.rs +++ b/src/token/cose/mod.rs @@ -208,19 +208,20 @@ use core::fmt::{Debug, Display}; pub mod crypto_impl; mod encrypted; -mod header_util; mod key; mod signed; mod maced; mod recipient; +mod header; + /// AAD providers and operations for those. pub mod aad; pub use aad::AadProvider; pub use encrypted::*; -pub use header_util::*; +pub use header::*; pub use key::*; pub use maced::*; pub use recipient::*; @@ -228,6 +229,8 @@ pub use signed::*; #[cfg(all(test, feature = "std"))] pub(crate) mod test_helper; +/// Utility functions for the cose module itself and for cryptographic backend implementors. +pub mod util; /// Trait for implementations of cryptographic functions that can be used for COSE structures. pub trait CryptoBackend { diff --git a/src/token/cose/recipient/mod.rs b/src/token/cose/recipient/mod.rs index 14554d0..a19e00f 100644 --- a/src/token/cose/recipient/mod.rs +++ b/src/token/cose/recipient/mod.rs @@ -22,12 +22,13 @@ use coset::{ use crate::error::CoseCipherError; use crate::token::cose::aad::{AadProvider, InvertedAadProvider}; -use crate::token::cose::header_util::{determine_algorithm, determine_key_candidates}; -use crate::token::cose::key::ensure_valid_aes_key; +use crate::token::cose::header::HeaderParam; use crate::token::cose::key::{CoseParsedKey, KeyProvider}; -use crate::token::cose::{ - determine_header_param, try_cose_crypto_operation, CoseSymmetricKey, CryptoBackend, HeaderParam, -}; +use crate::token::cose::util::determine_header_param; +use crate::token::cose::util::ensure_valid_aes_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}; /// Trait for cryptographic backends that can perform key distribution operations for algorithms /// used in COSE structures. diff --git a/src/token/cose/signed/mod.rs b/src/token/cose/signed/mod.rs index 9379798..7883de4 100644 --- a/src/token/cose/signed/mod.rs +++ b/src/token/cose/signed/mod.rs @@ -18,7 +18,8 @@ pub use sign1::{CoseSign1BuilderExt, CoseSign1Ext}; use crate::error::CoseCipherError; use crate::token::cose::key::{CoseEc2Key, CoseParsedKey, KeyProvider}; -use crate::token::cose::{header_util, key, CryptoBackend}; +use crate::token::cose::util::{ensure_valid_ecdsa_key, try_cose_crypto_operation}; +use crate::token::cose::CryptoBackend; mod sign; mod sign1; @@ -183,7 +184,7 @@ fn try_sign( unprotected: Option<&Header>, payload: &[u8], ) -> Result, CoseCipherError> { - header_util::try_cose_crypto_operation( + try_cose_crypto_operation( key_provider, protected, unprotected, @@ -196,7 +197,7 @@ fn try_sign( | iana::Algorithm::ES512 | iana::Algorithm::ES256K => { // Check if this is a valid ECDSA key. - let ec2_key = key::ensure_valid_ecdsa_key::(alg, parsed_key, true)?; + let ec2_key = ensure_valid_ecdsa_key::(alg, parsed_key, true)?; // Perform signing operation using backend. backend.sign_ecdsa(alg, &ec2_key, payload) @@ -227,7 +228,7 @@ fn try_verify( signature: &[u8], toverify: &[u8], ) -> Result<(), CoseCipherError> { - header_util::try_cose_crypto_operation( + try_cose_crypto_operation( key_provider, Some(protected), Some(unprotected), @@ -240,7 +241,7 @@ fn try_verify( | iana::Algorithm::ES512 | iana::Algorithm::ES256K => { // Check if this is a valid ECDSA key. - let ec2_key = key::ensure_valid_ecdsa_key::(alg, parsed_key, false)?; + let ec2_key = ensure_valid_ecdsa_key::(alg, parsed_key, false)?; backend.verify_ecdsa(alg, &ec2_key, signature, toverify) } diff --git a/src/token/cose/header_util.rs b/src/token/cose/util/header.rs similarity index 62% rename from src/token/cose/header_util.rs rename to src/token/cose/util/header.rs index 937a682..e80f495 100644 --- a/src/token/cose/header_util.rs +++ b/src/token/cose/util/header.rs @@ -15,33 +15,10 @@ use core::fmt::Display; use alloc::borrow::Borrow; use coset::iana::EnumI64; -use coset::{iana, Algorithm, CoseKey, Header, HeaderBuilder, KeyOperation, Label}; +use coset::{iana, Algorithm, CoseKey, Header, KeyOperation, Label}; use crate::error::CoseCipherError; use crate::token::cose::key::KeyProvider; -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)] -pub enum HeaderParam { - /// Generic header parameter applicable to all algorithms. - Generic(iana::HeaderParameter), - /// Header parameter that is specific for a set of algorithms. - Algorithm(iana::HeaderAlgorithmParameter), -} - -impl From for HeaderParam { - fn from(value: iana::HeaderParameter) -> Self { - HeaderParam::Generic(value) - } -} - -impl From for HeaderParam { - fn from(value: iana::HeaderAlgorithmParameter) -> Self { - HeaderParam::Algorithm(value) - } -} - /// Returns the set of header parameters that are set in the given `header_bucket`. fn create_header_parameter_set(header_bucket: &Header) -> BTreeSet