---
url: 'https://www.corbado.com/blog/webauthn-credprotect-security-keys'
title: 'WebAuthn credProtect: Security Keys'
description: 'How the credProtect extension affects security key interoperability across Chrome, Safari and Firefox - and what relying parties can do.'
lang: 'en'
author: 'Vincent Delitz'
date: '2026-03-02T19:59:07.967Z'
lastModified: '2026-03-26T07:01:13.950Z'
keywords: 'webauthn credProtect, credentialProtectionPolicy, security key passkey, user verification security key, cross-browser passkeys'
category: 'WebAuthn Know-How'
---

# WebAuthn credProtect: Security Keys

## Key Facts

- **credProtect** (credentialProtectionPolicy) is a CTAP 2.1 extension controlling whether
  a security key credential can be enumerated or used without user verification.
- Chrome silently escalates to **credProtect Level 3** when residentKey is set to required
  and userVerification is set to preferred, requiring UV for every subsequent assertion.
- Safari ignores the credProtect extension entirely for security keys and does not prompt
  users for PIN setup when userVerification is set to preferred.
- Setting userVerification to required prevents Chrome's Level 3 escalation, producing
  credProtect Level 2 credentials that authenticate successfully across Chrome, Safari and
  Firefox.
- **Firefox 139**, released mid-2025, added credProtect support via the authenticator-rs
  library, adhering to explicit RP requests without applying Chrome's implicit escalation
  logic.

## 1. Introduction

