1use crate::cbor::{Cbor, CborKey};
9use rustlavel_core::{Error, Result};
10use std::collections::BTreeMap;
11
12const LABEL_KTY: i64 = 1;
14const LABEL_ALG: i64 = 3;
16const LABEL_CRV: i64 = -1;
18const LABEL_X: i64 = -2;
20const LABEL_Y: i64 = -3;
22
23const KTY_OKP: i64 = 1;
24const KTY_EC2: i64 = 2;
25const KTY_RSA: i64 = 3;
26
27const LABEL_N: i64 = -1;
30const LABEL_E: i64 = -2;
32const CRV_P256: i64 = 1;
33const CRV_ED25519: i64 = 6;
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum SignatureAlgorithm {
38 Es256,
42 Rs256,
48 EdDsa,
50}
51
52impl SignatureAlgorithm {
53 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 pub fn offered() -> [SignatureAlgorithm; 3] {
80 [SignatureAlgorithm::Es256, SignatureAlgorithm::Rs256, SignatureAlgorithm::EdDsa]
81 }
82}
83
84#[derive(Clone, PartialEq, Eq)]
90pub enum CoseKey {
91 Es256 { x: [u8; 32], y: [u8; 32] },
93 EdDsa { x: [u8; 32] },
95 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 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 (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 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 pub fn to_bytes(&self) -> Vec<u8> {
198 self.to_cbor().to_bytes()
199 }
200
201 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
215impl 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
225fn 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 <[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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 let registered = CoseKey::parse(&Cbor::parse(&device.cose_key()).unwrap()).unwrap();
507 let stored = CoseKey::parse(&Cbor::parse(®istered.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(®istered.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 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 for algorithm in SignatureAlgorithm::offered() {
553 assert_eq!(SignatureAlgorithm::from_cose_id(algorithm.cose_id()), Some(algorithm));
554 }
555 }
556}