@didtools/key-webauthn
Version:
Implements support to authenticate, authorize and verify blocks produced by webauthn/passkey compatible hardware authenticators and OS/software implementations.
173 lines (172 loc) • 6.85 kB
JavaScript
import { decode, decodeFirst } from 'cborg';
import { p256 } from '@noble/curves/p256';
import * as u8a from 'uint8arrays';
import varint from 'varint';
export async function authenticatorSign(challenge, credentialId) {
const allowCredentials = [];
if (credentialId) {
if (typeof credentialId === 'string') credentialId = u8a.fromString(credentialId, 'base64url');
allowCredentials.push({
type: 'public-key',
id: credentialId
});
}
const credential = await globalThis.navigator.credentials.get({
publicKey: {
rpId: globalThis.location.hostname,
challenge,
allowCredentials,
timeout: 240000,
// @ts-ignore
attestation: 'direct' // https://developer.mozilla.org/en-US/docs/Web/API/CredentialsContainer/get#publickey_object_structure
}
});
if (!credential) throw new Error('AbortedByUser');
const { response } = credential;
const { clientDataJSON, signature } = response;
const authenticatorData = getAuthenticatorData(response);
const recovered = recoverPublicKeys(signature, authenticatorData, clientDataJSON);
return {
signature,
recovered,
credential
};
}
export function verify(signature, publicKey, authData, clientDataJSON) {
const clientDataHash = p256.CURVE.hash(clientDataJSON);
const msg = u8a.concat([
authData,
clientDataHash
]);
const hashBase = p256.CURVE.hash(msg);
return p256.verify(signature, hashBase, publicKey);
}
// --- tools.js
export function randomBytes(n) {
return p256.CURVE.randomBytes(n);
}
export function decodeAttestationObject(attestationObject) {
return decode(assertU8(attestationObject));
}
/**
* Extracts PublicKey from AuthenticatorData as received from hardware key.
*
* See box `CREDENTIAL PUBLIC KEY` in picture:
* https://w3c.github.io/webauthn/images/fido-attestation-structures.svg
* @param {Uint8Array|ArrayBuffer} authData Use getAuthenticatorData(response).
*/ export function decodeAuthenticatorData(authData) {
authData = assertU8(authData);
// https://w3c.github.io/webauthn/#sctn-authenticator-data
if (authData.length < 37) throw new Error('AuthenticatorDataTooShort');
let o = 0;
const rpidHash = authData.slice(o, o += 32) // SHA-256 hash of rp.id
;
const flags = authData[o++];
// console.debug(`Flags: 0b` + flags.toString(2).padStart(8, '0'))
if (!(flags & 1 << 6)) throw new Error('AuthenticatorData has no Key');
const view = new DataView(authData.buffer);
const signCounter = view.getUint32(o);
o += 4;
// https://w3c.github.io/webauthn/#sctn-attested-credential-data
const aaguid = authData.slice(o, o += 16);
const clen = view.getUint16(o);
o += 2;
const credentialId = authData.slice(o, o += clen);
// https://datatracker.ietf.org/doc/html/rfc9052#section-7
const [cose, _] = decodeFirst(authData.slice(o), {
useMaps: true
});
// KTY = 1
if (cose.get(1) !== 2) throw new Error('Expected COSE key-type to be a EC Coordinate pair');
// ALG = 3
if (cose.get(3) !== -7) throw new Error('Expected ES256 Algorithm');
// X = -2, Y = -3
const [x, y] = [
cose.get(-2),
cose.get(-3)
];
if (!(x instanceof Uint8Array) || !(y instanceof Uint8Array)) throw new Error('Expected X and Y coordinate to be buffers');
// Compress publicKey
const publicKey = new Uint8Array(x.length + 1);
publicKey[0] = 2 + (y[y.length - 1] & 1);
publicKey.set(x, 1);
return {
rpidHash,
flags,
signCounter,
aaguid,
credentialId,
publicKey,
cose
};
}
/**
* Normalize authenticatorData across browsers/runtimes.
*/ export function getAuthenticatorData(response) {
// Only on Chrome
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
if (typeof response.getAuthenticatorData === 'function') return assertU8(response.getAuthenticatorData());
// Sometimes missing in Mozilla
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (response.authenticatorData) return assertU8(response.authenticatorData);
// Worst case scenario, decode attestationObject
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (response.attestationObject) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const { authData } = decode(assertU8(response.attestationObject));
return assertU8(authData);
}
throw new Error('Failed to recover authenticator data from credential response') // Give up
;
}
/**
* Normalize ArrayBuffer|Uint8Array|node:Buffer => Uint8Array or throw
*/ export function assertU8(o) {
if (o instanceof ArrayBuffer) return new Uint8Array(o);
if (o instanceof Uint8Array) return o;
// node:Buffer to Uint8Array
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (!(o instanceof Uint8Array) && o?.buffer) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-argument
return new Uint8Array(o.buffer, o.byteOffset, o.byteLength);
}
throw new Error('Expected Uint8Array');
}
/**
* Recovers both recovery bit 0|1 candidates from
* an authenticator produced signature.
* @param signature Authenticator generated signature
* @param authenticatorData Authenticator Data
* @param clientDataJSON Authenticator generated clientDataJSON - watch out for https://goo.gl/yabPex
* @returns Recovered tuple of pk0 and pk1
*/ export function recoverPublicKeys(signature, authenticatorData, clientDataJSON) {
// normalize to u8
signature = assertU8(signature);
authenticatorData = assertU8(authenticatorData);
clientDataJSON = assertU8(clientDataJSON);
const hash = (b)=>p256.CURVE.hash(b);
const msg = u8a.concat([
authenticatorData,
hash(clientDataJSON)
]);
const msgHash = hash(msg);
return [
0,
1
].map((rBit)=>p256.Signature.fromDER(signature).addRecoveryBit(rBit).recoverPublicKey(msgHash).toRawBytes(true));
}
export function decodePubFromDID(did) {
const multicodecPubKey = u8a.fromString(did.replace('did:key:z', ''), 'base58btc');
const keyType = varint.decode(multicodecPubKey);
if (keyType !== 0x1200) throw new Error('Expected p256 public key');
return multicodecPubKey.slice(varint.decode.bytes);
}
export function encodeDIDFromPub(publicKey) {
const CODE = new Uint8Array(varint.encode(0x1200)) // p-256 multicodec
;
const bytes = u8a.concat([
CODE,
publicKey
]);
return `did:key:z${u8a.toString(bytes, 'base58btc')}`;
}