diff --git a/src/lib.rs b/src/lib.rs index 59cbb15..2aa99e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,7 +85,7 @@ //! Following up from the previous example, let's assume we now want to create a signed //! access token containing the existing `key`, as well as claims about the audience and issuer //! of the token, using an existing cipher of type `FakeCrypto`[^cipher]: -//! ``` +//! ```ignore //! # use ciborium::value::Value; //! # use coset::{AsCborValue, CoseKey, CoseKeyBuilder, Header, iana, Label, ProtectedHeader}; //! # use coset::cwt::{ClaimsSetBuilder, Timestamp}; diff --git a/src/token/cose/encrypt/encrypt0.rs b/src/token/cose/encrypt/encrypt0.rs index 7afeb1f..255c18f 100644 --- a/src/token/cose/encrypt/encrypt0.rs +++ b/src/token/cose/encrypt/encrypt0.rs @@ -10,26 +10,6 @@ use ciborium::Value; use core::fmt::Display; use coset::{iana, Algorithm, CoseEncrypt0, CoseEncrypt0Builder, Header, KeyOperation}; -pub trait CoseEncrypt0BuilderExt {} - -impl CoseEncrypt0BuilderExt for CoseEncrypt0Builder {} - -pub trait CoseEncrypt0Ext { - fn try_decrypt< - 'a, - 'b, - B: CoseEncryptCipher, - CKP: CoseKeyProvider<'a>, - CAP: CoseAadProvider<'b>, - >( - &self, - backend: &mut B, - key_provider: &mut CKP, - try_all_keys: bool, - external_aad: &mut CAP, - ) -> Result, CoseCipherError>; -} - fn is_valid_aes_key<'a, BE: Display>( algorithm: &Algorithm, parsed_key: CoseParsedKey<'a, BE>, @@ -90,8 +70,8 @@ fn is_valid_aes_key<'a, BE: Display>( fn try_encrypt_single<'a, 'b, B: CoseEncryptCipher, CKP: CoseKeyProvider<'a>>( backend: &mut B, key_provider: &mut CKP, - protected: &Header, - unprotected: &Header, + protected: Option<&Header>, + unprotected: Option<&Header>, try_all_keys: bool, plaintext: &[u8], // NOTE: aad ist not the external AAD provided by the user, but the Enc_structure as defined in RFC 9052, Section 5.3 @@ -99,15 +79,15 @@ fn try_encrypt_single<'a, 'b, B: CoseEncryptCipher, CKP: CoseKeyProvider<'a>>( ) -> Result, CoseCipherError> { let parsed_key = determine_key_candidates( key_provider, - Some(protected), - Some(unprotected), + protected, + unprotected, &KeyOperation::Assigned(iana::KeyOperation::Sign), false, )? .into_iter() .next() .ok_or(CoseCipherError::NoKeyFound)?; - let algorithm = determine_algorithm(&parsed_key, Some(protected), Some(unprotected))?; + let algorithm = determine_algorithm(&parsed_key, protected, unprotected)?; match algorithm { Algorithm::Assigned( @@ -116,10 +96,10 @@ fn try_encrypt_single<'a, 'b, B: CoseEncryptCipher, CKP: CoseKeyProvider<'a>>( // Check if this is a valid AES key. let symm_key = is_valid_aes_key::(&algorithm, parsed_key)?; - let iv = if !protected.iv.is_empty() { - protected.iv.as_ref() - } else if !unprotected.iv.is_empty() { - unprotected.iv.as_ref() + let iv = if protected.is_some() && !protected.unwrap().iv.is_empty() { + protected.unwrap().iv.as_ref() + } else if unprotected.is_some() && !unprotected.unwrap().iv.is_empty() { + unprotected.unwrap().iv.as_ref() } else { return Err(CoseCipherError::IvRequired); }; @@ -205,6 +185,22 @@ fn try_decrypt<'a, 'b, B: CoseEncryptCipher, CKP: CoseKeyProvider<'a>>( Err(CoseCipherError::NoKeyFound) } +pub trait CoseEncrypt0Ext { + fn try_decrypt< + 'a, + 'b, + B: CoseEncryptCipher, + CKP: CoseKeyProvider<'a>, + CAP: CoseAadProvider<'b>, + >( + &self, + backend: &mut B, + key_provider: &mut CKP, + try_all_keys: bool, + external_aad: &mut CAP, + ) -> Result, CoseCipherError>; +} + impl CoseEncrypt0Ext for CoseEncrypt0 { fn try_decrypt< 'a, @@ -235,3 +231,323 @@ impl CoseEncrypt0Ext for CoseEncrypt0 { ) } } + +pub trait CoseEncrypt0BuilderExt: Sized { + fn try_encrypt< + 'a, + 'b, + B: CoseEncryptCipher, + CKP: CoseKeyProvider<'a>, + CAP: CoseAadProvider<'b>, + >( + self, + backend: &mut B, + key_provider: &mut CKP, + try_all_keys: bool, + protected: Option
, + unprotected: Option
, + plaintext: &[u8], + external_aad: &mut CAP, + ) -> Result>; +} + +impl CoseEncrypt0BuilderExt for CoseEncrypt0Builder { + fn try_encrypt< + 'a, + 'b, + B: CoseEncryptCipher, + CKP: CoseKeyProvider<'a>, + CAP: CoseAadProvider<'b>, + >( + self, + backend: &mut B, + key_provider: &mut CKP, + try_all_keys: bool, + protected: Option
, + unprotected: Option
, + plaintext: &[u8], + external_aad: &mut CAP, + ) -> Result> { + let mut builder = self; + if let Some(protected) = &protected { + builder = builder.protected(protected.clone()); + } + if let Some(unprotected) = &unprotected { + builder = builder.unprotected(unprotected.clone()); + } + builder.try_create_ciphertext( + plaintext, + external_aad.lookup_aad(protected.as_ref(), unprotected.as_ref()), + |ciphertext, aad| { + try_encrypt_single( + backend, + key_provider, + protected.as_ref(), + unprotected.as_ref(), + try_all_keys, + ciphertext, + aad, + ) + }, + ) + } +} + +#[cfg(all(test, feature = "std"))] +mod tests { + use crate::token::cose::crypto_impl::openssl::OpensslContext; + use crate::token::cose::encrypt::encrypt0::{CoseEncrypt0BuilderExt, CoseEncrypt0Ext}; + use crate::token::cose::encrypt::{CoseEncryptCipher, HeaderBuilderExt}; + use crate::token::cose::sign::CoseSign1BuilderExt; + use crate::token::cose::sign::CoseSign1Ext; + use crate::token::cose::sign::{CoseSignBuilderExt, CoseSignExt}; + use crate::token::cose::test_helper::{ + apply_attribute_failures, apply_header_failures, serialize_cose_with_failures, TestCase, + TestCaseEncrypted, TestCaseFailures, TestCaseInput, TestCaseRecipient, TestCaseSign, + }; + use crate::CoseSignCipher; + use base64::Engine; + use coset::iana::EnumI64; + use coset::{ + AsCborValue, CborSerializable, CoseEncrypt0, CoseEncrypt0Builder, CoseError, CoseKey, + CoseSign, CoseSign1, CoseSign1Builder, CoseSignBuilder, CoseSignature, + CoseSignatureBuilder, Header, HeaderBuilder, Label, TaggedCborSerializable, + }; + use hex::FromHex; + use rstest::rstest; + use serde::de::{MapAccess, Visitor}; + use serde::{Deserialize, Deserializer}; + use serde_json::Value; + use std::any::Any; + use std::path::PathBuf; + + fn serialize_encrypt0_and_apply_failures( + failures: &mut TestCaseFailures, + key: &mut CoseKey, + mut value: CoseEncrypt0, + ) -> (Option, Vec) { + if let Some(1) = &failures.change_tag { + let byte = value.ciphertext.as_mut().unwrap().first_mut().unwrap(); + *byte = byte.wrapping_add(1); + } + + apply_header_failures(&mut value.protected.header, &failures); + + let serialized_data = serialize_cose_with_failures(value, &failures); + + (apply_attribute_failures(key, &failures), serialized_data) + } + + fn verify_encrypt0_test_case( + backend: &mut T, + encrypt0: &CoseEncrypt0, + test_case: &mut TestCaseEncrypted, + expected_plaintext: &[u8], + should_fail: bool, + ) { + let keys: Vec = test_case + .recipients + .iter() + .map(|v| { + let mut key_with_alg = v.key.clone(); + if key_with_alg.alg.is_none() { + key_with_alg.alg = v.alg.map(|a| coset::Algorithm::Assigned(a)); + } + key_with_alg + }) + .collect(); + let mut aad = test_case.external.as_slice(); + + let verify_result = encrypt0.try_decrypt(backend, &mut &keys, false, &mut aad); + + if should_fail { + verify_result.expect_err("invalid token was successfully verified"); + } else { + let plaintext = verify_result.expect("unable to verify token"); + + assert_eq!(expected_plaintext, plaintext.as_slice()); + let empty_hdr = Header::default(); + // TODO IV is apprarently taken from rng_stream field, not header field, but still implicitly added to header. + // ugh... + let mut unprotected = test_case.unprotected.clone().unwrap_or_default(); + let mut protected = test_case.protected.clone().unwrap_or_default(); + unprotected.iv = encrypt0.unprotected.iv.clone(); + protected.iv = encrypt0.protected.header.iv.clone(); + assert_eq!(&unprotected, &encrypt0.unprotected); + assert_eq!(&protected, &encrypt0.protected.header); + } + } + + fn perform_encrypt0_reference_output_test( + test_path: PathBuf, + mut backend: impl CoseEncryptCipher, + ) { + let test_case_description: TestCase = serde_json::from_reader( + std::fs::File::open(test_path).expect("unable to open test case"), + ) + .expect("invalid test case"); + + let mut encrypt0_cfg = test_case_description + .input + .encrypted + .expect("expected a CoseSign test case, but it was not found"); + + let example_output = + match CoseEncrypt0::from_tagged_slice(test_case_description.output.cbor.as_slice()) + .or_else(|e1| { + CoseEncrypt0::from_slice(test_case_description.output.cbor.as_slice()) + .map_err(|e2| Result::::Err((e1, e2))) + }) { + Ok(v) => v, + e => { + if test_case_description.fail { + println!("test case failed as expected. Error: {:?}", e); + return; + } else { + e.expect("unable to deserialize test case data"); + unreachable!() + } + } + }; + + verify_encrypt0_test_case( + &mut backend, + &example_output, + &mut encrypt0_cfg, + test_case_description.input.plaintext.as_bytes(), + test_case_description.fail, + ) + } + + fn perform_encrypt0_self_signed_test(test_path: PathBuf, mut backend: impl CoseEncryptCipher) { + let mut test_case_description: TestCase = serde_json::from_reader( + std::fs::File::open(test_path).expect("unable to open test case"), + ) + .expect("invalid test case"); + + let mut encrypt0_cfg = test_case_description + .input + .encrypted + .as_mut() + .expect("expected a CoseEncrypt0 test case, but it was not found"); + + let mut encrypt0 = CoseEncrypt0Builder::new(); + + let mut recipient = encrypt0_cfg + .recipients + .first_mut() + .expect("test case has no recipient"); + + // Need to generate an IV. Have to do this quite ugly, because we have implemented our IV + // generation on the header builder only. + let iv_generator = HeaderBuilder::new() + .gen_iv( + &mut backend, + &encrypt0_cfg + .protected + .as_ref() + .or_else(|| encrypt0_cfg.unprotected.as_ref()) + .unwrap() + .alg + .as_ref() + .unwrap() + .clone(), + ) + .expect("unable to generate IV") + .build(); + let mut unprotected = encrypt0_cfg.unprotected.clone().unwrap_or_default(); + unprotected.iv = iv_generator.iv; + + let mut encrypt0 = encrypt0 + .try_encrypt( + &mut backend, + &mut &recipient.key, + false, + encrypt0_cfg.protected.clone(), + Some(unprotected), + &test_case_description.input.plaintext.clone().into_bytes(), + &mut encrypt0_cfg.external.as_slice(), + ) + .expect("unable to encrypt Encrypt0 object"); + + let (failure, sign_serialized) = serialize_encrypt0_and_apply_failures( + &mut test_case_description.input.failures, + &mut recipient.key, + encrypt0.build(), + ); + + if failure.is_some() && test_case_description.fail { + println!( + "serialization failed as expected for test case: {:?}", + failure.unwrap() + ); + return; + } else if failure.is_some() && !test_case_description.fail { + panic!( + "unexpected error occurred while serializing Sign1 object: {:?}", + failure.unwrap() + ) + } + + let encrypt0_redeserialized = + match CoseEncrypt0::from_tagged_slice(sign_serialized.as_slice()).or_else(|e1| { + CoseEncrypt0::from_slice(sign_serialized.as_slice()) + .map_err(|e2| Result::::Err((e1, e2))) + }) { + Ok(v) => v, + e => { + if test_case_description.fail { + println!("test case failed as expected. Error: {:?}", e); + return; + } else { + e.expect("unable to deserialize test case data"); + unreachable!() + } + } + }; + + verify_encrypt0_test_case( + &mut backend, + &encrypt0_redeserialized, + test_case_description + .input + .encrypted + .as_mut() + .expect("expected a CoseSign test case, but it was not found"), + &test_case_description.input.plaintext.as_bytes(), + test_case_description.fail, + ) + } + + #[rstest] + fn cose_examples_encrypted_encrypt0_reference_output( + #[files("tests/cose_examples/encrypted-tests/enc-*.json")] test_path: PathBuf, + #[values(OpensslContext {})] backend: impl CoseEncryptCipher, + ) { + perform_encrypt0_reference_output_test(test_path, backend) + } + + #[rstest] + fn cose_examples_encrypted_encrypt0_self_signed( + #[files("tests/cose_examples/encrypted-tests/enc-*.json")] test_path: PathBuf, + #[values(OpensslContext {})] backend: impl CoseEncryptCipher, + ) { + perform_encrypt0_self_signed_test(test_path, backend) + } + + #[rstest] + fn cose_examples_aes_gcm_encrypt0_reference_output( + #[files("tests/cose_examples/aes-gcm-examples/aes-gcm-enc-*.json")] test_path: PathBuf, + #[values(OpensslContext {})] backend: impl CoseEncryptCipher, + ) { + perform_encrypt0_reference_output_test(test_path, backend) + } + + #[rstest] + fn cose_examples_aes_gcm_encrypt0_self_signed( + #[files("tests/cose_examples/aes-gcm-examples/aes-gcm-enc-*.json")] test_path: PathBuf, + #[values(OpensslContext {})] backend: impl CoseEncryptCipher, + ) { + perform_encrypt0_self_signed_test(test_path, backend) + } +} diff --git a/src/token/cose/sign/mod.rs b/src/token/cose/sign/mod.rs index 68fba4e..dd8f84d 100644 --- a/src/token/cose/sign/mod.rs +++ b/src/token/cose/sign/mod.rs @@ -42,32 +42,24 @@ pub trait CoseSignCipher { /// # Arguments /// /// * `alg` - The variant of ECDSA to use (determines the hash function). - /// /// If unsupported by the backend, a [CoseCipherError::UnsupportedAlgorithm] error /// should be returned. - /// /// If the given algorithm is an IANA-assigned value that is unknown, the /// implementation should return [CoseCipherError::UnsupportedAlgorithm] (in case /// additional variants of ECDSA are ever added). - /// /// If the algorithm is not an ECDSA algorithm, the implementation may return /// [CoseCipherError::UnsupportedAlgorithm] or panic. /// * `key` - Elliptic curve key that should be used. - /// /// Implementations may assume that if the [CoseEc2Key::crv] field is an IANA-assigned /// value, it will always be a curve feasible for ECDSA. - /// /// If the given algorithm is an IANA-assigned value that is unknown, the /// implementation should return [CoseCipherError::UnsupportedAlgorithm] (in case /// additional variants of ECDSA are ever added). If the algorithm is not an ECDSA /// algorithm, the implementation may return [CoseCipherError::UnsupportedAlgorithm] /// or panic. - /// /// Note that curve and hash bit sizes do not necessarily match. - /// /// Implementations may assume the struct field `d` (the private key) to always be set /// and panic if this is not the case. - /// /// The fields x and y (the public key) may be used by implementations if they are /// set. If they are not, implementations may either derive the public key from `d` or /// return a [CoseCipherError::UnsupportedKeyDerivation] if this derivation is @@ -105,32 +97,24 @@ pub trait CoseSignCipher { /// # Arguments /// /// * `alg` - The variant of ECDSA to use (determines the hash function). - /// /// If unsupported by the backend, a [CoseCipherError::UnsupportedAlgorithm] error /// should be returned. - /// /// If the given algorithm is an IANA-assigned value that is unknown, the /// implementation should return [CoseCipherError::UnsupportedAlgorithm] (in case /// additional variants of ECDSA are ever added). - /// /// If the algorithm is not an ECDSA algorithm, the implementation may return /// [CoseCipherError::UnsupportedAlgorithm] or panic. /// * `key` - Elliptic curve key that should be used. - /// /// Implementations may assume that if the [CoseEc2Key::crv] field is an IANA-assigned /// value, it will always be a curve feasible for ECDSA. - /// /// If the given algorithm is an IANA-assigned value that is unknown, the /// implementation should return [CoseCipherError::UnsupportedAlgorithm] (in case /// additional variants of ECDSA are ever added). If the algorithm is not an ECDSA /// algorithm, the implementation may return [CoseCipherError::UnsupportedAlgorithm] /// or panic. - /// /// Note that curve and hash bit sizes do not necessarily match. - /// /// Implementations may assume the struct field `d` (the private key) to always be set /// and panic if this is not the case. - /// /// The fields x and y (the public key) may be used by implementations if they are /// set. If they are not, implementations may either derive the public key from `d` or /// return a [CoseCipherError::UnsupportedKeyDerivation] if this derivation is diff --git a/src/token/cose/sign/sign.rs b/src/token/cose/sign/sign.rs index 8a81923..57b26c5 100644 --- a/src/token/cose/sign/sign.rs +++ b/src/token/cose/sign/sign.rs @@ -210,7 +210,8 @@ mod tests { use crate::token::cose::sign::CoseSign1Ext; use crate::token::cose::sign::{CoseSignBuilderExt, CoseSignExt}; use crate::token::cose::test_helper::{ - TestCase, TestCaseFailures, TestCaseInput, TestCaseSign, TestCaseSigner, + apply_attribute_failures, apply_header_failures, serialize_cose_with_failures, TestCase, + TestCaseFailures, TestCaseInput, TestCaseRecipient, TestCaseSign, }; use crate::CoseSignCipher; use base64::Engine; @@ -232,86 +233,7 @@ mod tests { test_case_input: &mut TestCaseInput, mut value: CoseSign, ) -> (Option, Vec) { - if let Some(headers_to_remove) = &test_case_input.failures.remove_protected_headers { - if !headers_to_remove.key_id.is_empty() { - value.protected.header.key_id = Vec::new(); - } - if !headers_to_remove.counter_signatures.is_empty() { - value.protected.header.counter_signatures = Vec::new(); - } - if let Some(new_val) = &headers_to_remove.alg { - value.protected.header.alg = None - } - if let Some(new_val) = &headers_to_remove.content_type { - value.protected.header.content_type = None - } - if !headers_to_remove.crit.is_empty() { - value.protected.header.crit = Vec::new(); - } - if !headers_to_remove.iv.is_empty() { - value.protected.header.iv = Vec::new(); - } - if !headers_to_remove.partial_iv.is_empty() { - value.protected.header.partial_iv = Vec::new() - } - let removed_fields = headers_to_remove - .rest - .iter() - .map(|(label, _value)| label) - .cloned() - .collect::>(); - let new_headers = value - .protected - .header - .rest - .iter() - .filter(|(label, _value)| !removed_fields.contains(label)) - .cloned() - .collect::>(); - value.protected.header.rest = new_headers - } - - if let Some(headers_to_add) = &test_case_input.failures.add_protected_headers { - if !headers_to_add.key_id.is_empty() { - value.protected.header.key_id = headers_to_add.key_id.clone(); - } - if !headers_to_add.counter_signatures.is_empty() { - value.protected.header.counter_signatures = - headers_to_add.counter_signatures.clone(); - } - if let Some(new_val) = &headers_to_add.alg { - value.protected.header.alg = Some(new_val.clone()) - } - if let Some(new_val) = &headers_to_add.content_type { - value.protected.header.content_type = Some(new_val.clone()) - } - if !headers_to_add.crit.is_empty() { - value.protected.header.crit = headers_to_add.crit.clone(); - } - if !headers_to_add.iv.is_empty() { - value.protected.header.iv = headers_to_add.iv.clone(); - } - if !headers_to_add.partial_iv.is_empty() { - value.protected.header.partial_iv = headers_to_add.partial_iv.clone(); - } - - let removed_fields = headers_to_add - .rest - .iter() - .map(|(label, _value)| label) - .cloned() - .collect::>(); - let mut new_headers = value - .protected - .header - .rest - .iter() - .filter(|(label, _value)| !removed_fields.contains(label)) - .cloned() - .collect::>(); - new_headers.append(&mut headers_to_add.rest.clone()); - value.protected.header.rest = new_headers - } + apply_header_failures(&mut value.protected.header, &test_case_input.failures); let mut alg_change_error = None; let mut signers = test_case_input @@ -329,18 +251,7 @@ mod tests { }; } - let serialized_data = if let Some(new_tag) = &test_case_input.failures.change_cbor_tag { - let untagged_value = value - .to_cbor_value() - .expect("unable to generate CBOR value of CoseSign1"); - ciborium::Value::Tag(*new_tag, Box::new(untagged_value)) - .to_vec() - .expect("unable to serialize CBOR value") - } else { - value - .to_tagged_vec() - .expect("unable to generate CBOR value of CoseSign1") - }; + let serialized_data = serialize_cose_with_failures(value, &test_case_input.failures); (alg_change_error, serialized_data) } @@ -355,117 +266,9 @@ mod tests { *byte = byte.wrapping_add(1); } - if let Some(headers_to_remove) = &failures.remove_protected_headers { - if !headers_to_remove.key_id.is_empty() { - value.protected.header.key_id = Vec::new(); - } - if !headers_to_remove.counter_signatures.is_empty() { - value.protected.header.counter_signatures = Vec::new(); - } - if let Some(new_val) = &headers_to_remove.alg { - value.protected.header.alg = None - } - if let Some(new_val) = &headers_to_remove.content_type { - value.protected.header.content_type = None - } - if !headers_to_remove.crit.is_empty() { - value.protected.header.crit = Vec::new(); - } - if !headers_to_remove.iv.is_empty() { - value.protected.header.iv = Vec::new(); - } - if !headers_to_remove.partial_iv.is_empty() { - value.protected.header.partial_iv = Vec::new() - } - let removed_fields = headers_to_remove - .rest - .iter() - .map(|(label, _value)| label) - .cloned() - .collect::>(); - let new_headers = value - .protected - .header - .rest - .iter() - .filter(|(label, _value)| !removed_fields.contains(label)) - .cloned() - .collect::>(); - value.protected.header.rest = new_headers - } + apply_header_failures(&mut value.protected.header, &failures); - if let Some(headers_to_add) = &failures.add_protected_headers { - if !headers_to_add.key_id.is_empty() { - value.protected.header.key_id = headers_to_add.key_id.clone(); - } - if !headers_to_add.counter_signatures.is_empty() { - value.protected.header.counter_signatures = - headers_to_add.counter_signatures.clone(); - } - if let Some(new_val) = &headers_to_add.alg { - value.protected.header.alg = Some(new_val.clone()) - } - if let Some(new_val) = &headers_to_add.content_type { - value.protected.header.content_type = Some(new_val.clone()) - } - if !headers_to_add.crit.is_empty() { - value.protected.header.crit = headers_to_add.crit.clone(); - } - if !headers_to_add.iv.is_empty() { - value.protected.header.iv = headers_to_add.iv.clone(); - } - if !headers_to_add.partial_iv.is_empty() { - value.protected.header.partial_iv = headers_to_add.partial_iv.clone(); - } - - let removed_fields = headers_to_add - .rest - .iter() - .map(|(label, _value)| label) - .cloned() - .collect::>(); - let mut new_headers = value - .protected - .header - .rest - .iter() - .filter(|(label, _value)| !removed_fields.contains(label)) - .cloned() - .collect::>(); - new_headers.append(&mut headers_to_add.rest.clone()); - value.protected.header.rest = new_headers - } - - if let Some(attribute_changes) = &failures.change_attribute { - match attribute_changes.get("alg") { - None => None, - Some(Value::Number(v)) => { - let cbor_value = ciborium::Value::Integer(ciborium::value::Integer::from( - v.as_i64().expect("unable to parse algorithm number"), - )); - match coset::Algorithm::from_cbor_value(cbor_value) { - Ok(value) => { - key.alg = Some(value); - None - } - Err(e) => Some(e), - } - } - Some(Value::String(v)) => { - let cbor_value = ciborium::Value::Text(v.to_string()); - match coset::Algorithm::from_cbor_value(cbor_value) { - Ok(value) => { - key.alg = Some(value); - None - } - Err(e) => Some(e), - } - } - v => panic!("unable to set algorithm to {:?}", v), - } - } else { - None - } + apply_attribute_failures(key, &failures) } fn verify_sign_test_case( diff --git a/src/token/cose/sign/sign1.rs b/src/token/cose/sign/sign1.rs index 5f9f2d2..960e192 100644 --- a/src/token/cose/sign/sign1.rs +++ b/src/token/cose/sign/sign1.rs @@ -221,7 +221,8 @@ mod tests { use crate::token::cose::sign::CoseSign1Ext; use crate::token::cose::sign::{CoseSignBuilderExt, CoseSignExt}; use crate::token::cose::test_helper::{ - TestCase, TestCaseFailures, TestCaseInput, TestCaseSign, TestCaseSigner, + apply_attribute_failures, apply_header_failures, serialize_cose_with_failures, TestCase, + TestCaseFailures, TestCaseInput, TestCaseRecipient, TestCaseSign, }; use crate::CoseSignCipher; use base64::Engine; @@ -249,136 +250,19 @@ mod tests { *byte = byte.wrapping_add(1); } - if let Some(headers_to_remove) = &test_case_input.failures.remove_protected_headers { - if !headers_to_remove.key_id.is_empty() { - value.protected.header.key_id = Vec::new(); - } - if !headers_to_remove.counter_signatures.is_empty() { - value.protected.header.counter_signatures = Vec::new(); - } - if let Some(new_val) = &headers_to_remove.alg { - value.protected.header.alg = None - } - if let Some(new_val) = &headers_to_remove.content_type { - value.protected.header.content_type = None - } - if !headers_to_remove.crit.is_empty() { - value.protected.header.crit = Vec::new(); - } - if !headers_to_remove.iv.is_empty() { - value.protected.header.iv = Vec::new(); - } - if !headers_to_remove.partial_iv.is_empty() { - value.protected.header.partial_iv = Vec::new() - } - let removed_fields = headers_to_remove - .rest - .iter() - .map(|(label, _value)| label) - .cloned() - .collect::>(); - let new_headers = value - .protected - .header - .rest - .iter() - .filter(|(label, _value)| !removed_fields.contains(label)) - .cloned() - .collect::>(); - value.protected.header.rest = new_headers - } + apply_header_failures(&mut value.protected.header, &test_case_input.failures); + let serialized_data = serialize_cose_with_failures(value, &test_case_input.failures); - if let Some(headers_to_add) = &test_case_input.failures.add_protected_headers { - if !headers_to_add.key_id.is_empty() { - value.protected.header.key_id = headers_to_add.key_id.clone(); - } - if !headers_to_add.counter_signatures.is_empty() { - value.protected.header.counter_signatures = - headers_to_add.counter_signatures.clone(); - } - if let Some(new_val) = &headers_to_add.alg { - value.protected.header.alg = Some(new_val.clone()) - } - if let Some(new_val) = &headers_to_add.content_type { - value.protected.header.content_type = Some(new_val.clone()) - } - if !headers_to_add.crit.is_empty() { - value.protected.header.crit = headers_to_add.crit.clone(); - } - if !headers_to_add.iv.is_empty() { - value.protected.header.iv = headers_to_add.iv.clone(); - } - if !headers_to_add.partial_iv.is_empty() { - value.protected.header.partial_iv = headers_to_add.partial_iv.clone(); - } - - let removed_fields = headers_to_add - .rest - .iter() - .map(|(label, _value)| label) - .cloned() - .collect::>(); - let mut new_headers = value - .protected - .header - .rest - .iter() - .filter(|(label, _value)| !removed_fields.contains(label)) - .cloned() - .collect::>(); - new_headers.append(&mut headers_to_add.rest.clone()); - value.protected.header.rest = new_headers - } - - let serialized_data = if let Some(new_tag) = &test_case_input.failures.change_cbor_tag { - let untagged_value = value - .to_cbor_value() - .expect("unable to generate CBOR value of CoseSign1"); - ciborium::Value::Tag(*new_tag, Box::new(untagged_value)) - .to_vec() - .expect("unable to serialize CBOR value") - } else { - value - .to_tagged_vec() - .expect("unable to generate CBOR value of CoseSign1") - }; - - if let Some(attribute_changes) = &test_case_input.failures.change_attribute { - match attribute_changes.get("alg") { - None => (None, serialized_data), - Some(Value::Number(v)) => { - let cbor_value = ciborium::Value::Integer(ciborium::value::Integer::from( - v.as_i64().expect("unable to parse algorithm number"), - )); - match coset::Algorithm::from_cbor_value(cbor_value) { - Ok(value) => { - key.alg = Some(value); - (None, serialized_data) - } - Err(e) => (Some(e), serialized_data), - } - } - Some(Value::String(v)) => { - let cbor_value = ciborium::Value::Text(v.to_string()); - match coset::Algorithm::from_cbor_value(cbor_value) { - Ok(value) => { - key.alg = Some(value); - (None, serialized_data) - } - Err(e) => (Some(e), serialized_data), - } - } - v => panic!("unable to set algorithm to {:?}", v), - } - } else { - (None, serialized_data) - } + ( + apply_attribute_failures(key, &test_case_input.failures), + serialized_data, + ) } fn verify_sign1_test_case( backend: &mut T, sign1: &CoseSign1, - test_case: &TestCaseSigner, + test_case: &TestCaseRecipient, should_fail: bool, aad: &[u8], ) { diff --git a/src/token/cose/test_helper.rs b/src/token/cose/test_helper.rs index 33bf72b..aa8dbb7 100644 --- a/src/token/cose/test_helper.rs +++ b/src/token/cose/test_helper.rs @@ -1,7 +1,13 @@ +use crate::error::CoseCipherError; +use crate::token::cose::encrypt::CoseEncryptCipher; +use crate::token::cose::key::CoseSymmetricKey; use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::Engine; use coset::iana::{Algorithm, EnumI64}; -use coset::{iana, CoseKey, CoseKeyBuilder, Header, HeaderBuilder}; +use coset::{ + iana, AsCborValue, CborSerializable, CoseError, CoseKey, CoseKeyBuilder, Header, HeaderBuilder, + Label, TaggedCborSerializable, +}; use serde::de::Error; use serde::{de, Deserialize, Deserializer}; use serde_json::Value; @@ -64,6 +70,27 @@ where Ok(builder.build()) } + Some("oct") => { + let k = if let Some(v) = key_obj + .get("k") + .map(Value::as_str) + .flatten() + .map(|v| URL_SAFE_NO_PAD.decode(v).ok()) + .flatten() + { + v + } else { + return Err(de::Error::custom("COSE key does not have valid k")); + }; + + let mut builder = CoseKeyBuilder::new_symmetric_key(k); + + if let Some(v) = key_obj.get("kid").map(Value::as_str).flatten() { + builder = builder.key_id(v.to_string().into_bytes()) + } + + Ok(builder.build()) + } _ => Err(de::Error::custom("COSE key does not have valid key type")), } } @@ -98,6 +125,10 @@ where Some("ES256") => builder.algorithm(Algorithm::ES256), Some("ES384") => builder.algorithm(Algorithm::ES384), Some("ES512") => builder.algorithm(Algorithm::ES512), + Some("A128GCM") => builder.algorithm(Algorithm::A128GCM), + Some("A192GCM") => builder.algorithm(Algorithm::A192GCM), + Some("A256GCM") => builder.algorithm(Algorithm::A256GCM), + Some("direct") => builder.algorithm(Algorithm::Direct), Some(_) => return Err(de::Error::custom("could not parse test case algorithm")), None => builder, }; @@ -127,6 +158,10 @@ where Some("ES256") => Ok(Some(Algorithm::ES256)), Some("ES384") => Ok(Some(Algorithm::ES384)), Some("ES512") => Ok(Some(Algorithm::ES512)), + Some("A128GCM") => Ok(Some(Algorithm::A128GCM)), + Some("A192GCM") => Ok(Some(Algorithm::A192GCM)), + Some("A256GCM") => Ok(Some(Algorithm::A256GCM)), + Some("direct") => Ok(Some(Algorithm::Direct)), _ => Err(de::Error::custom("could not parse test case algorithm")), } } @@ -169,8 +204,9 @@ pub struct TestCaseInput { pub plaintext: String, #[serde(default)] pub detached: bool, - pub sign0: Option, + pub sign0: Option, pub sign: Option, + pub encrypted: Option, #[serde(default)] pub failures: TestCaseFailures, } @@ -181,11 +217,22 @@ pub struct TestCaseSign { pub unprotected: Option
, #[serde(deserialize_with = "deserialize_header", default)] pub protected: Option
, - pub signers: Vec, + pub signers: Vec, } #[derive(Deserialize, Debug, Clone)] -pub struct TestCaseSigner { +pub struct TestCaseEncrypted { + #[serde(deserialize_with = "deserialize_header", default)] + pub unprotected: Option
, + #[serde(deserialize_with = "deserialize_header", default)] + pub protected: Option
, + #[serde(deserialize_with = "hex::deserialize", default)] + pub external: Vec, + pub recipients: Vec, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct TestCaseRecipient { #[serde(deserialize_with = "deserialize_key")] pub key: CoseKey, #[serde(deserialize_with = "deserialize_header", default)] @@ -208,3 +255,173 @@ pub struct TestCaseOutput { #[serde(deserialize_with = "hex::deserialize")] pub cbor: Vec, } + +pub(crate) fn apply_header_failures(hdr: &mut Header, failures: &TestCaseFailures) { + if let Some(headers_to_remove) = &failures.remove_protected_headers { + if !headers_to_remove.key_id.is_empty() { + hdr.key_id = Vec::new(); + } + if !headers_to_remove.counter_signatures.is_empty() { + hdr.counter_signatures = Vec::new(); + } + if let Some(new_val) = &headers_to_remove.alg { + hdr.alg = None + } + if let Some(new_val) = &headers_to_remove.content_type { + hdr.content_type = None + } + if !headers_to_remove.crit.is_empty() { + hdr.crit = Vec::new(); + } + if !headers_to_remove.iv.is_empty() { + hdr.iv = Vec::new(); + } + if !headers_to_remove.partial_iv.is_empty() { + hdr.partial_iv = Vec::new() + } + let removed_fields = headers_to_remove + .rest + .iter() + .map(|(label, _hdr)| label) + .cloned() + .collect::>(); + let new_headers = hdr + .rest + .iter() + .filter(|(label, _hdr)| !removed_fields.contains(label)) + .cloned() + .collect::>(); + hdr.rest = new_headers + } + + if let Some(headers_to_add) = &failures.add_protected_headers { + if !headers_to_add.key_id.is_empty() { + hdr.key_id = headers_to_add.key_id.clone(); + } + if !headers_to_add.counter_signatures.is_empty() { + hdr.counter_signatures = headers_to_add.counter_signatures.clone(); + } + if let Some(new_val) = &headers_to_add.alg { + hdr.alg = Some(new_val.clone()) + } + if let Some(new_val) = &headers_to_add.content_type { + hdr.content_type = Some(new_val.clone()) + } + if !headers_to_add.crit.is_empty() { + hdr.crit = headers_to_add.crit.clone(); + } + if !headers_to_add.iv.is_empty() { + hdr.iv = headers_to_add.iv.clone(); + } + if !headers_to_add.partial_iv.is_empty() { + hdr.partial_iv = headers_to_add.partial_iv.clone(); + } + + let removed_fields = headers_to_add + .rest + .iter() + .map(|(label, _hdr)| label) + .cloned() + .collect::>(); + let mut new_headers = hdr + .rest + .iter() + .filter(|(label, _hdr)| !removed_fields.contains(label)) + .cloned() + .collect::>(); + new_headers.append(&mut headers_to_add.rest.clone()); + hdr.rest = new_headers + } +} + +pub(crate) fn serialize_cose_with_failures( + value: T, + failures: &TestCaseFailures, +) -> Vec { + if let Some(new_tag) = &failures.change_cbor_tag { + let untagged_value = value + .to_cbor_value() + .expect("unable to generate CBOR value of CoseSign1"); + ciborium::Value::Tag(*new_tag, Box::new(untagged_value)) + .to_vec() + .expect("unable to serialize CBOR value") + } else { + value + .to_tagged_vec() + .expect("unable to generate CBOR value of CoseSign1") + } +} + +pub(crate) fn apply_attribute_failures( + key: &mut CoseKey, + failures: &TestCaseFailures, +) -> Option { + if let Some(attribute_changes) = &failures.change_attribute { + match attribute_changes.get("alg") { + None => None, + Some(Value::Number(v)) => { + let cbor_value = ciborium::Value::Integer(ciborium::value::Integer::from( + v.as_i64().expect("unable to parse algorithm number"), + )); + match coset::Algorithm::from_cbor_value(cbor_value) { + Ok(value) => { + key.alg = Some(value); + None + } + Err(e) => Some(e), + } + } + Some(Value::String(v)) => { + let cbor_value = ciborium::Value::Text(v.to_string()); + match coset::Algorithm::from_cbor_value(cbor_value) { + Ok(value) => { + key.alg = Some(value); + None + } + Err(e) => Some(e), + } + } + v => panic!("unable to set algorithm to {:?}", v), + } + } else { + None + } +} + +pub struct RngMockCipher { + rng_outputs: Vec>, + cipher: T, +} + +impl CoseEncryptCipher for RngMockCipher { + type Error = T::Error; + + // TODO reproducible outputs by mocking the RNG + fn generate_rand(&mut self, buf: &mut [u8]) -> Result<(), CoseCipherError> { + todo!() + } + + fn encrypt_aes_gcm( + &mut self, + algorithm: coset::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + plaintext: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + self.cipher + .encrypt_aes_gcm(algorithm, key, plaintext, aad, iv) + } + + fn decrypt_aes_gcm( + &mut self, + algorithm: coset::Algorithm, + key: CoseSymmetricKey<'_, Self::Error>, + ciphertext_with_tag: &[u8], + aad: &[u8], + iv: &[u8], + ) -> Result, CoseCipherError> { + self.cipher + .decrypt_aes_gcm(algorithm, key, ciphertext_with_tag, aad, iv) + } +}