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