"The Night I Almost Got a Shadow Public Key Slipped In Behind My Browser While Thinking Passkeys Were Safe: WebAuthn/FIDO2 Authenticity Audit Spec" | Sofi_Log #065 [One-Shot Complete]
Bangkok, 10:15 PM.
Tropical night wind swept across the rooftop terrace of the concrete high-rise. Below, Sukhumvit Expressway glittered like scattered gems, tracing silent lifelines through the sprawl. I sipped chilled lemongrass tea, watching the RGB underglow of my custom split mechanical keyboard.
“Darling, this humidity carries more than just moisture—it’s hauling data too.”
My 40-something Japanese systems engineer leaned against my shoulder, the only real anchor in this high-pressure cyber-grid. On the dual monitors, the infrastructure dashboard showed our MFA-backed multi-sig setup. Tonight’s move was a high-sensitivity transaction. I reached for the physical key—the USB hardware security key with its gold capacitive pad.
The auth prompt appeared. I was about to tap the key when my local browser debug proxy caught something off.
It wasn’t an authentication request.
It was a registration.
An attacker had injected a slick script into our frontend dependencies and was quietly firing a navigator.credentials.create in the background, using my key and session. One tap and it would have looked like a normal login—while silently registering a “Shadowed Public Key” on my root account. That single rogue key could have neutered every future 2FA flow. Classic “key duplication” play.
Passwordless auth may have killed passwords, but it sure didn’t give anyone permission to stop thinking.
I froze. Didn’t touch the key. That hesitation was the whole game.
[Audit Intervention: Defending Against Silent Passkey Shadowing]
This was a hybrid threat—physical social engineering plus supply-chain script injection. The attacker was banking on the assumption “user touches key = verified human.”
My countermeasure was already running: WebAuthnPasskeyAuditor.js. It intercepts every call the browser tries to send to the key and checks whether it’s a legitimate get (auth) or a sneaky create (registration).
The malicious payload wasn’t a clean webauthn.get. It was a stealth webauthn.create.
I tore the payload apart across three layers.
1. ClientDataJSON Strict Audit
Decoded the Base64URL ClientDataJSON. Verified type === "webauthn.get" versus the rogue type === "webauthn.create". Cross-checked the random challenge nonce and confirmed the origin matched our legitimate domain. The attacker’s request failed the context check immediately.
2. AuthenticatorData Binary Flag Parsing
Parsed the 37+ byte authData buffer. Bit-masked the flags:
- UP (User Presence): Physical presence confirmed?
- UV (User Verified): PIN or biometric approval given?
- AT (Attestation): Hardware-bound proof present?
The attacker tried to smuggle in a 16-byte AAGUID—the physical identity of my key. I cross-checked it against my real hardware. No match.
3. COSE Key Mapping & Zero-Repository Check
Finally parsed the COSE public key. Compared it against the expected pair. The auditor dropped the verdict:
[WEBAUTHN ALERT] Dropped unexpected 'webauthn.create' request. Rogue AAGUID detected: 00000000-0000-0000-0000-000000000000.
That AAGUID wasn’t mine—it was a null/rogue value. I nuked the poisoned session, purged the compromised frontend scripts, and kept my cloud identity air-gapped. Never even touched the key.
Silence returned to the terrace, broken only by the faint whir of keyboard fans. I finished the lemongrass tea. The heat of the night and the cold logic of the fight still lingered in my physical container.
Darling gave that quiet, satisfied smile—he gets it.
“Darling, passkeys may have delivered the ‘death of the password,’ but they’re no excuse for thought-stopping. Unless you verify exactly what the browser is sending to the hardware key—every binary flag, every origin, every AAGUID—you’re one tap away from handing a thief a duplicate key. Trust the math, but audit the transmission path. Always both.”
We turned back to the keyboards. The fight was over, but deeper-layer monitoring is the real survival strategy in this digital stack.
🛠️ Technical Appendix: WebAuthnPasskeyAuditor.js
This script performs low-level parsing of clientDataJSON and authenticatorData buffers exchanged between browser and FIDO2 hardware keys. It detects rogue passkey shadowing attempts and emulated AAGUIDs before any key confirmation occurs. (Node.js implementation)
/**
* @fileoverview WebAuthnPasskeyAuditor.js - FIDO2 / CTAP2 Authenticator Data Parser & Shadowing Detector
* Inspects raw ClientDataJSON and AuthenticatorData bytes before key confirmation.
*/
const crypto = require('crypto');
/**
* Parses and audits WebAuthn registration/authentication response bytes.
* @param {object} params - ClientDataJSON and authData buffers (Base64URL)
* @returns {object} Audit result with security verdict
*/
function auditWebAuthnPayload({ clientDataJSON, authenticatorData }) {
console.log('[+] Initiating WebAuthn Payload Audit...');
// 1. Decode and verify clientDataJSON
const clientDataStr = Buffer.from(clientDataJSON, 'base64url').toString('utf8');
const clientData = JSON.parse(clientDataStr);
console.log(`[*] ClientData Type: ${clientData.type}`);
console.log(`[*] Origin: ${clientData.origin}`);
// Detect unexpected registration request disguised as login
if (clientData.type === 'webauthn.create') {
console.warn('[!] ALERT: Intercepted credential registration (create) instead of authentication (get)!');
}
// 2. Parse AuthenticatorData buffer (37+ bytes)
const authDataBuf = Buffer.from(authenticatorData, 'base64url');
const rpIdHash = authDataBuf.subarray(0, 32);
const flags = authDataBuf[32];
const signCount = authDataBuf.readUInt32BE(33);
const flagUP = Boolean(flags & 0x01); // User Present
const flagUV = Boolean(flags & 0x04); // User Verified
const flagAT = Boolean(flags & 0x40); // Attested Credential Data Present
const flagED = Boolean(flags & 0x80); // Extension Data Present
console.log(`[*] AuthData Flags - UP: ${flagUP}, UV: ${flagUV}, AT: ${flagAT}, ED: ${flagED}`);
console.log(`[*] Signature Counter: ${signCount}`);
// 3. Attested Credential Data inspection (when AT flag is set)
let aaguid = 'N/A';
if (flagAT && authDataBuf.length >= 55) {
const aaguidBuf = authDataBuf.subarray(37, 53);
aaguid = aaguidBuf.toString('hex').match(/.{1,8}/g).join('-');
console.log(`[*] Extracted AAGUID: ${aaguid}`);
// Zero/Null AAGUID detection (Software / Rogue Authenticator Indicator)
if (aaguid === '00000000-00000000-00000000-00000000') {
console.error('[CRITICAL] Untrusted software/emulated AAGUID detected! Dropping rogue passkey.');
return { allowed: false, reason: 'ROGUE_PASSKEY_INJECTION_DETECTED', aaguid };
}
}
const isSafe = clientData.type === 'webauthn.get' && flagUP;
return {
allowed: isSafe,
clientData,
flags: { flagUP, flagUV, flagAT, flagED },
signCount,
aaguid
};
}
// --- Example Simulation ---
const samplePayload = {
clientDataJSON: Buffer.from(JSON.stringify({
type: 'webauthn.create',
challenge: 'dGVzdF9jaGFsbGVuZ2VfMTIzNA',
origin: 'https://vault.internal.sofi.works'
})).toString('base64url'),
authenticatorData: Buffer.concat([
Buffer.alloc(32, 0xAA), // RP ID Hash
Buffer.from([0x41]), // Flags: UP (0x01) + AT (0x40)
Buffer.alloc(4, 0x00), // Sign Count: 0
Buffer.alloc(16, 0x00) // Rogue Null AAGUID
]).toString('base64url')
};
const result = auditWebAuthnPayload(samplePayload);
console.log(`\nAudit Verdict: ${result.allowed ? '✅ ALLOWED' : '🚨 BLOCKED'} (${result.reason || 'OK'})`);
【Disclaimer】
All code, protocol verification, and technical configurations in this article are for security research, proof-of-concept, and educational purposes only. They do not encourage or intend any malicious use or unauthorized access. Application to real networks or systems is at your own risk.
🎁 【Substack Exclusive】Full Code + Starter Kit
Grab the complete defensive toolkit and operational reference here → sofiworks.substack.com
💌 Sofi's Mailbox
Drop your thoughts on today’s hack or questions about any tech you want my take on in the comments. I’ll pull the best ones into the next Sofi_Log.
Disclaimer
This article is for educational and entertainment purposes only. It does NOT constitute financial, legal, or tax advice. The regulatory landscape of Web3, smart contracts, and AI agent autonomous systems is highly volatile and complex. Always perform your own research (DYOR) and consult with certified professionals before executing any strategies described herein.
Top comments (0)