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:
-
You must not be relying on passed-in
signCount
to decide on whether to perform validity check, instead to rely on both storedsignCount
and newly passed-in value. If any of these 2 are not0
, performsignCount
validation.- Using passed-in
signCount
would allow authenticator to pass in0
to bypass validation.
- Using passed-in
-
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 to0
.
- Simply storing the newly passed-in
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!