You register a [YubiKey](https://www.corbado.com/glossary/yubikey) on a website in Chrome. Everything works. You
open Safari on the same machine, try to log in with the same key and it fails silently. No
useful error message, no prompt for a PIN - just nothing.

The root cause is **credProtect** - formally called **credentialProtectionPolicy** - a
[WebAuthn extension](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API/WebAuthn_extensions)
that Chrome automatically applies when creating discoverable credentials on security keys.
Chrome does this with good intention: as the
[Chromium source](https://source.chromium.org/chromium/chromium/src/+/main:content/browser/webauth/cred_protect.md)
explains, the goal is to ensure that "physical possession of a
[security key](https://www.corbado.com/glossary/security-key) does not allow sign-in to a site that doesn't
demand [user verification](https://www.corbado.com/blog/webauthn-user-verification)." This is a sensible default
that protects users from unauthorized access if their key is lost or stolen. The side
effect, however, is that other browsers like Safari cannot always negotiate the higher
requirements Chrome applied, causing authentication to fail silently.

This article explains what **credProtect** is, how Chrome, Safari and Firefox handle it
differently and what relying parties should do to avoid locking out
[security key](https://www.corbado.com/glossary/security-key) users across browsers.

## 2. When does this matter?

**This only affects you if you use `userVerification: "preferred"` instead of
`"required"`.** Many [large-scale](https://www.corbado.com/blog/introducing-passkeys-large-scale-overview)
deployments deliberately choose `"preferred"` because `UV=required` can cause hard
failures in edge cases: platform [authenticators](https://www.corbado.com/glossary/authenticator) where biometric
sensors are temporarily unavailable (e.g. macOS clamshell mode), sensor failures at scale
or environments where not all devices support
[user verification](https://www.corbado.com/blog/webauthn-user-verification). Using `"preferred"` reduces error
rates and avoids blocking users in production.

The trade-off: `"preferred"` is more forgiving for platform passkeys but triggers Chrome's
**credProtect** escalation logic for security keys, creating the cross-browser breakage
documented in this article.

If you always use `UV=required`, Chrome and every other browser will prompt for
PIN/biometric uniformly. In that case, credProtect Level 3 is harmless because user
verification is already enforced at the browser level and Chrome users a lower protection
level.

A real-world example: Google uses `userVerification: "preferred"` on accounts.google.com
for both passkey and [security key](https://www.corbado.com/glossary/security-key) registration. The
browser-specific behavior illustrates the problem well:

- **Chrome**: when a security key has no PIN set, Chrome still forces PIN creation because
  of the credProtect escalation described below.
  [User verification](https://www.corbado.com/blog/webauthn-user-verification) is delivered and the credential is
  fully functional as a passkey.
- **Safari**: when the same security key has no PIN set, Safari does NOT prompt the user
  to create one. `UV=preferred` means Safari accepts the ceremony without user
  verification. The credential is created but the UV flag is off.

Google handles this gracefully on the server side: when UV is not delivered, Google still
accepts the security key but marks it as **"This key can only be used with a password"** -
effectively downgrading it to a second-factor-only credential rather than a standalone
passkey. This means the user must still enter their password alongside the security key
tap.

![Google security key registered without UV showing "This key can only be used with a password"](https://www.corbado.com/website-assets/google_security_key_449f59a415.png)

This pattern shows how large relying parties handle the `preferred` + no-UV case: accept
the credential but reduce its trust level. Smaller RPs that do not have this fallback
logic may simply break or silently create credentials that fail in other browsers.

## 3. What is credProtect (credentialProtectionPolicy)?

The **credProtect** extension is defined in the
[CTAP 2.1 specification](https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html)
and allows the browser (or the [relying party](https://www.corbado.com/glossary/relying-party) via the WebAuthn
extensions input) to specify how the [authenticator](https://www.corbado.com/glossary/authenticator) should
protect a stored credential. It controls two things:

- Whether the credential can be **enumerated** (discovered) without user verification
- Whether the credential can be **used** via an explicit `allowCredentials` list without
  user verification

The extension defines three protection levels:

| Level | Policy name                                    | Value  | Discovery without UV | Use with allowCredentials without UV |
| ----- | ---------------------------------------------- | ------ | -------------------- | ------------------------------------ |
| 1     | `userVerificationOptional`                     | `0x01` | Allowed              | Allowed                              |
| 2     | `userVerificationOptionalWithCredentialIDList` | `0x02` | Blocked              | Allowed                              |
| 3     | `userVerificationRequired`                     | `0x03` | Blocked              | Blocked                              |

**Level 1** provides maximum backward compatibility. The
[authenticator](https://www.corbado.com/glossary/authenticator) responds to any request regardless of whether the
user provided a PIN or biometric.

**Level 2** introduces privacy protection for **discoverable credentials**. The
[authenticator](https://www.corbado.com/glossary/authenticator) hides the credential during usernameless
discovery flows (empty `allowCredentials`) unless user verification is performed. It still
responds when the [relying party](https://www.corbado.com/glossary/relying-party) provides the
[credential ID](https://www.corbado.com/blog/webauthn-user-id-userhandle) explicitly - supporting traditional
identifier-first authentication without a PIN.

**Level 3** is the strictest mode. The authenticator refuses to use the credential for any
purpose unless user verification succeeds. This effectively turns the security key into a
hardware-enforced multi-factor device.

## 4. How Chrome applies credProtect defaults

Chrome adds its own security defaults on top of the
[relying party](https://www.corbado.com/glossary/relying-party)'s request. When creating discoverable credentials
on security keys, the engine applies **credProtect** levels designed to protect users even
when the RP does not explicitly request protection.

The full
[Chromium credProtect documentation](https://source.chromium.org/chromium/chromium/src/+/main:content/browser/webauth/cred_protect.md)
describes this behavior:

> Chromium will request a protection level of userVerificationOptionalWithCredentialIDList
> when creating a credential if residentKey is set to preferred or required. This ensures
> that simple physical possession of a security key does not allow the presence of a
> [discoverable credential](https://www.corbado.com/blog/webauthn-resident-key-discoverable-credentials-passkeys)
> for a given RP ID to be queried.
>
> Additionally, if residentKey is required and
> [userVerification](https://www.corbado.com/glossary/user-verification) is preferred, the protection level will
> be increased to userVerificationRequired. This ensures that physical possession of a
> security key does not allow sign-in to a site that doesn't demand user verification.
>
> If an explicit credProtect level is requested by the site, that will override these
> defaults.

This means a relying party that sends the following configuration:

```javascript
const options = {
    publicKey: {
        authenticatorSelection: {
            residentKey: "required",
            userVerification: "preferred",
        },
        // ...
    },
};
```

...will cause Chrome to silently set `credProtect: 3` (`userVerificationRequired`) on the
CTAP `MakeCredential` command sent to the security key.

### 4.1 Chrome's implicit escalation matrix

The resulting **credProtect** level in Chrome depends on the combination of `residentKey`
and `userVerification`:

| `residentKey` | `userVerification` | Resulting credProtect level     |
| ------------- | ------------------ | ------------------------------- |
| `preferred`   | `discouraged`      | **Level 3**                     |
| `required`    | `discouraged`      | **Level 3**                     |
| `required`    | `preferred`        | **Level 3**                     |
| `required`    | `required`         | Level 2 (UV enforced by client) |

The counterintuitive row is `required` + `preferred` resulting in Level 3 while
`required` + `required` only gets Level 2. The rationale: when the RP explicitly requires
UV, Chrome trusts the client-side enforcement and does not need the authenticator to
double-enforce it. But when the RP only "prefers" UV, Chrome hardens the credential at the
hardware level as a safety net.

### 4.2 CTAP debug log evidence

The following is an excerpt from a real Chrome CTAP debug log during a registration on
webauthn.[io](https://www.corbado.com/blog/webauthn-errors) with a [YubiKey](https://www.corbado.com/glossary/yubikey) 5 series
(FIDO_2_1_PRE firmware, [AAGUID](https://www.corbado.com/glossary/aaguid)
`2FC0579F-8113-47EA-B116-BB5A8DB9202A`). The RP requested `residentKey: "required"` and
`userVerification: "preferred"`:

```
FIDODebug <- 0x1 (kAuthenticatorMakeCredential)
  {1: h'C43B...', 2: {"id": "webauthn.io", "name": "webauthn.io"},
   3: {"id": h'...', "name": "corbadotest10"},
   4: [{"alg": -8}, {"alg": -7}, {"alg": -257}],
   6: {"credProtect": 3},
   7: {"rk": true},
   9: 2}
```

Key observations:

- `6: {"credProtect": 3}` - Chrome injected Level 3 even though the RP never requested it
- `7: {"rk": true}` -
  [resident key](https://www.corbado.com/blog/webauthn-resident-key-discoverable-credentials-passkeys)
  (discoverable credential) is enabled
- `9: 2` - PIN protocol v2 was used for the PIN/UV negotiation
- The authenticator flags byte `0xC5` confirms UP=1, UV=1, AT=1 - user was verified via
  PIN

The credential was created with **Ed25519** (`alg: -8`) and stored with credProtect
Level 3. From this point forward, this credential will refuse to respond to any
[assertion](https://www.corbado.com/glossary/assertion) request that does not include successful user
verification.

## 5. Safari and Firefox: missing or limited credProtect support

### 5.1 Safari ignores credProtect for roaming authenticators

Safari's WebAuthn implementation is built on Apple's AuthenticationServices framework,
which is primarily designed for the [iCloud Keychain](https://www.corbado.com/glossary/icloud-keychain)
[platform authenticator](https://www.corbado.com/glossary/platform-authenticator). For platform credentials,
Apple's [Secure Enclave](https://www.corbado.com/glossary/secure-enclave) provides hardware-enforced protection
equivalent to Level 3 by design - Touch ID, [Face ID](https://www.corbado.com/faq/is-face-id-passkey) or the
system passcode is always required.

However, when an external security key is used, Safari does **not** send the credProtect
extension in the CTAP `MakeCredential` command. Even if the relying party explicitly
requests `credentialProtectionPolicy` in the extensions input, Safari ignores it.

Safari also handles **user verification** differently for security keys with
`UV=preferred`:

- `UV=discouraged` or `UV=preferred`: Safari does NOT prompt the user to create a PIN on a
  security key that has no PIN set. The ceremony completes with
  [User Presence](https://www.corbado.com/blog/webauthn-user-verification) only.
- `UV=required`: Safari prompts for PIN setup on the security key and will not proceed
  without it.

This means a credential created in Safari on a security key without a PIN:

- Has default protection for credProtect
- Has **UV=false** in the authenticator data
- Works everywhere (because no browser or authenticator will refuse a Level 1 credential)
- But provides minimal hardware-level security

### 5.2 Firefox: recent progress with credProtect

Firefox historically lacked support for the credProtect extension. Testing shows the
following extension support as reported by the browser:

| Browser           | `credProps` | `credentialProtectionPolicy` | `enforceCredentialProtectionPolicy` |
| ----------------- | ----------- | ---------------------------- | ----------------------------------- |
| Safari            | true        | true                         | true                                |
| Firefox (pre-139) | true        | false                        | false                               |
| Firefox (139+)    | true        | true                         | true                                |
| Chrome            | true        | true                         | true                                |

**Firefox 139** (released mid-2025) added credProtect support through the
`authenticator-rs` Rust library. Unlike Chrome, Firefox adheres more strictly to the
relying party's explicit request rather than applying implicit escalation. If the RP does
not request a `credentialProtectionPolicy`, Firefox is more likely to leave the credential
at Level 1 or Level 2 depending on the authenticator defaults.

Firefox's implementation was partly motivated by the Infineon ECDSA side-channel
[vulnerability](https://www.corbado.com/glossary/vulnerability)
([YSA-2024-03](https://www.yubico.com/support/security-advisories/ysa-2024-03/)) which
demonstrated that credProtect Level 2 or 3 credentials are significantly more resistant to
physical key-extraction attacks because the attacker cannot trigger signing operations
without first obtaining the user's PIN.

## 6. Interoperability failure: Chrome-to-Safari gap

The most common failure occurs when a security key registered in Chrome is subsequently
used in Safari. Here is the step-by-step breakdown:

### 6.1 Registration in Chrome

The relying party initiates a registration ceremony with `residentKey: "required"` and
`userVerification: "preferred"`. Chrome escalates to credProtect Level 3 and prompts the
user for their security key PIN. The credential is created and stored on the
[YubiKey](https://www.corbado.com/glossary/yubikey) with the internal policy flag set to
`userVerificationRequired`.

### 6.2 Authentication in Safari

The user opens Safari and attempts to log in. The relying party calls
`navigator.credentials.get()` with `userVerification: "preferred"` or even
`userVerification: "required"` . Safari translates this into a CTAP `GetAssertion`
command. Because the security key already has a PIN set (Chrome forced it during
registration), Safari should in theory negotiate UV. However, Safari's limited CTAP
extension handling may fail to properly complete the PIN/UV token handshake required by
the credential's Level 3 policy. The user sees a generic error or "No credentials found" -
with no indication of why.

### 6.3 credProtect 2 vs credProtect 3

From testing, the distinction is clear:

| Scenario                   | credProtect | Cross-browser result        |
| -------------------------- | ----------- | --------------------------- |
| Chrome with `UV=required`  | Level 2     | Works in Safari and Firefox |
| Chrome with `UV=preferred` | **Level 3** | **Fails in Safari**         |

When `UV=required` is set, Chrome assigns Level 2 (trusting client enforcement) and the
credential works across browsers because Level 2 allows [assertion](https://www.corbado.com/glossary/assertion)
with an explicit [credential ID](https://www.corbado.com/blog/webauthn-user-id-userhandle) without UV. When
`UV=preferred` is set, Chrome assigns Level 3 and the credential requires UV for every
operation - breaking in Safari.

## 7. The credentialProtectionPolicy extension

The **credProtect** extension is usable only during registration (`create()`) and is
processed directly by the authenticator - the
[user agent](https://www.corbado.com/blog/client-hints-user-agent-chrome-safari-firefox) passes the input through
to the security key. Relying parties can explicitly control the credProtect level by
including the extension in the `navigator.credentials.create()` call:

```javascript
const options = {
    publicKey: {
        authenticatorSelection: {
            residentKey: "required",
            userVerification: "preferred",
        },
        extensions: {
            credentialProtectionPolicy: "userVerificationOptionalWithCredentialIDList",
            enforceCredentialProtectionPolicy: true,
        },
        // ...
    },
};
```

### 7.1 Extension inputs

| Extension input                     | Type    | Description                                                                                                                                                                                                         |
| ----------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `credentialProtectionPolicy`        | string  | One of `userVerificationOptional` (experimental), `userVerificationOptionalWithCredentialIDList` or `userVerificationRequired`. Maps to CTAP values `0x01`, `0x02` and `0x03` respectively                          |
| `enforceCredentialProtectionPolicy` | boolean | If `true`, the `create()` call fails when the policy cannot be adhered to. If `false`, the system makes a best attempt to conform to the policy but still creates a credential if the exact level is not achievable |

### 7.2 Caveats

- Setting the policy to `userVerificationOptional` (Level 1) while requesting a
  **discoverable credential** may cause Chrome to override the request and still apply
  Level 2 to protect [user privacy](https://www.corbado.com/faq/ensure-gdpr-compliance-with-passkeys).
- Setting `enforceCredentialProtectionPolicy: true` with
  `userVerificationOptionalWithCredentialIDList` on an authenticator that does not support
  credProtect (older CTAP 2.0 firmware) will cause the registration to fail entirely.
- Safari ignores these extension inputs for roaming
  [authenticators](https://www.corbado.com/glossary/authenticator) as of early 2026.

### 7.3 Browser support for extension inputs

| Browser        | Sends `credentialProtectionPolicy` to authenticator |
| -------------- | --------------------------------------------------- |
| Chrome         | Yes                                                 |
| Firefox 139+   | Yes                                                 |
| Safari         | No                                                  |
| Edge (Windows) | Yes                                                 |

## 8. Recommendations for Relying Parties

There are two primary approaches depending on your deployment constraints:

### 8.1 Option 1: use `userVerification: "required"` for cross-platform flows

If your authentication flow might involve security keys (i.e. you do not filter with
`authenticatorAttachment: "platform"`), set `userVerification: "required"`. This is the
simplest and most reliable approach. It avoids Chrome's escalation to Level 3, ensures
consistent PIN prompts across all browsers and produces Level 2 credentials that work
everywhere.

```javascript
const options = {
    publicKey: {
        authenticatorSelection: {
            residentKey: "required",
            userVerification: "required",
        },
        // ...
    },
};
```

### 8.2 Option 2: keep `UV=preferred` and detect the UV gap server-side

If you cannot use `UV=required` because of error rate concerns in
[large-scale](https://www.corbado.com/blog/introducing-passkeys-large-scale-overview) deployments, you can keep
`userVerification: "preferred"` and handle the consequences on the server.

This is what Google does on accounts.google.com. Google uses `UV=preferred` for both
passkey and security key registration. When a security key is registered in Safari without
a PIN (Safari does not prompt for PIN creation with `UV=preferred`), the UV flag comes
back as `false`. Google's server detects this and still accepts the credential but marks
it as **"This key can only be used with a password"** - downgrading it to a
second-factor-only credential that requires a password alongside it.

To implement this approach:

- After registration, check the **UV flag** in the authenticator data. If UV is `false`,
  store the credential with a reduced trust level.
- Use `getClientExtensionResults()` to check whether credProtect was applied and at which
  level. If the extension was ignored (as in Safari), flag the credential accordingly.
- At login time, require an additional factor (e.g. password) for credentials that were
  created without user verification.

This gives you the error-rate benefits of `UV=preferred` for platform passkeys while
gracefully handling the security key edge case.

## 9. Conclusion

**credProtect** is a necessary security feature that prevents unauthorized credential
enumeration on security keys. The problem is not the extension itself but the inconsistent
way browsers handle it:

- Chrome silently escalates to Level 3 when `residentKey: "required"` is combined with
  `userVerification: "preferred"`, creating credentials that require UV for every
  operation
- Safari does not send the extension at all and does not prompt for PIN creation with
  `UV=preferred`
- Firefox 139+ brought credProtect support closer to parity with Chrome but with less
  implicit escalation

For relying parties, the safest cross-browser approach is `userVerification: "required"`
for any flow that may involve security keys. If that is not feasible, identifier-first
flows and server-side UV detection provide workable alternatives.

As Safari evolves to support more [WebAuthn Level 3](https://www.corbado.com/blog/passkeys-prf-webauthn)
extensions and CTAP 2.1 features, the current interoperability friction will likely
diminish. Until then, understanding Chrome's implicit credProtect escalation is essential
for any deployment that supports [FIDO2](https://www.corbado.com/glossary/fido2)
[hardware security keys](https://www.corbado.com/blog/best-fido2-hardware-security-keys) across browsers.

## Frequently Asked Questions

### Why does my security key work in Chrome but fail silently in Safari after registration?

Chrome silently escalates the credProtect policy to Level 3 when a discoverable credential
is registered with residentKey: required and userVerification: preferred. Level 3 requires
user verification for every assertion, but Safari's limited CTAP extension handling cannot
complete the PIN/UV token handshake required by that policy. The result is a silent
failure or a generic 'No credentials found' error with no indication of the root cause.

### Can I explicitly override Chrome's credProtect escalation in my WebAuthn registration options?

Yes. You can include credentialProtectionPolicy:
userVerificationOptionalWithCredentialIDList and enforceCredentialProtectionPolicy: true
in the extensions input of navigator.credentials.create(). Chrome documentation states
that an explicit credProtect level from the site overrides Chrome's implicit defaults,
though Safari still ignores these extension inputs for roaming authenticators as of
early 2026.

### What is the difference between credProtect Level 2 and Level 3 for FIDO2 security keys?

credProtect Level 2 (userVerificationOptionalWithCredentialIDList) blocks credential
discovery without user verification but still allows assertion when the relying party
provides an explicit credential ID, making it broadly compatible across browsers. Level 3
(userVerificationRequired) blocks all use of the credential unless user verification
succeeds at the authenticator level. Safari's incomplete CTAP handling cannot satisfy
Level 3 requirements, causing silent authentication failures for credentials registered in
Chrome with userVerification: preferred.

### Why did Firefox add credProtect support in version 139?

Firefox 139's credProtect implementation was partly motivated by the Infineon ECDSA
side-channel vulnerability (YSA-2024-03), which demonstrated that Level 2 or Level 3
credentials are significantly more resistant to physical key-extraction attacks. Without
credProtect, an attacker with physical access to a security key could trigger signing
operations to exploit the vulnerability without needing the user's PIN. Higher credProtect
levels prevent this by requiring PIN verification before any signing operation can be
performed.

### How should I handle security key credentials that were registered without user verification in Safari?

Follow Google's approach on accounts.google.com: after registration, check the UV flag in
the authenticator data, and if UV is false, store the credential with a reduced trust
level in your database. At login time, require an additional factor such as a password for
credentials flagged as created without user verification. This downgrades the credential
to second-factor-only status rather than breaking the user's authentication flow entirely.
