Jasper Barcelona
Jasper Barcelona

Reputation: 21

Leftover bytes detected while parsing authenticator data

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 keyPair = await crypto.subtle.generateKey(
            {
                name: "ECDSA",
                namedCurve: "P-256",
            },
            true,
            ["sign", "verify"]
        );

        const publicKey = new Uint8Array(
            await crypto.subtle.exportKey("raw", keyPair.publicKey)
        );

        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

Upvotes: 2

Views: 82

Answers (2)

Asthor
Asthor

Reputation: 979

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

Update:

Crypto-js doesn't let you create CBOR encoded public keys, you'd need to find something to do that for you. However the CBOR object will have most of the data identical when you generate it, with only the x and y coordinates different.

So generating the following string

const publicKey = "A5010203262001215820" + xCoordinatesAsHex + "225820" yCoordinatesAsHex

How you generate the hex is a bit up to you but if you have fetch the key through exportKey, it is probably easiest to work with through jwk

An example of a valid public key would for example be

const publicKey = "A501020326200121582065eda5a12577c2bae829437fe338701a10aaa375e1bb5b5de108de439c08551d2258201e52ed75701163f7f9e40ddf9f341b3dc9ba860af7e0ca7ca7e9eecd0084d19c"

And you could then use this directly into your code and the validation should work.

Second update:

Given your publicKey you could convert the value in it to hex with

publicKey.map(x => x.toString(16).padStart(2,'0')).join('')

This would return the full value as a single string. The first 2 characters would be the compression point and can be discared. The X would be the next 64 characters of the string and the Y would be the final 64 characters of the string.

Upvotes: 0

Ki-Eun Shin
Ki-Eun Shin

Reputation: 46

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.

Upvotes: 0

Related Questions