1use base64::Engine;
14use base64::engine::general_purpose::URL_SAFE_NO_PAD;
15use serde::Deserialize;
16
17use super::cbor::{self, Value};
18
19pub const ES256: i64 = -7;
21pub const EDDSA: i64 = -8;
22pub const RS256: i64 = -257;
23pub const ALGORITHMS: [i64; 3] = [ES256, EDDSA, RS256];
24
25const FLAG_UP: u8 = 0x01;
26const FLAG_UV: u8 = 0x04;
27const FLAG_AT: u8 = 0x40;
28const MAX_CREDENTIAL_ID: usize = 1023;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct RelyingParty {
34 pub id: String,
35 pub origin: String,
36 pub name: String,
37}
38
39impl RelyingParty {
40 pub fn from_public_url(url: &str) -> Result<RelyingParty, String> {
44 let u = url.trim();
45 let (scheme, rest) = u
46 .split_once("://")
47 .ok_or_else(|| format!("public URL {u:?} has no scheme"))?;
48 let scheme = scheme.to_ascii_lowercase();
49 let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
50 if authority.is_empty() || authority.contains('@') {
51 return Err(format!("public URL {u:?} has no usable host"));
52 }
53 let host = authority
54 .split(':')
55 .next()
56 .unwrap_or("")
57 .to_ascii_lowercase();
58 if authority.starts_with('[') || host.parse::<std::net::IpAddr>().is_ok() {
59 return Err(format!(
60 "public URL {u:?}: passkeys need a domain name, not an IP address"
61 ));
62 }
63 match scheme.as_str() {
64 "https" => {}
65 "http" if host == "localhost" => {}
66 _ => {
67 return Err(format!(
68 "public URL {u:?}: passkeys need https (or http://localhost)"
69 ));
70 }
71 }
72 let port = &authority[host.len()..];
74 if !(port.is_empty() || port.len() > 1 && port[1..].bytes().all(|b| b.is_ascii_digit())) {
75 return Err(format!("public URL {u:?} has a bad port"));
76 }
77 let default_port =
78 (scheme == "https" && port == ":443") || (scheme == "http" && port == ":80");
79 let origin = if port.is_empty() || default_port {
80 format!("{scheme}://{host}")
81 } else {
82 format!("{scheme}://{host}{port}")
83 };
84 Ok(RelyingParty {
85 id: host,
86 origin,
87 name: "isb".into(),
88 })
89 }
90
91 fn id_hash(&self) -> [u8; 32] {
92 sha256(self.id.as_bytes())
93 }
94}
95
96pub fn sha256(b: &[u8]) -> [u8; 32] {
97 ring::digest::digest(&ring::digest::SHA256, b)
98 .as_ref()
99 .try_into()
100 .expect("SHA-256 is 32 bytes")
101}
102
103pub fn b64(b: &[u8]) -> String {
104 URL_SAFE_NO_PAD.encode(b)
105}
106
107pub fn unb64(s: &str) -> Result<Vec<u8>, String> {
109 URL_SAFE_NO_PAD
110 .decode(s.trim().trim_end_matches('='))
111 .map_err(|_| "not base64url".to_string())
112}
113
114#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum PublicKey {
117 Es256(Vec<u8>),
119 Ed25519(Vec<u8>),
120 Rs256 {
121 n: Vec<u8>,
122 e: Vec<u8>,
123 },
124}
125
126impl PublicKey {
127 pub fn from_cose(bytes: &[u8]) -> Result<PublicKey, String> {
130 let k = cbor::decode_all(bytes).map_err(|e| format!("credential public key: {e}"))?;
131 let int = |l: i64| k.get_int(l).and_then(Value::as_int);
132 let bytes_at = |l: i64| k.get_int(l).and_then(Value::as_bytes);
133 let alg = int(3).ok_or("credential public key has no algorithm")?;
134 let kty = int(1).ok_or("credential public key has no key type")?;
135 match (alg, kty) {
136 (ES256, 2) => {
137 if int(-1) != Some(1) {
138 return Err("ES256 key is not on P-256".into());
139 }
140 let (x, y) = (bytes_at(-2), bytes_at(-3));
141 match (x, y) {
142 (Some(x), Some(y)) if x.len() == 32 && y.len() == 32 => {
143 let mut p = Vec::with_capacity(65);
144 p.push(4);
145 p.extend_from_slice(x);
146 p.extend_from_slice(y);
147 Ok(PublicKey::Es256(p))
148 }
149 _ => Err("ES256 key has malformed coordinates".into()),
150 }
151 }
152 (EDDSA, 1) => {
153 if int(-1) != Some(6) {
154 return Err("EdDSA key is not Ed25519".into());
155 }
156 match bytes_at(-2) {
157 Some(x) if x.len() == 32 => Ok(PublicKey::Ed25519(x.to_vec())),
158 _ => Err("Ed25519 key is malformed".into()),
159 }
160 }
161 (RS256, 3) => match (bytes_at(-1), bytes_at(-2)) {
162 (Some(n), Some(e)) if n.len() >= 256 && !e.is_empty() && e.len() <= 4 => {
163 Ok(PublicKey::Rs256 {
164 n: n.to_vec(),
165 e: e.to_vec(),
166 })
167 }
168 _ => Err("RS256 key is malformed or shorter than 2048 bits".into()),
169 },
170 (alg, kty) => Err(format!(
171 "unsupported credential key (alg {alg}, kty {kty}); isb accepts ES256, EdDSA and RS256"
172 )),
173 }
174 }
175
176 pub fn alg(&self) -> i64 {
177 match self {
178 PublicKey::Es256(_) => ES256,
179 PublicKey::Ed25519(_) => EDDSA,
180 PublicKey::Rs256 { .. } => RS256,
181 }
182 }
183
184 pub fn verify(&self, msg: &[u8], sig: &[u8]) -> bool {
187 use ring::signature as s;
188 match self {
189 PublicKey::Es256(p) => s::UnparsedPublicKey::new(&s::ECDSA_P256_SHA256_ASN1, p)
190 .verify(msg, sig)
191 .is_ok(),
192 PublicKey::Ed25519(p) => s::UnparsedPublicKey::new(&s::ED25519, p)
193 .verify(msg, sig)
194 .is_ok(),
195 PublicKey::Rs256 { n, e } => s::RsaPublicKeyComponents { n, e }
196 .verify(&s::RSA_PKCS1_2048_8192_SHA256, msg, sig)
197 .is_ok(),
198 }
199 }
200}
201
202#[derive(Debug, Clone)]
204pub struct AuthData {
205 pub rp_id_hash: [u8; 32],
206 pub flags: u8,
207 pub sign_count: u32,
208 pub attested: Option<Attested>,
209}
210
211#[derive(Debug, Clone)]
213pub struct Attested {
214 pub aaguid: [u8; 16],
215 pub credential_id: Vec<u8>,
216 pub public_key_cose: Vec<u8>,
218}
219
220impl AuthData {
221 pub fn parse(b: &[u8]) -> Result<AuthData, String> {
222 if b.len() < 37 {
223 return Err("authenticator data is too short".into());
224 }
225 let rp_id_hash: [u8; 32] = b[..32].try_into().unwrap();
226 let flags = b[32];
227 let sign_count = u32::from_be_bytes(b[33..37].try_into().unwrap());
228 let attested = if flags & FLAG_AT != 0 {
229 let rest = &b[37..];
230 if rest.len() < 18 {
231 return Err("attested credential data is truncated".into());
232 }
233 let aaguid: [u8; 16] = rest[..16].try_into().unwrap();
234 let n = u16::from_be_bytes([rest[16], rest[17]]) as usize;
235 if n == 0 || n > MAX_CREDENTIAL_ID {
236 return Err(format!("credential id length {n} is out of range"));
237 }
238 let id_end = 18 + n;
239 let credential_id = rest
240 .get(18..id_end)
241 .ok_or("credential id is truncated")?
242 .to_vec();
243 let (_, used) =
244 cbor::decode(&rest[id_end..]).map_err(|e| format!("credential public key: {e}"))?;
245 Some(Attested {
246 aaguid,
247 credential_id,
248 public_key_cose: rest[id_end..id_end + used].to_vec(),
249 })
250 } else {
251 None
252 };
253 Ok(AuthData {
254 rp_id_hash,
255 flags,
256 sign_count,
257 attested,
258 })
259 }
260
261 pub fn user_present(&self) -> bool {
262 self.flags & FLAG_UP != 0
263 }
264
265 pub fn user_verified(&self) -> bool {
266 self.flags & FLAG_UV != 0
267 }
268
269 fn check(&self, rp: &RelyingParty, require_uv: bool) -> Result<(), String> {
270 if !super::secret::ct_eq(&self.rp_id_hash, &rp.id_hash()) {
271 return Err(format!("the credential is not for {}", rp.id));
272 }
273 if !self.user_present() {
274 return Err("the authenticator did not see a user present".into());
275 }
276 if require_uv && !self.user_verified() {
277 return Err("the authenticator did not verify the user".into());
278 }
279 Ok(())
280 }
281}
282
283#[derive(Deserialize)]
284struct ClientData {
285 #[serde(rename = "type")]
286 typ: String,
287 challenge: String,
288 origin: String,
289 #[serde(default, rename = "crossOrigin")]
290 cross_origin: Option<bool>,
291}
292
293pub fn client_challenge(client_data_json: &[u8]) -> Option<Vec<u8>> {
295 let c: ClientData = serde_json::from_slice(client_data_json).ok()?;
296 unb64(&c.challenge).ok()
297}
298
299fn check_client_data(
300 rp: &RelyingParty,
301 json: &[u8],
302 typ: &str,
303 challenge: &[u8],
304) -> Result<(), String> {
305 let c: ClientData = serde_json::from_slice(json).map_err(|e| format!("clientDataJSON: {e}"))?;
306 if c.typ != typ {
307 return Err(format!(
308 "clientDataJSON type is {:?}, wanted {typ:?}",
309 c.typ
310 ));
311 }
312 let got = unb64(&c.challenge).map_err(|e| format!("clientDataJSON challenge: {e}"))?;
313 if !super::secret::ct_eq(&got, challenge) {
314 return Err("the challenge does not match".into());
315 }
316 if c.origin != rp.origin {
317 return Err(format!("origin {:?} is not {:?}", c.origin, rp.origin));
318 }
319 if c.cross_origin == Some(true) {
320 return Err("cross-origin ceremonies are refused".into());
321 }
322 Ok(())
323}
324
325#[derive(Debug, Clone)]
327pub struct Registration {
328 pub credential_id: Vec<u8>,
329 pub public_key_cose: Vec<u8>,
330 pub alg: i64,
331 pub sign_count: u32,
332 pub aaguid: [u8; 16],
333 pub user_verified: bool,
334}
335
336pub fn verify_registration(
338 rp: &RelyingParty,
339 challenge: &[u8],
340 client_data_json: &[u8],
341 attestation_object: &[u8],
342 require_uv: bool,
343) -> Result<Registration, String> {
344 check_client_data(rp, client_data_json, "webauthn.create", challenge)?;
345 let att =
346 cbor::decode_all(attestation_object).map_err(|e| format!("attestation object: {e}"))?;
347 att.get_text("fmt")
350 .and_then(Value::as_text)
351 .ok_or("attestation object has no fmt")?;
352 let auth = att
353 .get_text("authData")
354 .and_then(Value::as_bytes)
355 .ok_or("attestation object has no authData")?;
356 let ad = AuthData::parse(auth)?;
357 ad.check(rp, require_uv)?;
358 let user_verified = ad.user_verified();
359 let a = ad
360 .attested
361 .ok_or("registration carries no attested credential")?;
362 let key = PublicKey::from_cose(&a.public_key_cose)?;
363 Ok(Registration {
364 credential_id: a.credential_id,
365 alg: key.alg(),
366 public_key_cose: a.public_key_cose,
367 sign_count: ad.sign_count,
368 aaguid: a.aaguid,
369 user_verified,
370 })
371}
372
373#[expect(clippy::too_many_arguments)]
376pub fn verify_assertion(
377 rp: &RelyingParty,
378 challenge: &[u8],
379 public_key_cose: &[u8],
380 stored_count: u32,
381 client_data_json: &[u8],
382 authenticator_data: &[u8],
383 signature: &[u8],
384 require_uv: bool,
385) -> Result<u32, String> {
386 check_client_data(rp, client_data_json, "webauthn.get", challenge)?;
387 let ad = AuthData::parse(authenticator_data)?;
388 ad.check(rp, require_uv)?;
389 let key = PublicKey::from_cose(public_key_cose)?;
390 let mut signed = authenticator_data.to_vec();
391 signed.extend_from_slice(&sha256(client_data_json));
392 if !key.verify(&signed, signature) {
393 return Err("bad signature".into());
394 }
395 check_counter(stored_count, ad.sign_count)?;
396 Ok(ad.sign_count)
397}
398
399pub fn check_counter(stored: u32, new: u32) -> Result<(), String> {
402 if (stored == 0 && new == 0) || new > stored {
403 Ok(())
404 } else {
405 Err(format!(
406 "signature counter went from {stored} to {new}: the authenticator may be cloned"
407 ))
408 }
409}
410
411#[cfg(test)]
414pub(crate) mod testkit {
415 use super::*;
416 use cbor::{Value, int};
417 use ring::rand::SystemRandom;
418 use ring::signature::{self as s, EcdsaKeyPair, Ed25519KeyPair, KeyPair};
419
420 pub enum Key {
421 Es256(EcdsaKeyPair),
422 Ed25519(Ed25519KeyPair),
423 }
424
425 pub struct Authenticator {
426 pub key: Key,
427 pub credential_id: Vec<u8>,
428 pub counter: u32,
429 pub flags: u8,
431 }
432
433 impl Authenticator {
434 pub fn es256() -> Self {
435 let rng = SystemRandom::new();
436 let pk8 =
437 EcdsaKeyPair::generate_pkcs8(&s::ECDSA_P256_SHA256_ASN1_SIGNING, &rng).unwrap();
438 let kp =
439 EcdsaKeyPair::from_pkcs8(&s::ECDSA_P256_SHA256_ASN1_SIGNING, pk8.as_ref(), &rng)
440 .unwrap();
441 Self::with(Key::Es256(kp))
442 }
443
444 pub fn ed25519() -> Self {
445 let pk8 = Ed25519KeyPair::generate_pkcs8(&SystemRandom::new()).unwrap();
446 Self::with(Key::Ed25519(
447 Ed25519KeyPair::from_pkcs8(pk8.as_ref()).unwrap(),
448 ))
449 }
450
451 fn with(key: Key) -> Self {
452 let id: [u8; 16] = crate::auth::secret::random_bytes().unwrap();
453 Authenticator {
454 key,
455 credential_id: id.to_vec(),
456 counter: 0,
457 flags: FLAG_UP | FLAG_UV,
458 }
459 }
460
461 pub fn cose(&self) -> Vec<u8> {
462 let m = match &self.key {
463 Key::Es256(kp) => {
464 let p = kp.public_key().as_ref();
465 vec![
466 (int(1), int(2)),
467 (int(3), int(ES256)),
468 (int(-1), int(1)),
469 (int(-2), Value::Bytes(p[1..33].to_vec())),
470 (int(-3), Value::Bytes(p[33..65].to_vec())),
471 ]
472 }
473 Key::Ed25519(kp) => vec![
474 (int(1), int(1)),
475 (int(3), int(EDDSA)),
476 (int(-1), int(6)),
477 (int(-2), Value::Bytes(kp.public_key().as_ref().to_vec())),
478 ],
479 };
480 cbor::encode(&Value::Map(m))
481 }
482
483 pub fn client_data(typ: &str, challenge: &[u8], origin: &str) -> Vec<u8> {
484 serde_json::to_vec(&serde_json::json!({
485 "type": typ,
486 "challenge": b64(challenge),
487 "origin": origin,
488 "crossOrigin": false,
489 }))
490 .unwrap()
491 }
492
493 fn auth_data(&self, rp_id: &str, attested: bool) -> Vec<u8> {
494 let mut a = sha256(rp_id.as_bytes()).to_vec();
495 a.push(self.flags | if attested { FLAG_AT } else { 0 });
496 a.extend(self.counter.to_be_bytes());
497 if attested {
498 a.extend([0x42; 16]);
499 a.extend((self.credential_id.len() as u16).to_be_bytes());
500 a.extend(&self.credential_id);
501 a.extend(self.cose());
502 }
503 a
504 }
505
506 pub fn register(&self, rp_id: &str, origin: &str, challenge: &[u8]) -> (Vec<u8>, Vec<u8>) {
508 let cd = Self::client_data("webauthn.create", challenge, origin);
509 let att = Value::Map(vec![
510 (Value::Text("fmt".into()), Value::Text("none".into())),
511 (Value::Text("attStmt".into()), Value::Map(vec![])),
512 (
513 Value::Text("authData".into()),
514 Value::Bytes(self.auth_data(rp_id, true)),
515 ),
516 ]);
517 (cd, cbor::encode(&att))
518 }
519
520 pub fn assert(
523 &mut self,
524 rp_id: &str,
525 origin: &str,
526 challenge: &[u8],
527 ) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
528 if self.counter > 0 {
529 self.counter += 1;
530 }
531 let cd = Self::client_data("webauthn.get", challenge, origin);
532 let ad = self.auth_data(rp_id, false);
533 let mut msg = ad.clone();
534 msg.extend(sha256(&cd));
535 let sig = match &self.key {
536 Key::Es256(kp) => kp
537 .sign(&SystemRandom::new(), &msg)
538 .unwrap()
539 .as_ref()
540 .to_vec(),
541 Key::Ed25519(kp) => kp.sign(&msg).as_ref().to_vec(),
542 };
543 (cd, ad, sig)
544 }
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use super::testkit::Authenticator;
551 use super::*;
552
553 fn rp() -> RelyingParty {
554 RelyingParty::from_public_url("https://isb.example.com").unwrap()
555 }
556
557 #[test]
558 fn relying_party_from_public_url() {
559 let r = rp();
560 assert_eq!(r.id, "isb.example.com");
561 assert_eq!(r.origin, "https://isb.example.com");
562 let r = RelyingParty::from_public_url("https://ISB.example.com:8443/app/").unwrap();
563 assert_eq!(r.origin, "https://isb.example.com:8443");
564 let r = RelyingParty::from_public_url("https://isb.example.com:443").unwrap();
565 assert_eq!(r.origin, "https://isb.example.com");
566 let r = RelyingParty::from_public_url("http://localhost:8092").unwrap();
567 assert_eq!(
568 (r.id.as_str(), r.origin.as_str()),
569 ("localhost", "http://localhost:8092")
570 );
571 for bad in [
572 "http://isb.example.com",
573 "https://127.0.0.1:8092",
574 "https://[::1]",
575 "isb.example.com",
576 "https://",
577 "https://u@isb.example.com",
578 ] {
579 assert!(RelyingParty::from_public_url(bad).is_err(), "{bad}");
580 }
581 }
582
583 #[test]
584 fn es256_and_ed25519_register_and_sign_in() {
585 let rp = rp();
586 for mut a in [Authenticator::es256(), Authenticator::ed25519()] {
587 a.counter = 5;
588 let ch = b"registration challenge 32 bytes!".to_vec();
589 let (cd, att) = a.register(&rp.id, &rp.origin, &ch);
590 let reg = verify_registration(&rp, &ch, &cd, &att, true).unwrap();
591 assert_eq!(reg.credential_id, a.credential_id);
592 assert_eq!(reg.sign_count, 5);
593 assert_eq!(reg.aaguid, [0x42; 16]);
594 assert!(reg.user_verified);
595 let ch2 = b"sign-in challenge".to_vec();
596 let (cd, ad, sig) = a.assert(&rp.id, &rp.origin, &ch2);
597 let n =
598 verify_assertion(&rp, &ch2, ®.public_key_cose, 5, &cd, &ad, &sig, true).unwrap();
599 assert_eq!(n, 6);
600 let e = verify_assertion(&rp, &ch2, ®.public_key_cose, 6, &cd, &ad, &sig, true)
602 .unwrap_err();
603 assert!(e.contains("counter"), "{e}");
604 let mut bad = sig.clone();
606 let last = bad.len() - 1;
607 bad[last] ^= 1;
608 assert!(
609 verify_assertion(&rp, &ch2, ®.public_key_cose, 5, &cd, &ad, &bad, true).is_err()
610 );
611 }
612 }
613
614 #[test]
615 fn rs256_keys_verify() {
616 use ring::signature::{RSA_PKCS1_SHA256, RsaKeyPair};
617 let kp = RsaKeyPair::from_pkcs8(include_bytes!("../server/testdata/access_test_key.pk8"))
618 .unwrap();
619 let p = ring::rsa::PublicKeyComponents::<Vec<u8>>::from(kp.public());
620 let cose = cbor::encode(&Value::Map(vec![
621 (cbor::int(1), cbor::int(3)),
622 (cbor::int(3), cbor::int(RS256)),
623 (cbor::int(-1), Value::Bytes(p.n.clone())),
624 (cbor::int(-2), Value::Bytes(p.e.clone())),
625 ]));
626 let key = PublicKey::from_cose(&cose).unwrap();
627 let mut sig = vec![0; kp.public().modulus_len()];
628 kp.sign(
629 &RSA_PKCS1_SHA256,
630 &ring::rand::SystemRandom::new(),
631 b"msg",
632 &mut sig,
633 )
634 .unwrap();
635 assert!(key.verify(b"msg", &sig));
636 assert!(!key.verify(b"msh", &sig));
637 }
638
639 #[test]
640 fn ceremony_checks() {
641 let rp = rp();
642 let a = Authenticator::es256();
643 let ch = b"challenge".to_vec();
644 let (cd, att) = a.register(&rp.id, &rp.origin, &ch);
646 assert!(
647 verify_registration(&rp, b"other", &cd, &att, true)
648 .unwrap_err()
649 .contains("challenge")
650 );
651 let (cd, att) = a.register(&rp.id, "https://evil.example", &ch);
652 assert!(
653 verify_registration(&rp, &ch, &cd, &att, true)
654 .unwrap_err()
655 .contains("origin")
656 );
657 let cd = Authenticator::client_data("webauthn.get", &ch, &rp.origin);
658 assert!(
659 verify_registration(&rp, &ch, &cd, &att, true)
660 .unwrap_err()
661 .contains("type")
662 );
663 let (cd, att) = a.register("evil.example", &rp.origin, &ch);
664 assert!(
665 verify_registration(&rp, &ch, &cd, &att, true)
666 .unwrap_err()
667 .contains("not for")
668 );
669 let mut a = Authenticator::es256();
671 a.flags = 0x01;
672 let (cd, att) = a.register(&rp.id, &rp.origin, &ch);
673 assert!(
674 verify_registration(&rp, &ch, &cd, &att, true)
675 .unwrap_err()
676 .contains("verify")
677 );
678 assert!(verify_registration(&rp, &ch, &cd, &att, false).is_ok());
679 a.flags = 0x04;
680 let (cd, att) = a.register(&rp.id, &rp.origin, &ch);
681 assert!(
682 verify_registration(&rp, &ch, &cd, &att, false)
683 .unwrap_err()
684 .contains("present")
685 );
686 let cd = serde_json::to_vec(&serde_json::json!({
688 "type": "webauthn.create", "challenge": b64(&ch),
689 "origin": rp.origin, "crossOrigin": true,
690 }))
691 .unwrap();
692 let a = Authenticator::es256();
693 let (_, att) = a.register(&rp.id, &rp.origin, &ch);
694 assert!(
695 verify_registration(&rp, &ch, &cd, &att, true)
696 .unwrap_err()
697 .contains("cross-origin")
698 );
699 assert_eq!(client_challenge(&cd), Some(ch));
700 }
701
702 #[test]
703 fn counters() {
704 assert!(check_counter(0, 0).is_ok());
705 assert!(check_counter(0, 1).is_ok());
706 assert!(check_counter(7, 8).is_ok());
707 assert!(check_counter(7, 7).is_err());
708 assert!(check_counter(7, 3).is_err());
709 assert!(check_counter(7, 0).is_err());
710 }
711
712 #[test]
713 fn refuses_unsupported_keys() {
714 let k = cbor::encode(&Value::Map(vec![
716 (cbor::int(1), cbor::int(2)),
717 (cbor::int(3), cbor::int(ES256)),
718 (cbor::int(-1), cbor::int(2)),
719 (cbor::int(-2), Value::Bytes(vec![1; 48])),
720 (cbor::int(-3), Value::Bytes(vec![1; 48])),
721 ]));
722 assert!(PublicKey::from_cose(&k).is_err());
723 let k = cbor::encode(&Value::Map(vec![
724 (cbor::int(1), cbor::int(2)),
725 (cbor::int(3), cbor::int(-35)),
726 ]));
727 assert!(
728 PublicKey::from_cose(&k)
729 .unwrap_err()
730 .contains("unsupported")
731 );
732 }
733}