Skip to main content

isb_server/auth/
webauthn.rs

1//! WebAuthn (passkeys), verified directly: CBOR from [`super::cbor`],
2//! signatures from `ring`. Implemented here rather than with a crate because
3//! the common Rust WebAuthn crate is MPL-2.0.
4//!
5//! - Algorithms: ES256 (P-256), EdDSA (Ed25519) and RS256.
6//! - Attestation is not verified (isb asks for `none`): a passkey is trusted
7//!   because the signed-in user registered it, not because of who made it.
8//! - Checked on every ceremony: `clientDataJSON` type, challenge and origin
9//!   (exact match with the relying party's origin, no cross-origin frames),
10//!   the authenticator data's `rpIdHash`, user present, user verified when
11//!   required, and on sign-in the signature and the signature counter.
12
13use base64::Engine;
14use base64::engine::general_purpose::URL_SAFE_NO_PAD;
15use serde::Deserialize;
16
17use super::cbor::{self, Value};
18
19/// COSE algorithm identifiers isb accepts, in preference order.
20pub 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/// Who passkeys are bound to: the RP id is the host of `ISB_PUBLIC_URL`,
31/// the origin its scheme, host and port.
32#[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    /// From a public URL like `https://isb.example.com` (a path is ignored).
41    /// Plain http is accepted for `localhost` only, which browsers treat as
42    /// a secure context.
43    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        // ASCII lowercasing keeps the length, so the port starts after it.
73        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
107/// base64url, padded or not (some clients pad).
108pub 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/// A credential public key, parsed from its COSE form.
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum PublicKey {
117    /// The uncompressed SEC1 point, `04 || x || y`.
118    Es256(Vec<u8>),
119    Ed25519(Vec<u8>),
120    Rs256 {
121        n: Vec<u8>,
122        e: Vec<u8>,
123    },
124}
125
126impl PublicKey {
127    /// Parse a COSE_Key (RFC 9053), refusing algorithms isb does not accept
128    /// and keys whose type or curve does not match their algorithm.
129    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    /// Check `sig` over `msg`. ES256 signatures are ASN.1 DER, as WebAuthn
185    /// sends them.
186    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/// Parsed authenticator data.
203#[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/// The attested credential data of a registration.
212#[derive(Debug, Clone)]
213pub struct Attested {
214    pub aaguid: [u8; 16],
215    pub credential_id: Vec<u8>,
216    /// The COSE_Key bytes exactly as the authenticator encoded them.
217    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
293/// The challenge in a `clientDataJSON`, to find the ceremony it answers.
294pub 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/// A verified registration, ready to store.
326#[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
336/// Verify `navigator.credentials.create()`'s answer to `challenge`.
337pub 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    // The format is reported but not verified: isb asks for `none`, and a
348    // passkey is trusted because the signed-in user added it.
349    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/// Verify `navigator.credentials.get()`'s answer to `challenge` against a
374/// stored credential. Returns the new signature counter.
375#[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
399/// The counter must grow, unless the authenticator does not keep one (both
400/// zero). A regression suggests a cloned authenticator.
401pub 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/// Software authenticators for tests: ES256 and Ed25519 keys made with ring,
412/// authenticator data and clientDataJSON built by hand.
413#[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        /// Flags to set on the next ceremony (UP|UV by default).
430        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        /// `(clientDataJSON, attestationObject)` for `create()`.
507        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        /// `(clientDataJSON, authenticatorData, signature)` for `get()`,
521        /// bumping the counter first (unless it is zero: no counter).
522        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, &reg.public_key_cose, 5, &cd, &ad, &sig, true).unwrap();
599            assert_eq!(n, 6);
600            // The same assertion against a counter that has moved on: refused.
601            let e = verify_assertion(&rp, &ch2, &reg.public_key_cose, 6, &cd, &ad, &sig, true)
602                .unwrap_err();
603            assert!(e.contains("counter"), "{e}");
604            // A flipped signature byte.
605            let mut bad = sig.clone();
606            let last = bad.len() - 1;
607            bad[last] ^= 1;
608            assert!(
609                verify_assertion(&rp, &ch2, &reg.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        // Wrong challenge, wrong origin, wrong type, wrong RP id.
645        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        // No user verification when it was required; no user presence ever.
670        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        // Cross-origin.
687        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        // ES256 on the wrong curve, an unknown algorithm.
715        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}