I am U-Zyn Chua. I build, research and write about technology, AI and the open web.
Posted on :: Tags: , , , , ,

TL;DR: Passkey has a clever design that allows users to know if your keys are silently stolen and cloned, but the feature is currently not very useful today because its major providers such as Apple, Google and Microsoft do not play ball.

Passkey is all you need

Passkey adoption is accelerating. At this stage, it is generally considered ubiquitous enough to be implementing it for web services and apps and dropping traditional insecure password authentication mechanics.

Passkey is not just a second-factor authentication (2FA). It is secure and widely-adopted enough that it should be the only authentication mechanism. If you do not believe me, believe Microsoft. Microsoft has, from May 2025 onwards, adopt passkey for passwordless authentication for its accounts. Well done, Microsoft!

Passkey is great UX with no security penalty

It is commonly believed that security and usability have an inverse relationship: enhanced security often comes at the cost of user convenience and vice versa. Passkeys, however, disrupt this assumption entirely. They offer robust security while simultaneously providing an exceptional user experience, not merely matching the traditional email-password model but significantly surpassing it. With passkeys, both registration and authentication become seamless single-step processes, dramatically improving both user convenience and security.

WebAuthn, short for Web Authentication, is an open standard developed by the World Wide Web Consortium (W3C) and the FIDO Alliance to facilitate secure and passwordless authentication on the web. In short, Passkeys are a consumer-friendly implementation of WebAuthn, enabling seamless logins through biometrics or PINs without traditional passwords, and adhering to a standard set of protocols.

At its core, the system uses public-key cryptography, wherein each user holds a unique private key securely stored on their device. The corresponding public key is registered with the website or service provider during initial registration.

During authentication or log in, the device or authentication generates a cryptographic signature using the private key, which the relying party (RP), also commonly known simply as server, verifies using the public key, confirming the user's identity without ever sharing sensitive data.

Theft detection

One of the rarely documented features of Passkey is that it has a very clever feature to detect clone or theft.

You have your security credentials and you think that they are safely stored in your passwords vault, encrypted and kept away from prying eyes and hands. But how would you ever know that your passwords were never peeked and copied or your security vault was never cloned and broken into? Even if you have your printed, handwritten or embossed private keys or passwords stored securely in a physical vault, and you routinely check to ensure that they are physically in a vault, you cannot be certain that they are not cloned, for instance by taking a picture and routinely logs in to your account to read your mails.

Passkey has a very clever solution to that problem, if only everyone would follow the defined specifications.

Brilliance of signCount

An essential yet often overlooked feature of WebAuthn is the signature counter [W3C spec], or signCount. Every time a device successfully signs or authenticates, it independently increments this counter and sends it alongside the authentication request. Servers compare the received signCount against the previously recorded value:

// Server-side verification of signCount
function verifySignCount(authenticatorData, storedSignCount) {
  const currentSignCount = authenticatorData.signCount;
  if (currentSignCount <= storedSignCount) {
    throw new Error("Potential cloned or compromised device detected.");
  }
  
  // Update stored signCount
  storedSignCount = currentSignCount;
}

This mechanism effectively detects cloned or compromised authenticators, as a cloned authenticator would inevitably provide an outdated or duplicated signCount value, raising immediate security flags.

Current state

Despite the elegance and effectiveness of signCount, major passkey providers, Apple, Google, and Microsoft, currently implement it inadequately for synced credentials. These providers always return a signCount value of 0 for passkeys synced across multiple devices, rendering the clone detection feature ineffective.

Apple, unofficially, blames it on synchronization challenges across multiple devices that synced passkeys cannot support signCount.

Is synchronization really the problem?

Providers claim synchronization complexity as the barrier to implementing signCount. This seems like a lazy excuse that does not hold much water. Providers like Apple's iCloud and Google's Password Manager routinely sync sensitive data such as passwords within mere seconds. The incremental synchronization of a simple integer counter, even with eventual consistency, is technically straightforward.

Eventual consistency syncing is acceptable

Eventual consistency syncing of signCount is actually acceptable, definitely better than not providing them at all as today. Eventual consistency syncing still retains the ability for key cloning detection. All you have to do is introduce some allowance before you flag a key as compromised. Here is how that would look like on the server-side:

// Server-side verification of eventual-consistent signCount
const allowance = 3;

function verifySignCountWithAllowance(authenticatorData, storedSignCount) {
  const currentSignCount = authenticatorData.signCount;
  if (currentSignCount <= storedSignCount) {
    if (storedSignCount - currentSignCount > allowance) {
        throw new Error("Potential cloned or compromised device detected.");
    }
  }
  
  // IMPORTANT: Save the larger of the known and received signCount
  storedSignCount = Math.max(currentSignCount, storedSignCount);
}

Security gotcha alert!

As signCount is not widely implemented today, most relying party either not save or validate signCount, which is a pity for such an elegant feature, or implement selectively only for authenticators that provide non-zero signCount.

This is generally fine, but be careful about the following 2 points:

  1. You must not be relying on passed-in signCount to decide on whether to perform validity check, instead to rely on both stored signCount and newly passed-in value. If any of these 2 are not 0, perform signCount validation.

    • Using passed-in signCount would allow authenticator to pass in 0 to bypass validation.
  2. At the final step when saving the updated signCount in database, make sure you store the larger of existing stored value and the newly passed-in value.

    • Simply storing the newly passed-in signCount would allow cloned device to gradually, within allowance, to bring down the stored signCount, even all the way to 0.

Passcay - Zig passkey library

I learned about these as I was implementing Passcay, a free and open source Zig passkey library. Been wanting to write the blog for awhile but have been very very occupied working on my upcoming projects, which I will be sharing soon.

Passcay has all the above built-in, allowing a relying party to implement Passkey registration and authentication safely, securely and easily.

The various checks that are included in Passcay can be summarized here:

// Set up the authentication expectations
// It is important to verify the challenge and origin to prevent replay attacks.
const auth_expectations = passcay.auth.AuthVerifyExpectations{
    .public_key = user_credential.public_key,
    .challenge = challenge_from_session,       // The challenge generated in Step 1
    .origin = "https://yourdomain.com",        // Origin of your web app, or null to skip origin check
    .rp_id = "yourdomain.com",                 // RP ID for your domain, or null to skip RP ID check
    .require_user_verification = true,         // Whether user verification is required
    .require_user_presence = true,             // Whether user presence is required
    .enable_sign_count_check = true,           // Enable sign count checking if applicable
    .known_sign_count = user_credential.sign_count,  // Current sign count from database
    .sign_count_allowance = 1,                 // Allow some deviation in sign count for eventual consistency
};

You can learn more about it at the registration and login documentations.

Now's the time for passwordless

If you are still unsure if it is safe to go fully passwordless today, yes it is, in all fronts: security, user experience, and adoption rate.

In fact, you should also consider decoupling from email address as account identifier anti-pattern. More about that in the future!

Read more