Skip to main content

rustlavel_webauthn/
cose.rs

1//! COSE keys, and the one thing this package must never get wrong.
2//!
3//! An authenticator hands back its public key as a COSE_Key — a CBOR map with
4//! integer labels — and every later assertion is checked against it. If the
5//! parsing is loose or the verification is wrong, the package accepts forged
6//! logins, and nothing else it does matters.
7
8use crate::cbor::{Cbor, CborKey};
9use rustlavel_core::{Error, Result};
10use std::collections::BTreeMap;
11
12/// COSE label 1: which family of key this is.
13const LABEL_KTY: i64 = 1;
14/// COSE label 3: which algorithm it signs with.
15const LABEL_ALG: i64 = 3;
16/// COSE label -1: the curve, for both EC2 and OKP keys.
17const LABEL_CRV: i64 = -1;
18/// COSE label -2: `x`, or the whole public key for an OKP.
19const LABEL_X: i64 = -2;
20/// COSE label -3: `y`, for EC2 only.
21const LABEL_Y: i64 = -3;
22
23const KTY_OKP: i64 = 1;
24const KTY_EC2: i64 = 2;
25const KTY_RSA: i64 = 3;
26
27/// COSE label -1 again, meaning the modulus when the key type is RSA. The
28/// labels are per key type, not global — an RSA key has no curve.
29const LABEL_N: i64 = -1;
30/// COSE label -2 again, meaning the public exponent for an RSA key.
31const LABEL_E: i64 = -2;
32const CRV_P256: i64 = 1;
33const CRV_ED25519: i64 = 6;
34
35/// The algorithms this package will verify.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum SignatureAlgorithm {
38    /// COSE -7. ECDSA on P-256 with SHA-256, and what essentially every
39    /// passkey in the world uses: Apple, Google, Yubico, the password
40    /// managers.
41    Es256,
42    /// COSE -257. RSASSA-PKCS1-v1_5 with SHA-256. Not what a modern passkey
43    /// uses, and not optional either: TPM-backed Windows Hello credentials and
44    /// a number of older security keys sign with nothing else, and a relying
45    /// party that does not offer it turns those authenticators away at
46    /// registration.
47    Rs256,
48    /// COSE -8. Ed25519, which some security keys prefer.
49    EdDsa,
50}
51
52impl SignatureAlgorithm {
53    /// The number as it appears in a COSE key and in `pubKeyCredParams`.
54    pub fn cose_id(self) -> i64 {
55        match self {
56            SignatureAlgorithm::Es256 => -7,
57            SignatureAlgorithm::Rs256 => -257,
58            SignatureAlgorithm::EdDsa => -8,
59        }
60    }
61
62    pub fn from_cose_id(id: i64) -> Option<SignatureAlgorithm> {
63        match id {
64            -7 => Some(SignatureAlgorithm::Es256),
65            -8 => Some(SignatureAlgorithm::EdDsa),
66            -257 => Some(SignatureAlgorithm::Rs256),
67            _ => None,
68        }
69    }
70
71    /// What a relying party offers during registration, best first.
72    ///
73    /// ES256 leads because essentially every passkey in the world uses it.
74    /// RS256 is second because browsers warn when it is missing and because
75    /// the authenticators that need it can do nothing else. Everything listed
76    /// here can be verified by [`CoseKey::verify`]; offering an algorithm this
77    /// package cannot check would mean a ceremony that succeeds in the browser
78    /// and fails on the way back.
79    pub fn offered() -> [SignatureAlgorithm; 3] {
80        [SignatureAlgorithm::Es256, SignatureAlgorithm::Rs256, SignatureAlgorithm::EdDsa]
81    }
82}
83
84/// A public key an authenticator registered, in the only two shapes accepted.
85///
86/// Not `Debug`-derived: a public key is not a secret, but the credential it
87/// belongs to identifies a person, and a login flow is exactly the code whose
88/// logs get pasted into a bug report.
89#[derive(Clone, PartialEq, Eq)]
90pub enum CoseKey {
91    /// Uncompressed P-256 coordinates, 32 bytes each.
92    Es256 { x: [u8; 32], y: [u8; 32] },
93    /// A compressed Ed25519 point.
94    EdDsa { x: [u8; 32] },
95    /// An RSA modulus and public exponent, big-endian and unpadded.
96    Rs256 { n: Vec<u8>, e: Vec<u8> },
97}
98
99impl CoseKey {
100    pub fn algorithm(&self) -> SignatureAlgorithm {
101        match self {
102            CoseKey::Es256 { .. } => SignatureAlgorithm::Es256,
103            CoseKey::EdDsa { .. } => SignatureAlgorithm::EdDsa,
104            CoseKey::Rs256 { .. } => SignatureAlgorithm::Rs256,
105        }
106    }
107
108    /// Read a COSE_Key out of decoded CBOR.
109    ///
110    /// The algorithm is checked against the key type rather than trusted on
111    /// its own: a key claiming ES256 while carrying an Ed25519 point is not a
112    /// combination any honest authenticator produces, and accepting it would
113    /// mean verifying with something other than what was registered.
114    pub fn parse(value: &Cbor) -> Result<CoseKey> {
115        let kty = value
116            .get_int(LABEL_KTY)
117            .and_then(Cbor::as_i64)
118            .ok_or_else(|| Error::msg("the COSE key has no key type (label 1)"))?;
119        let alg = value
120            .get_int(LABEL_ALG)
121            .and_then(Cbor::as_i64)
122            .ok_or_else(|| Error::msg("the COSE key has no algorithm (label 3)"))?;
123        let crv = value.get_int(LABEL_CRV).and_then(Cbor::as_i64);
124
125        let algorithm = SignatureAlgorithm::from_cose_id(alg).ok_or_else(|| {
126            Error::msg(format!(
127                "COSE algorithm {alg} is not one this package verifies. It speaks ES256 (-7), \
128                 RS256 (-257) and EdDSA (-8)."
129            ))
130        })?;
131
132        match (algorithm, kty, crv) {
133            (SignatureAlgorithm::Es256, KTY_EC2, Some(CRV_P256)) => Ok(CoseKey::Es256 {
134                x: coordinate(value, LABEL_X, "x")?,
135                y: coordinate(value, LABEL_Y, "y")?,
136            }),
137            (SignatureAlgorithm::EdDsa, KTY_OKP, Some(CRV_ED25519)) => {
138                Ok(CoseKey::EdDsa { x: coordinate(value, LABEL_X, "x")? })
139            }
140            // An RSA key has no curve, so `crv` is read as absent: labels -1
141            // and -2 hold the modulus and the exponent here, not a curve and
142            // an x coordinate.
143            (SignatureAlgorithm::Rs256, KTY_RSA, None) => Ok(CoseKey::Rs256 {
144                n: big_endian(value, LABEL_N, "modulus")?,
145                e: big_endian(value, LABEL_E, "exponent")?,
146            }),
147            (algorithm, kty, crv) => Err(Error::msg(format!(
148                "the COSE key claims algorithm {} but has key type {kty} and curve {crv:?}, \
149                 which do not go together",
150                algorithm.cose_id()
151            ))),
152        }
153    }
154
155    /// The COSE_Key map this key came from.
156    ///
157    /// The labels and the values are the ones `parse` insists on, and no
158    /// others: kty, alg, crv, x and y for an EC2 key, and the same without y
159    /// for an OKP. Anything more would be a field this package never read and
160    /// so could not have checked.
161    ///
162    /// The pair `parse` / `to_cbor` is what makes a credential storable. A
163    /// store that keeps its credentials anywhere but memory needs bytes for a
164    /// column, and the bytes have to read back as the same key.
165    pub fn to_cbor(&self) -> Cbor {
166        let mut map = BTreeMap::new();
167        map.insert(CborKey::Int(LABEL_ALG), Cbor::Negative(self.algorithm().cose_id()));
168
169        match self {
170            CoseKey::Es256 { x, y } => {
171                map.insert(CborKey::Int(LABEL_KTY), Cbor::Unsigned(KTY_EC2 as u64));
172                map.insert(CborKey::Int(LABEL_CRV), Cbor::Unsigned(CRV_P256 as u64));
173                map.insert(CborKey::Int(LABEL_X), Cbor::Bytes(x.to_vec()));
174                map.insert(CborKey::Int(LABEL_Y), Cbor::Bytes(y.to_vec()));
175            }
176            CoseKey::EdDsa { x } => {
177                map.insert(CborKey::Int(LABEL_KTY), Cbor::Unsigned(KTY_OKP as u64));
178                map.insert(CborKey::Int(LABEL_CRV), Cbor::Unsigned(CRV_ED25519 as u64));
179                map.insert(CborKey::Int(LABEL_X), Cbor::Bytes(x.to_vec()));
180            }
181            CoseKey::Rs256 { n, e } => {
182                map.insert(CborKey::Int(LABEL_KTY), Cbor::Unsigned(KTY_RSA as u64));
183                map.insert(CborKey::Int(LABEL_N), Cbor::Bytes(n.clone()));
184                map.insert(CborKey::Int(LABEL_E), Cbor::Bytes(e.clone()));
185            }
186        }
187
188        Cbor::Map(map)
189    }
190
191    /// The key as canonical CTAP2 CBOR: what to put in a database column.
192    ///
193    /// Because both ends of this are canonical, storing what an authenticator
194    /// registered and reading it back gives the same bytes it sent — which is
195    /// worth having, because it means a stored credential can be compared with
196    /// the attestation it came from without parsing either.
197    pub fn to_bytes(&self) -> Vec<u8> {
198        self.to_cbor().to_bytes()
199    }
200
201    /// Whether `signature` really covers `message` under this key.
202    ///
203    /// The whole package rests here. A `false` — or an error — must mean the
204    /// signature is not good, and nothing that reaches this function may be
205    /// able to make it say otherwise.
206    pub fn verify(&self, message: &[u8], signature: &[u8]) -> bool {
207        match self {
208            CoseKey::Es256 { x, y } => verify_es256(x, y, message, signature),
209            CoseKey::EdDsa { x } => verify_eddsa(x, message, signature),
210            CoseKey::Rs256 { n, e } => crate::rsa::verify_pkcs1_sha256(n, e, message, signature),
211        }
212    }
213}
214
215/// Names the algorithm, never the key material.
216impl std::fmt::Debug for CoseKey {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        f.debug_struct("CoseKey")
219            .field("algorithm", &self.algorithm())
220            .field("key", &"<public key>")
221            .finish()
222    }
223}
224
225/// A byte string of any length, for the two RSA fields.
226fn big_endian(value: &Cbor, label: i64, name: &str) -> Result<Vec<u8>> {
227    let bytes = value
228        .get_int(label)
229        .and_then(Cbor::as_bytes)
230        .ok_or_else(|| Error::msg(format!("the RSA key has no {name} (label {label})")))?;
231    if bytes.is_empty() {
232        return Err(Error::msg(format!("the RSA key's {name} is empty")));
233    }
234    Ok(bytes.to_vec())
235}
236
237fn coordinate(value: &Cbor, label: i64, name: &str) -> Result<[u8; 32]> {
238    let bytes = value
239        .get_int(label)
240        .and_then(Cbor::as_bytes)
241        .ok_or_else(|| Error::msg(format!("the COSE key has no `{name}` coordinate")))?;
242
243    // Exactly 32, not "at most". A short coordinate left-padded by the reader
244    // is a different point from the one that was registered.
245    <[u8; 32]>::try_from(bytes).map_err(|_| {
246        Error::msg(format!(
247            "the COSE key's `{name}` coordinate is {} bytes, and must be exactly 32",
248            bytes.len()
249        ))
250    })
251}
252
253fn verify_es256(x: &[u8; 32], y: &[u8; 32], message: &[u8], signature: &[u8]) -> bool {
254    use p256::ecdsa::signature::Verifier;
255
256    let point = p256::EncodedPoint::from_affine_coordinates(x.into(), y.into(), false);
257    let Ok(key) = p256::ecdsa::VerifyingKey::from_encoded_point(&point) else {
258        return false;
259    };
260
261    // WebAuthn signatures are DER, not the fixed-width form. `from_der` also
262    // rejects the non-canonical encodings that let one signature be written
263    // several ways.
264    let Ok(parsed) = p256::ecdsa::Signature::from_der(signature) else {
265        return false;
266    };
267
268    key.verify(message, &parsed).is_ok()
269}
270
271fn verify_eddsa(x: &[u8; 32], message: &[u8], signature: &[u8]) -> bool {
272    use ed25519_dalek::Verifier;
273
274    let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(x) else {
275        return false;
276    };
277    let Ok(bytes) = <[u8; 64]>::try_from(signature) else {
278        return false;
279    };
280
281    key.verify(message, &ed25519_dalek::Signature::from_bytes(&bytes)).is_ok()
282}
283
284#[cfg(test)]
285mod tests {
286    use super::*;
287    use crate::cbor::Cbor;
288
289    /// `{1: 2, 3: -7, -1: 1, -2: x, -3: y}` — the shape every passkey sends.
290    fn es256_key(x: [u8; 32], y: [u8; 32]) -> Cbor {
291        let mut bytes = vec![0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20];
292        bytes.extend_from_slice(&x);
293        bytes.extend_from_slice(&[0x22, 0x58, 0x20]);
294        bytes.extend_from_slice(&y);
295        Cbor::parse(&bytes).expect("a well-formed COSE key")
296    }
297
298    /// A signing key from fixed bytes, so the test needs no randomness and
299    /// gives the same answer on every machine.
300    fn signing_key() -> p256::ecdsa::SigningKey {
301        p256::ecdsa::SigningKey::from_bytes(&[7u8; 32].into()).expect("a valid scalar")
302    }
303
304    fn public_coordinates(key: &p256::ecdsa::SigningKey) -> ([u8; 32], [u8; 32]) {
305        let point = key.verifying_key().to_encoded_point(false);
306        (
307            point.x().expect("an x coordinate").as_slice().try_into().unwrap(),
308            point.y().expect("a y coordinate").as_slice().try_into().unwrap(),
309        )
310    }
311
312    #[test]
313    fn reads_the_key_a_passkey_registers() {
314        let key = CoseKey::parse(&es256_key([1u8; 32], [2u8; 32])).unwrap();
315
316        assert_eq!(key.algorithm(), SignatureAlgorithm::Es256);
317        assert!(matches!(key, CoseKey::Es256 { x, y } if x == [1u8; 32] && y == [2u8; 32]));
318    }
319
320    #[test]
321    fn a_real_signature_verifies_and_a_tampered_one_does_not() {
322        use p256::ecdsa::signature::Signer;
323
324        let signing = signing_key();
325        let (x, y) = public_coordinates(&signing);
326        let key = CoseKey::parse(&es256_key(x, y)).unwrap();
327
328        let message = b"authenticator data || client data hash";
329        let signature: p256::ecdsa::Signature = signing.sign(message);
330        let der = signature.to_der();
331
332        assert!(key.verify(message, der.as_bytes()), "a genuine signature must verify");
333        assert!(!key.verify(b"a different message", der.as_bytes()), "wrong message");
334
335        // One bit of the signature, flipped.
336        let mut tampered = der.as_bytes().to_vec();
337        let last = tampered.len() - 1;
338        tampered[last] ^= 1;
339        assert!(!key.verify(message, &tampered), "a tampered signature must not verify");
340    }
341
342    #[test]
343    fn a_signature_from_a_different_key_does_not_verify() {
344        use p256::ecdsa::signature::Signer;
345
346        let signing = signing_key();
347        let other = p256::ecdsa::SigningKey::from_bytes(&[9u8; 32].into()).unwrap();
348        let (x, y) = public_coordinates(&signing);
349        let key = CoseKey::parse(&es256_key(x, y)).unwrap();
350
351        let message = b"assertion";
352        let signature: p256::ecdsa::Signature = other.sign(message);
353
354        assert!(!key.verify(message, signature.to_der().as_bytes()));
355    }
356
357    #[test]
358    fn garbage_in_the_signature_field_is_false_and_never_a_panic() {
359        // Everything here is attacker-controlled, so every shape must return
360        // false rather than unwinding out of a login handler.
361        let signing = signing_key();
362        let (x, y) = public_coordinates(&signing);
363        let key = CoseKey::parse(&es256_key(x, y)).unwrap();
364
365        for signature in [b"".as_slice(), b"\x00", &[0xff; 8], &[0x30; 72], &[0u8; 64]] {
366            assert!(!key.verify(b"message", signature));
367        }
368    }
369
370    #[test]
371    fn an_algorithm_that_disagrees_with_the_key_type_is_refused() {
372        // A key claiming ES256 while carrying an Ed25519 point is not something
373        // an honest authenticator produces, and accepting it would mean
374        // verifying with something other than what was registered.
375        let mut bytes = vec![0xa4, 0x01, 0x01, 0x03, 0x26, 0x20, 0x06, 0x21, 0x58, 0x20];
376        bytes.extend_from_slice(&[1u8; 32]);
377        let error = CoseKey::parse(&Cbor::parse(&bytes).unwrap()).unwrap_err().to_string();
378
379        assert!(error.contains("do not go together"), "got {error}");
380    }
381
382    #[test]
383    fn a_short_coordinate_is_refused_rather_than_padded() {
384        // Left-padding a 31-byte coordinate produces a different point from the
385        // one that was registered, and would verify against nothing.
386        let mut bytes = vec![0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x1f];
387        bytes.extend_from_slice(&[1u8; 31]);
388        bytes.extend_from_slice(&[0x22, 0x58, 0x20]);
389        bytes.extend_from_slice(&[2u8; 32]);
390
391        let error = CoseKey::parse(&Cbor::parse(&bytes).unwrap()).unwrap_err().to_string();
392        assert!(error.contains("31 bytes"), "got {error}");
393        assert!(error.contains("exactly 32"), "got {error}");
394    }
395
396    #[test]
397    fn an_rs256_key_round_trips_and_verifies_a_signature_openssl_made() {
398        // The COSE shape a TPM-backed Windows Hello credential registers with:
399        // `{1: 3, 3: -257, -1: n, -2: e}`. Labels -1 and -2 mean modulus and
400        // exponent here, not a curve and an x coordinate.
401        let n = crate::rsa::tests::openssl_modulus();
402        let e = vec![0x01, 0x00, 0x01];
403
404        let mut map = BTreeMap::new();
405        map.insert(CborKey::Int(1), Cbor::Unsigned(3));
406        map.insert(CborKey::Int(3), Cbor::Negative(-257));
407        map.insert(CborKey::Int(-1), Cbor::Bytes(n.clone()));
408        map.insert(CborKey::Int(-2), Cbor::Bytes(e.clone()));
409
410        let key = CoseKey::parse(&Cbor::Map(map)).unwrap();
411        assert_eq!(key.algorithm(), SignatureAlgorithm::Rs256);
412
413        // Stored and read back is the same key, which is what makes a
414        // credential survive a restart.
415        let stored = CoseKey::parse(&Cbor::parse(&key.to_bytes()).unwrap()).unwrap();
416        assert_eq!(stored, key);
417
418        assert!(stored.verify(
419            b"rustlavel webauthn rs256 test vector",
420            &crate::rsa::tests::openssl_signature()
421        ));
422        assert!(!stored.verify(b"something else", &crate::rsa::tests::openssl_signature()));
423    }
424
425    /// The three algorithms offered are exactly the three that can be checked,
426    /// and ES256 and RS256 are both among them — a browser warns when RS256 is
427    /// absent, because the authenticators that need it can do nothing else.
428    #[test]
429    fn the_offer_includes_es256_and_rs256() {
430        let ids: Vec<i64> = SignatureAlgorithm::offered().iter().map(|a| a.cose_id()).collect();
431
432        assert_eq!(ids, vec![-7, -257, -8]);
433    }
434
435    #[test]
436    fn debug_does_not_print_key_material() {
437        let key = CoseKey::parse(&es256_key([0xab; 32], [0xcd; 32])).unwrap();
438        let printed = format!("{key:?}");
439
440        assert!(!printed.contains("171"), "coordinates reached the output: {printed}");
441        assert!(printed.contains("Es256"));
442    }
443
444    /// `{1: 1, 3: -8, -1: 6, -2: x}` — the OKP shape, in canonical CTAP2
445    /// order, written out by hand.
446    ///
447    /// Unlike the EC2 case there is no fixture in this crate to borrow: the
448    /// fake authenticator the ceremony tests use only speaks ES256. These are
449    /// the bytes a security key that prefers Ed25519 puts on the wire, and the
450    /// point of writing them here rather than building them with the encoder
451    /// is that the encoder is what they are checking.
452    fn eddsa_key_bytes(x: &[u8; 32]) -> Vec<u8> {
453        let mut bytes = vec![0xa4, 0x01, 0x01, 0x03, 0x27, 0x20, 0x06, 0x21, 0x58, 0x20];
454        bytes.extend_from_slice(x);
455        bytes
456    }
457
458    #[test]
459    fn a_real_es256_key_re_encodes_to_the_bytes_the_authenticator_sent() {
460        // Byte-for-byte, not merely equivalent. A real authenticator already
461        // sends canonical CBOR, so anything less than identical bytes would
462        // mean this encoder and the specification disagree somewhere.
463        for seed in 1..=4 {
464            let device = crate::ceremony::fake::Authenticator::new(seed);
465            let sent = device.cose_key();
466            let key = CoseKey::parse(&Cbor::parse(&sent).unwrap()).unwrap();
467
468            assert_eq!(key.to_bytes(), sent, "seed {seed}");
469            assert_eq!(
470                CoseKey::parse(&Cbor::parse(&key.to_bytes()).unwrap()).unwrap(),
471                key,
472                "seed {seed} did not read back as itself"
473            );
474        }
475    }
476
477    #[test]
478    fn an_ed25519_key_re_encodes_to_the_bytes_a_security_key_sends() {
479        let signing = ed25519_dalek::SigningKey::from_bytes(&[11u8; 32]);
480        let sent = eddsa_key_bytes(&signing.verifying_key().to_bytes());
481
482        let key = CoseKey::parse(&Cbor::parse(&sent).unwrap()).unwrap();
483        assert_eq!(key.algorithm(), SignatureAlgorithm::EdDsa);
484        assert_eq!(key.to_bytes(), sent);
485        assert_eq!(CoseKey::parse(&Cbor::parse(&key.to_bytes()).unwrap()).unwrap(), key);
486    }
487
488    #[test]
489    fn a_stored_es256_key_still_verifies_a_real_assertion() {
490        // The one that matters. Self-consistency proves nothing on its own:
491        // an encoder that swapped x and y would still round-trip through its
492        // own parser. Only a genuine signature, made by the device that
493        // registered, says the stored bytes are the key that was registered.
494        let device = crate::ceremony::fake::Authenticator::new(4);
495        let auth_data =
496            device.authenticator_data("example.test", crate::ceremony::fake::ASSERTION_FLAGS, 9);
497        let client_data =
498            device.client_data("webauthn.get", b"a challenge", "https://example.test");
499        let signature = device.sign(&auth_data, &client_data);
500
501        let mut message = auth_data.clone();
502        message.extend_from_slice(&crate::ceremony::sha256(&client_data));
503
504        // Registered, written to bytes the way a database-backed store would,
505        // and read back out of them.
506        let registered = CoseKey::parse(&Cbor::parse(&device.cose_key()).unwrap()).unwrap();
507        let stored = CoseKey::parse(&Cbor::parse(&registered.to_bytes()).unwrap()).unwrap();
508
509        assert!(registered.verify(&message, &signature), "the fixture itself must verify");
510        assert!(stored.verify(&message, &signature), "the key survived storage in name only");
511    }
512
513    #[test]
514    fn a_stored_ed25519_key_still_verifies_a_real_signature() {
515        use ed25519_dalek::Signer;
516
517        let signing = ed25519_dalek::SigningKey::from_bytes(&[11u8; 32]);
518        let sent = eddsa_key_bytes(&signing.verifying_key().to_bytes());
519        let registered = CoseKey::parse(&Cbor::parse(&sent).unwrap()).unwrap();
520        let stored = CoseKey::parse(&Cbor::parse(&registered.to_bytes()).unwrap()).unwrap();
521
522        let message = b"authenticator data || client data hash";
523        let signature = signing.sign(message).to_bytes();
524
525        assert!(stored.verify(message, &signature));
526        assert!(!stored.verify(b"a different message", &signature));
527    }
528
529    #[test]
530    fn the_written_key_carries_the_labels_parse_reads_and_nothing_else() {
531        // A label this package never read is a label it never checked, so
532        // writing one out would be claiming more than was verified.
533        let key = CoseKey::parse(&es256_key([1u8; 32], [2u8; 32])).unwrap();
534        let Cbor::Map(map) = key.to_cbor() else { unreachable!("a COSE key is a map") };
535
536        assert_eq!(map.len(), 5);
537        for label in [LABEL_KTY, LABEL_ALG, LABEL_CRV, LABEL_X, LABEL_Y] {
538            assert!(map.contains_key(&CborKey::Int(label)), "label {label} is missing");
539        }
540
541        let key = CoseKey::EdDsa { x: [3u8; 32] };
542        let Cbor::Map(map) = key.to_cbor() else { unreachable!("a COSE key is a map") };
543
544        assert_eq!(map.len(), 4);
545        assert!(!map.contains_key(&CborKey::Int(LABEL_Y)), "an OKP key has no y");
546    }
547
548    #[test]
549    fn the_offered_algorithms_are_the_ones_that_can_be_verified() {
550        // An offer the verifier cannot honour registers a credential that can
551        // never log in again.
552        for algorithm in SignatureAlgorithm::offered() {
553            assert_eq!(SignatureAlgorithm::from_cose_id(algorithm.cose_id()), Some(algorithm));
554        }
555    }
556}