skip to Main Content

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


  1. 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.

    Login or Signup to reply.
  2. The expectation is that the public key in the response is in COSE format. But as far as I can see you are doing

    const publicKey = generateRandomBuffer(256); // 256-byte public key
    

    To fix this you probably just need to create a proper object that then gets encoded.

    {
      1:   2,  //EC2 key type
      3:  -7,  //ES256 --- -8 is EdDSA
     -1:   1,  //P-256 curve
     -2:   generateRandomBuffer(32),  //x-coordinate
     -3:   generateRandomBuffer(32)   //y-coordinate
    }
    

    You probably have to encode this first into CBOR before adding it to your AttestedCredentialData.

    See Attested Credential Data for more info

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search