I’m writing a password manager browser extension that also supports passkeys. The extension injects a script to websites that intercepts navigator.credentials.create
requests which then opens a popup that waits for the user to authenticate (if not already logged in) and click the Create Passkey button. I’m currently having trouble generating/encoding the attestationObject.
Auth data generation:
function generateAuthData(
rpIdHash: Uint8Array,
attestedCredentialData: Uint8Array
): Uint8Array {
const flags = 0x41; // User presence (UP) + Attested Credential Data (AT) flag
const counter = new Uint8Array([0, 0, 0, 1]); // Counter = 1
// Concatenate RP ID Hash (32 bytes), Flags (1 byte), Counter (4 bytes), and Attested Credential Data
return new Uint8Array([
...rpIdHash,
flags,
...counter,
...attestedCredentialData,
]);
}
Attestation object creation:
function createAttestationObject(
authData: Uint8Array,
credentialId: Uint8Array,
publicKey: Uint8Array
): Buffer {
const aaguid = new Uint8Array(16); // Typically zero-filled if unavailable
const attestedCredentialData = new Uint8Array(
16 + credentialId.length + publicKey.length
);
attestedCredentialData.set(aaguid, 0);
attestedCredentialData.set(credentialId, 16);
attestedCredentialData.set(publicKey, 16 + credentialId.length);
const attestationObject = {
authData,
fmt: "none",
attStmt: {
alg: -8, // ES256
x5c: [],
},
};
// Encode using CBOR
return encode(attestationObject);
}
Credentials generation:
const createCustomCredential = (
options: PublicKeyCredentialCreationOptions
): CustomCredential => {
if (!options || !options.publicKey) {
throw new Error("Invalid options: PublicKey options are required.");
}
const { rp, challenge } = options.publicKey;
// Simulate RP ID hash (SHA-256 of RP ID)
const rpIdHash = new Uint8Array(
CryptoJS.SHA256(rp.id ?? "").words.flatMap((word) => [
(word >> 24) & 0xff,
(word >> 16) & 0xff,
(word >> 8) & 0xff,
word & 0xff,
])
);
// Simulate credential ID and public key
const credentialId = generateRandomBuffer(32); // 32-byte random ID
const publicKey = generateRandomBuffer(256); // 256-byte public key
const aaguid = new Uint8Array(16); // Zero-filled AAGUID
const credentialIdLength = new Uint8Array([
(credentialId.length >> 8) & 0xff,
credentialId.length & 0xff,
]);
const attestedCredentialData = new Uint8Array(
aaguid.length +
credentialIdLength.length +
credentialId.length +
publicKey.length
);
attestedCredentialData.set(aaguid, 0);
attestedCredentialData.set(credentialIdLength, aaguid.length);
attestedCredentialData.set(
credentialId,
aaguid.length + credentialIdLength.length
);
attestedCredentialData.set(
publicKey,
aaguid.length + credentialIdLength.length + credentialId.length
);
// Generate authenticator data
const authData = generateAuthData(rpIdHash, attestedCredentialData);
// Create attestation object
const attestationObject = createAttestationObject(
authData,
credentialId,
publicKey
);
return {
id: btoa(String.fromCharCode(...credentialId))
.replace(/+/g, "-")
.replace(///g, "_")
.replace(/=+$/, ""),
rawId: credentialId,
response: {
clientDataJSON: btoa(
JSON.stringify({
challenge: btoa(String.fromCharCode(...new Uint8Array(challenge)))
.replace(/+/g, "-")
.replace(///g, "_")
.replace(/=+$/, ""),
origin: `https://${options.publicKey.rp.id}`,
type: "webauthn.create",
})
)
.replace(/+/g, "-")
.replace(///g, "_")
.replace(/=+$/, ""),
attestationObject: attestationObject,
},
type: "public-key",
};
};
And this is how I resolve the window.credentials.create
request by sending back the generated credentials to the injected script:
const generateKey = async () => {
setLoading(true);
if (parsedData) {
const credential = createCustomCredential(parsedData);
if (tabId) {
browser.tabs.sendMessage(parseInt(tabId), {
type: "PASSKEY_RESULT",
success: true,
data: credential,
});
}
}
setLoading(false);
window.close();
};
I’m testing this via webauthn.io and I’m getting this error: Registration failed: Leftover bytes detected while parsing authenticator data
2
Answers
When handling credential creation on the backend, there are couple of validation steps whether the given created credential is valid or not. When parsing the data on the backend for your data, the data should not have left over bytes which mean that returned data somehow malformed.
The reason why you get such error seems that you just assign random bytes to the public key. Note that the public key should be COSE encoded. So, your random bytes does not conform to the spec and it may throw an unexpected errors on the RP side.
The expectation is that the public key in the response is in COSE format. But as far as I can see you are doing
To fix this you probably just need to create a proper object that then gets encoded.
You probably have to encode this first into CBOR before adding it to your
AttestedCredentialData
.See Attested Credential Data for more info