Skip to main content

bitwarden_crypto/
cose.rs

1//! This file contains private-use constants for COSE encoded key types and algorithms.
2//! Standardized values from <https://www.iana.org/assignments/cose/cose.xhtml> should always be preferred
3//! unless there is a a clear benefit, such as a clear cryptographic benefit, which MUST
4//! be documented publicly.
5
6use std::fmt::Debug;
7
8use coset::{
9    CborSerializable, ContentType, CoseEncrypt0, CoseEncrypt0Builder, Header, Label,
10    iana::{self, CoapContentFormat, KeyOperation},
11};
12use hybrid_array::Array;
13use thiserror::Error;
14use tracing::instrument;
15use typenum::U32;
16
17use crate::{
18    ContentFormat, CoseEncrypt0Bytes, CryptoError, SymmetricCryptoKey, XChaCha20Poly1305Key,
19    content_format::{Bytes, ConstContentFormat, CoseContentFormat},
20    error::{EncStringParseError, EncodingError},
21    xchacha20,
22};
23
24// Custom COSE algorithm values
25// NOTE: Any algorithm value below -65536 is reserved for private use in the IANA allocations and
26// can be used freely.
27/// XChaCha20 <https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03> is used over ChaCha20
28/// to be able to randomly generate nonces, and to not have to worry about key wearout. Since
29/// the draft was never published as an RFC, we use a private-use value for the algorithm.
30pub(crate) const XCHACHA20_POLY1305: i64 = -70000;
31pub(crate) const ALG_ARGON2ID13: i64 = -71000;
32
33// Custom labels for COSE headers
34// NOTE: Any label below -65536 is reserved for private use in the IANA allocations and can be used
35// freely.
36pub(crate) const ARGON2_SALT: i64 = -71001;
37pub(crate) const ARGON2_ITERATIONS: i64 = -71002;
38pub(crate) const ARGON2_MEMORY: i64 = -71003;
39pub(crate) const ARGON2_PARALLELISM: i64 = -71004;
40/// Indicates for any object containing a key (wrapped key, password protected key envelope) which
41/// key ID that contained key has
42pub(crate) const CONTAINED_KEY_ID: i64 = -71005;
43
44// Note: These are in the "unregistered" tree: https://datatracker.ietf.org/doc/html/rfc6838#section-3.4
45// These are only used within Bitwarden, and not meant for exchange with other systems.
46const CONTENT_TYPE_PADDED_UTF8: &str = "application/x.bitwarden.utf8-padded";
47pub(crate) const CONTENT_TYPE_PADDED_CBOR: &str = "application/x.bitwarden.cbor-padded";
48const CONTENT_TYPE_BITWARDEN_LEGACY_KEY: &str = "application/x.bitwarden.legacy-key";
49const CONTENT_TYPE_SPKI_PUBLIC_KEY: &str = "application/x.bitwarden.spki-public-key";
50
51/// The label used for the namespace ensuring strong domain separation when using signatures.
52pub(crate) const SIGNING_NAMESPACE: i64 = -80000;
53
54// Domain separation / Namespaces
55//
56// Cryptographic objects are strongly domain separated so that items can only be decrypted
57// in the correct context, making cryptographic analysis significantly easier and preventing
58// misuse of cryptographic objects. For this, there is a partitioning at two layers. First,
59// the object types are partitioned into e.g. EncString, DataEnvelope, Signature, KeyEnvelope, and
60// so on. Second, within each of these types, each of these spans their own namespace for usages.
61// For instance, a DataEnvelope may describe that the contained item is only valid as a vault item,
62// or as account settings.
63
64/// MUST be placed in the protected header of cose objects
65pub(crate) const SAFE_OBJECT_NAMESPACE: i64 = -80002;
66
67#[allow(clippy::enum_variant_names)]
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub(crate) enum SafeObjectNamespace {
70    PasswordProtectedKeyEnvelope = 1,
71    DataEnvelope = 2,
72    SymmetricKeyEnvelope = 3,
73    //Reserved:
74    //PrivateKeyEnvelope = 4,
75    //SigningKeyEnvelope = 5,
76}
77
78impl TryFrom<i128> for SafeObjectNamespace {
79    type Error = ();
80
81    fn try_from(value: i128) -> Result<Self, Self::Error> {
82        match value {
83            1 => Ok(SafeObjectNamespace::PasswordProtectedKeyEnvelope),
84            2 => Ok(SafeObjectNamespace::DataEnvelope),
85            3 => Ok(SafeObjectNamespace::SymmetricKeyEnvelope),
86            _ => Err(()),
87        }
88    }
89}
90
91impl From<SafeObjectNamespace> for i128 {
92    fn from(namespace: SafeObjectNamespace) -> Self {
93        namespace as i128
94    }
95}
96
97pub(crate) trait ContentNamespace: TryFrom<i128> + Into<i128> + PartialEq + Debug {}
98
99/// Each type of object has it's own namespace for strong domain separation to eliminate
100/// attacks which attempt to confuse object types. For signatures, this refers to signature
101/// namespaces, for data envelopes to data envelope namespaces and so on.
102pub(crate) const SAFE_CONTENT_NAMESPACE: i64 = -80001;
103
104const XCHACHA20_TEXT_PAD_BLOCK_SIZE: usize = 32;
105
106/// Encrypt a plaintext message with a given key
107pub(crate) fn encrypt_cose(
108    cose_encrypt0_builder: CoseEncrypt0Builder,
109    plaintext: &[u8],
110    key: &XChaCha20Poly1305Key,
111) -> CoseEncrypt0 {
112    let mut nonce = [0u8; xchacha20::NONCE_SIZE];
113    cose_encrypt0_builder
114        .create_ciphertext(plaintext, &[], |data, aad| {
115            let ciphertext =
116                crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
117            nonce = ciphertext.nonce();
118            ciphertext.encrypted_bytes().to_vec()
119        })
120        .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
121        .build()
122}
123
124pub struct DecryptFailed;
125/// Decrypt a CoseEncrypt0 message with a CoseKey
126pub(crate) fn decrypt_cose(
127    cose_encrypt0: &CoseEncrypt0,
128    key: &XChaCha20Poly1305Key,
129) -> Result<Vec<u8>, DecryptFailed> {
130    let nonce: [u8; xchacha20::NONCE_SIZE] = cose_encrypt0
131        .unprotected
132        .iv
133        .clone()
134        .try_into()
135        .map_err(|_| DecryptFailed)?;
136    cose_encrypt0
137        .clone()
138        .decrypt_ciphertext(
139            &[],
140            || CryptoError::MissingField("ciphertext"),
141            |data, aad| {
142                xchacha20::decrypt_xchacha20_poly1305(&nonce, &(*key.enc_key).into(), data, aad)
143            },
144        )
145        .map_err(|_| DecryptFailed)
146}
147
148/// Encrypts a plaintext message using XChaCha20Poly1305 and returns a COSE Encrypt0 message
149pub(crate) fn encrypt_xchacha20_poly1305(
150    plaintext: &[u8],
151    key: &crate::XChaCha20Poly1305Key,
152    content_format: ContentFormat,
153) -> Result<CoseEncrypt0Bytes, CryptoError> {
154    let mut plaintext = plaintext.to_vec();
155
156    let header_builder: coset::HeaderBuilder = content_format.into();
157    let mut protected_header = header_builder
158        .key_id(key.key_id.as_slice().to_vec())
159        .build();
160    // This should be adjusted to use the builder pattern once implemented in coset.
161    // The related coset upstream issue is:
162    // https://github.com/google/coset/issues/105
163    protected_header.alg = Some(coset::Algorithm::PrivateUse(XCHACHA20_POLY1305));
164
165    if should_pad_content(&content_format) {
166        // Pad the data to a block size in order to hide plaintext length
167        let min_length =
168            XCHACHA20_TEXT_PAD_BLOCK_SIZE * (1 + (plaintext.len() / XCHACHA20_TEXT_PAD_BLOCK_SIZE));
169        crate::keys::utils::pad_bytes(&mut plaintext, min_length)?;
170    }
171
172    let mut nonce = [0u8; xchacha20::NONCE_SIZE];
173    let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
174        .protected(protected_header)
175        .create_ciphertext(&plaintext, &[], |data, aad| {
176            let ciphertext =
177                crate::xchacha20::encrypt_xchacha20_poly1305(&(*key.enc_key).into(), data, aad);
178            nonce = ciphertext.nonce();
179            ciphertext.encrypted_bytes().to_vec()
180        })
181        .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
182        .build();
183
184    cose_encrypt0
185        .to_vec()
186        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))
187        .map(CoseEncrypt0Bytes::from)
188}
189
190/// Decrypts a COSE Encrypt0 message, using a XChaCha20Poly1305 key
191pub(crate) fn decrypt_xchacha20_poly1305(
192    cose_encrypt0_message: &CoseEncrypt0Bytes,
193    key: &crate::XChaCha20Poly1305Key,
194) -> Result<(Vec<u8>, ContentFormat), CryptoError> {
195    let msg = coset::CoseEncrypt0::from_slice(cose_encrypt0_message.as_ref())
196        .map_err(|err| CryptoError::EncString(EncStringParseError::InvalidCoseEncoding(err)))?;
197
198    let Some(ref alg) = msg.protected.header.alg else {
199        return Err(CryptoError::EncString(
200            EncStringParseError::CoseMissingAlgorithm,
201        ));
202    };
203
204    if *alg != coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) {
205        return Err(CryptoError::WrongKeyType);
206    }
207
208    let content_format = ContentFormat::try_from(&msg.protected.header)
209        .map_err(|_| CryptoError::EncString(EncStringParseError::CoseMissingContentType))?;
210
211    if key.key_id.as_slice() != msg.protected.header.key_id {
212        return Err(CryptoError::WrongCoseKeyId);
213    }
214
215    let decrypted_message = msg.decrypt_ciphertext(
216        &[],
217        || CryptoError::MissingField("ciphertext"),
218        |data, aad| {
219            let nonce = msg.unprotected.iv.as_slice();
220            crate::xchacha20::decrypt_xchacha20_poly1305(
221                nonce
222                    .try_into()
223                    .map_err(|_| CryptoError::InvalidNonceLength)?,
224                &(*key.enc_key).into(),
225                data,
226                aad,
227            )
228        },
229    )?;
230
231    if should_pad_content(&content_format) {
232        // Unpad the data to get the original plaintext
233        let data = crate::keys::utils::unpad_bytes(&decrypted_message)?;
234        return Ok((data.to_vec(), content_format));
235    }
236
237    Ok((decrypted_message, content_format))
238}
239
240const SYMMETRIC_KEY: Label = Label::Int(iana::SymmetricKeyParameter::K as i64);
241
242impl TryFrom<&coset::CoseKey> for SymmetricCryptoKey {
243    type Error = CryptoError;
244
245    #[instrument(err, skip_all)]
246    fn try_from(cose_key: &coset::CoseKey) -> Result<Self, Self::Error> {
247        let key_bytes = cose_key
248            .params
249            .iter()
250            .find_map(|(label, value)| match (label, value) {
251                (&SYMMETRIC_KEY, ciborium::Value::Bytes(bytes)) => Some(bytes),
252                _ => None,
253            })
254            .ok_or(CryptoError::InvalidKey)?;
255        let alg = cose_key.alg.as_ref().ok_or(CryptoError::InvalidKey)?;
256        let key_opts = cose_key
257            .key_ops
258            .iter()
259            .map(|op| match op {
260                coset::RegisteredLabel::Assigned(iana::KeyOperation::Encrypt) => {
261                    Ok(KeyOperation::Encrypt)
262                }
263                coset::RegisteredLabel::Assigned(iana::KeyOperation::Decrypt) => {
264                    Ok(KeyOperation::Decrypt)
265                }
266                coset::RegisteredLabel::Assigned(iana::KeyOperation::WrapKey) => {
267                    Ok(KeyOperation::WrapKey)
268                }
269                coset::RegisteredLabel::Assigned(iana::KeyOperation::UnwrapKey) => {
270                    Ok(KeyOperation::UnwrapKey)
271                }
272                _ => Err(CryptoError::InvalidKey),
273            })
274            .collect::<Result<Vec<KeyOperation>, CryptoError>>()?;
275
276        match alg {
277            coset::Algorithm::PrivateUse(XCHACHA20_POLY1305) => {
278                let enc_key = Box::pin(
279                    Array::<u8, U32>::try_from(key_bytes).map_err(|_| CryptoError::InvalidKey)?,
280                );
281                let key_id = cose_key
282                    .key_id
283                    .as_slice()
284                    .try_into()
285                    .map_err(|_| CryptoError::InvalidKey)?;
286                Ok(SymmetricCryptoKey::XChaCha20Poly1305Key(
287                    XChaCha20Poly1305Key {
288                        enc_key,
289                        key_id,
290                        supported_operations: key_opts,
291                    },
292                ))
293            }
294            _ => Err(CryptoError::InvalidKey),
295        }
296    }
297}
298
299impl From<ContentFormat> for coset::HeaderBuilder {
300    fn from(format: ContentFormat) -> Self {
301        let header_builder = coset::HeaderBuilder::new();
302
303        match format {
304            ContentFormat::Utf8 => {
305                header_builder.content_type(CONTENT_TYPE_PADDED_UTF8.to_string())
306            }
307            ContentFormat::Pkcs8PrivateKey => {
308                header_builder.content_format(CoapContentFormat::Pkcs8)
309            }
310            ContentFormat::SPKIPublicKeyDer => {
311                header_builder.content_type(CONTENT_TYPE_SPKI_PUBLIC_KEY.to_string())
312            }
313            ContentFormat::CoseSign1 => header_builder.content_format(CoapContentFormat::CoseSign1),
314            ContentFormat::CoseKey => header_builder.content_format(CoapContentFormat::CoseKey),
315            ContentFormat::CoseEncrypt0 => {
316                header_builder.content_format(CoapContentFormat::CoseEncrypt0)
317            }
318            ContentFormat::BitwardenLegacyKey => {
319                header_builder.content_type(CONTENT_TYPE_BITWARDEN_LEGACY_KEY.to_string())
320            }
321            ContentFormat::OctetStream => {
322                header_builder.content_format(CoapContentFormat::OctetStream)
323            }
324            ContentFormat::Cbor => header_builder.content_format(CoapContentFormat::Cbor),
325        }
326    }
327}
328
329impl TryFrom<&coset::Header> for ContentFormat {
330    type Error = CryptoError;
331
332    fn try_from(header: &coset::Header) -> Result<Self, Self::Error> {
333        match header.content_type.as_ref() {
334            Some(ContentType::Text(format)) if format == CONTENT_TYPE_PADDED_UTF8 => {
335                Ok(ContentFormat::Utf8)
336            }
337            Some(ContentType::Text(format)) if format == CONTENT_TYPE_BITWARDEN_LEGACY_KEY => {
338                Ok(ContentFormat::BitwardenLegacyKey)
339            }
340            Some(ContentType::Text(format)) if format == CONTENT_TYPE_SPKI_PUBLIC_KEY => {
341                Ok(ContentFormat::SPKIPublicKeyDer)
342            }
343            Some(ContentType::Assigned(CoapContentFormat::Pkcs8)) => {
344                Ok(ContentFormat::Pkcs8PrivateKey)
345            }
346            Some(ContentType::Assigned(CoapContentFormat::CoseKey)) => Ok(ContentFormat::CoseKey),
347            Some(ContentType::Assigned(CoapContentFormat::OctetStream)) => {
348                Ok(ContentFormat::OctetStream)
349            }
350            Some(ContentType::Assigned(CoapContentFormat::Cbor)) => Ok(ContentFormat::Cbor),
351            _ => Err(CryptoError::EncString(
352                EncStringParseError::CoseMissingContentType,
353            )),
354        }
355    }
356}
357
358fn should_pad_content(format: &ContentFormat) -> bool {
359    matches!(format, ContentFormat::Utf8)
360}
361
362/// Trait for structs that are serializable to COSE objects.
363pub trait CoseSerializable<T: CoseContentFormat + ConstContentFormat> {
364    /// Serializes the struct to COSE serialization
365    fn to_cose(&self) -> Bytes<T>;
366    /// Deserializes a serialized COSE object to a struct
367    fn from_cose(bytes: &Bytes<T>) -> Result<Self, EncodingError>
368    where
369        Self: Sized;
370}
371
372pub(crate) fn extract_integer(
373    header: &Header,
374    target_label: i64,
375    value_name: &str,
376) -> Result<i128, CoseExtractError> {
377    header
378        .rest
379        .iter()
380        .find_map(|(label, value)| match (label, value) {
381            (Label::Int(label_value), ciborium::Value::Integer(int_value))
382                if *label_value == target_label =>
383            {
384                Some(*int_value)
385            }
386            _ => None,
387        })
388        .map(Into::into)
389        .ok_or_else(|| CoseExtractError::MissingValue(value_name.to_string()))
390}
391
392pub(crate) fn extract_bytes(
393    header: &Header,
394    target_label: i64,
395    value_name: &str,
396) -> Result<Vec<u8>, CoseExtractError> {
397    header
398        .rest
399        .iter()
400        .find_map(|(label, value)| match (label, value) {
401            (Label::Int(label_value), ciborium::Value::Bytes(byte_value))
402                if *label_value == target_label =>
403            {
404                Some(byte_value.clone())
405            }
406            _ => None,
407        })
408        .ok_or(CoseExtractError::MissingValue(value_name.to_string()))
409}
410
411#[derive(Debug, Error)]
412pub(crate) enum CoseExtractError {
413    #[error("Missing value {0}")]
414    MissingValue(String),
415}
416
417/// Helper function to convert a COSE KeyOperation to a debug string
418pub(crate) fn debug_key_operation(key_operation: KeyOperation) -> &'static str {
419    match key_operation {
420        KeyOperation::Sign => "Sign",
421        KeyOperation::Verify => "Verify",
422        KeyOperation::Encrypt => "Encrypt",
423        KeyOperation::Decrypt => "Decrypt",
424        KeyOperation::WrapKey => "WrapKey",
425        KeyOperation::UnwrapKey => "UnwrapKey",
426        KeyOperation::DeriveKey => "DeriveKey",
427        KeyOperation::DeriveBits => "DeriveBits",
428        _ => "Unknown",
429    }
430}
431
432#[cfg(test)]
433mod test {
434    use super::*;
435    use crate::keys::KeyId;
436
437    const KEY_ID: [u8; 16] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
438    const KEY_DATA: [u8; 32] = [
439        0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
440        0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
441        0x1e, 0x1f,
442    ];
443    const TEST_VECTOR_PLAINTEXT: &[u8] = b"Message test vector";
444    const TEST_VECTOR_COSE_ENCRYPT0: &[u8] = &[
445        131, 88, 28, 163, 1, 58, 0, 1, 17, 111, 3, 24, 42, 4, 80, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
446        11, 12, 13, 14, 15, 161, 5, 88, 24, 78, 20, 28, 157, 180, 246, 131, 220, 82, 104, 72, 73,
447        75, 43, 69, 139, 216, 167, 145, 220, 67, 168, 144, 173, 88, 35, 127, 234, 194, 83, 189,
448        172, 65, 29, 156, 73, 98, 87, 231, 87, 129, 15, 235, 127, 125, 97, 211, 51, 212, 211, 2,
449        13, 36, 123, 53, 12, 31, 191, 40, 13, 175,
450    ];
451
452    #[test]
453    fn test_encrypt_decrypt_roundtrip_octetstream() {
454        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
455            SymmetricCryptoKey::make_xchacha20_poly1305_key()
456        else {
457            panic!("Failed to create XChaCha20Poly1305Key");
458        };
459
460        let plaintext = b"Hello, world!";
461        let encrypted =
462            encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::OctetStream).unwrap();
463        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
464        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::OctetStream));
465    }
466
467    #[test]
468    fn test_encrypt_decrypt_roundtrip_utf8() {
469        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
470            SymmetricCryptoKey::make_xchacha20_poly1305_key()
471        else {
472            panic!("Failed to create XChaCha20Poly1305Key");
473        };
474
475        let plaintext = b"Hello, world!";
476        let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Utf8).unwrap();
477        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
478        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::Utf8));
479    }
480
481    #[test]
482    fn test_encrypt_decrypt_roundtrip_pkcs8() {
483        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
484            SymmetricCryptoKey::make_xchacha20_poly1305_key()
485        else {
486            panic!("Failed to create XChaCha20Poly1305Key");
487        };
488
489        let plaintext = b"Hello, world!";
490        let encrypted =
491            encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::Pkcs8PrivateKey).unwrap();
492        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
493        assert_eq!(
494            decrypted,
495            (plaintext.to_vec(), ContentFormat::Pkcs8PrivateKey)
496        );
497    }
498
499    #[test]
500    fn test_encrypt_decrypt_roundtrip_cosekey() {
501        let SymmetricCryptoKey::XChaCha20Poly1305Key(ref key) =
502            SymmetricCryptoKey::make_xchacha20_poly1305_key()
503        else {
504            panic!("Failed to create XChaCha20Poly1305Key");
505        };
506
507        let plaintext = b"Hello, world!";
508        let encrypted = encrypt_xchacha20_poly1305(plaintext, key, ContentFormat::CoseKey).unwrap();
509        let decrypted = decrypt_xchacha20_poly1305(&encrypted, key).unwrap();
510        assert_eq!(decrypted, (plaintext.to_vec(), ContentFormat::CoseKey));
511    }
512
513    #[test]
514    fn test_decrypt_test_vector() {
515        let key = XChaCha20Poly1305Key {
516            key_id: KeyId::from(KEY_ID),
517            enc_key: Box::pin(Array::from(KEY_DATA)),
518            supported_operations: vec![
519                KeyOperation::Decrypt,
520                KeyOperation::Encrypt,
521                KeyOperation::WrapKey,
522                KeyOperation::UnwrapKey,
523            ],
524        };
525        let decrypted =
526            decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key)
527                .unwrap();
528        assert_eq!(
529            decrypted,
530            (TEST_VECTOR_PLAINTEXT.to_vec(), ContentFormat::OctetStream)
531        );
532    }
533
534    #[test]
535    fn test_fail_wrong_key_id() {
536        let key = XChaCha20Poly1305Key {
537            key_id: KeyId::from([1; 16]), // Different key ID
538            enc_key: Box::pin(Array::from(KEY_DATA)),
539            supported_operations: vec![
540                KeyOperation::Decrypt,
541                KeyOperation::Encrypt,
542                KeyOperation::WrapKey,
543                KeyOperation::UnwrapKey,
544            ],
545        };
546        assert!(matches!(
547            decrypt_xchacha20_poly1305(&CoseEncrypt0Bytes::from(TEST_VECTOR_COSE_ENCRYPT0), &key),
548            Err(CryptoError::WrongCoseKeyId)
549        ));
550    }
551
552    #[test]
553    fn test_fail_wrong_algorithm() {
554        let protected_header = coset::HeaderBuilder::new()
555            .algorithm(iana::Algorithm::A256GCM)
556            .key_id(KEY_ID.to_vec())
557            .build();
558        let nonce = [0u8; 16];
559        let cose_encrypt0 = coset::CoseEncrypt0Builder::new()
560            .protected(protected_header)
561            .create_ciphertext(&[], &[], |_, _| Vec::new())
562            .unprotected(coset::HeaderBuilder::new().iv(nonce.to_vec()).build())
563            .build();
564        let serialized_message = CoseEncrypt0Bytes::from(cose_encrypt0.to_vec().unwrap());
565
566        let key = XChaCha20Poly1305Key {
567            key_id: KeyId::from(KEY_ID),
568            enc_key: Box::pin(Array::from(KEY_DATA)),
569            supported_operations: vec![
570                KeyOperation::Decrypt,
571                KeyOperation::Encrypt,
572                KeyOperation::WrapKey,
573                KeyOperation::UnwrapKey,
574            ],
575        };
576        assert!(matches!(
577            decrypt_xchacha20_poly1305(&serialized_message, &key),
578            Err(CryptoError::WrongKeyType)
579        ));
580    }
581}