Skip to content

Commit

Permalink
Bugfix/webauthn conformity (#1)
Browse files Browse the repository at this point in the history
* Fix WebAuthn registration and authentication flow

* Fix origin during authentication
  • Loading branch information
Blobonat authored Jul 16, 2020
1 parent f170598 commit 6dd6ede
Show file tree
Hide file tree
Showing 6 changed files with 100 additions and 65 deletions.
5 changes: 3 additions & 2 deletions dist/chromium/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
"manifest_version": 2,
"name": "CKey",
"description": "A Chrome Extension that emulates a Hardware Authentication Device",
"version": "1.0.2",
"version": "1.0.4",
"minimum_chrome_version": "36.0.1985.18",
"content_scripts": [
{
"all_frames": true,
"matches": [
"https://*/*"
"https://*/*",
"http://localhost/*"
],
"exclude_matches": [
"https://*/*.xml"
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"@types/jquery": "^3.3.31",
"@types/loglevel": "^1.6.3",
"@types/webappsec-credential-management": "^0.3.11",
"asn1js": "^2.0.26",
"bn.js": "^5.1.2",
"cbor": "^4.3.0",
"jquery": "^3.4.1",
"loglevel": "^1.6.6",
Expand Down
7 changes: 4 additions & 3 deletions src/background.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { disabledIcons, enabledIcons } from './constants';
import { getLogger } from './logging';
import { getOriginFromUrl, webauthnParse, webauthnStringify } from './utils';
import { generateKeyRequestAndAttestation, generateRegistrationKeyAndAttestation } from './webauthn';
import { generateAssertionResponse, generateAttestationResponse } from './webauthn';

const log = getLogger('background');

Expand Down Expand Up @@ -44,7 +44,7 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => {

try {
const opts = webauthnParse(msg.options);
const credential = await generateRegistrationKeyAndAttestation(
const credential = await generateAttestationResponse(
origin,
opts.publicKey,
`${pin}`,
Expand All @@ -66,10 +66,11 @@ const create = async (msg, sender: chrome.runtime.MessageSender) => {

const sign = async (msg, sender: chrome.runtime.MessageSender) => {
const opts = webauthnParse(msg.options);
const origin = getOriginFromUrl(sender.url);
const pin = await requestPin(sender.tab.id, origin);

try {
const credential = await generateKeyRequestAndAttestation(origin, opts.publicKey, `${pin}`);
const credential = await generateAssertionResponse(origin, opts.publicKey, `${pin}`);
const authenticatedResponseData = {
credential: webauthnStringify(credential),
requestID: msg.requestID,
Expand Down
51 changes: 30 additions & 21 deletions src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import * as CBOR from 'cbor';
import { getLogger } from './logging';
import { base64ToByteArray, byteArrayToBase64 } from './utils';
import * as asn1 from 'asn1.js';
import { BN } from 'bn.js';

const log = getLogger('crypto');

// Generated with pseudo random values via
// https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues
export const CKEY_ID = new Uint8Array([
const CKEY_ID = new Uint8Array([
194547236, 76082241, 3628762690, 4137210381,
1214244733, 1205845608, 840015201, 3897052717,
4072880437, 4027233456, 675224361, 2305433287,
Expand All @@ -29,23 +31,19 @@ function counterToBytes(c: number): Uint8Array {

const coseEllipticCurveNames: { [s: number]: string } = {
1: 'SHA-256',
2: 'SHA-384',
3: 'SHA-512',
};

const ellipticNamedCurvesToCOSE: { [s: string]: number } = {
'P-256': -7,
'P-384': -35,
'P-512': -36,
};

interface ICOSECompatibleKey {
algorithm: number;
privateKey: CryptoKey;
publicKey?: CryptoKey;
generateClientData(challenge: ArrayBuffer, extraOptions: any): Promise<string>;
generateAuthenticatorData(rpID: string, counter: number): Promise<Uint8Array>;
sign(clientData: string): Promise<any>;
generateAuthenticatorData(rpID: string, counter: number, credentialID: Uint8Array): Promise<Uint8Array>;
sign(data: Uint8Array): Promise<ArrayBuffer>;
}

class ECDSA implements ICOSECompatibleKey {
Expand Down Expand Up @@ -102,7 +100,7 @@ class ECDSA implements ICOSECompatibleKey {
});
}

public async generateAuthenticatorData(rpID: string, counter: number): Promise<Uint8Array> {
public async generateAuthenticatorData(rpID: string, counter: number, credentialID: Uint8Array): Promise<Uint8Array> {
const rpIdDigest = await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(rpID));
const rpIdHash = new Uint8Array(rpIdDigest);

Expand All @@ -116,13 +114,13 @@ class ECDSA implements ICOSECompatibleKey {
aaguid = CKEY_ID.slice(0, 16);
// 16-bit unsigned big-endian integer.
credIdLen = new Uint8Array(2);
credIdLen[0] = (CKEY_ID.length >> 8) & 0xff;
credIdLen[1] = CKEY_ID.length & 0xff;
credIdLen[0] = (credentialID.length >> 8) & 0xff;
credIdLen[1] = credentialID.length & 0xff;
const coseKey = await this.toCOSE(this.publicKey);
encodedKey = new Uint8Array(CBOR.encode(coseKey));
authenticatorDataLength += aaguid.length
+ credIdLen.byteLength
+ CKEY_ID.length
+ credentialID.length
+ encodedKey.byteLength;
}

Expand All @@ -139,8 +137,8 @@ class ECDSA implements ICOSECompatibleKey {
if (this.publicKey) {
// attestation flag goes on the 7th bit (from the right)
authenticatorData[rpIdHash.length] |= (1 << 6);
offset++;
}
offset++;

// 4 bytes for the counter. big-endian uint32
// https://www.w3.org/TR/webauthn/#signature-counter
Expand All @@ -155,29 +153,42 @@ class ECDSA implements ICOSECompatibleKey {
authenticatorData.set(aaguid, offset);
offset += aaguid.length;

// 2 bytes for the authenticator key ID length. 16-bit unsigned big-endian integer.
// 2 bytes for the credential ID length. 16-bit unsigned big-endian integer.
authenticatorData.set(credIdLen, offset);
offset += credIdLen.byteLength;

// Variable length authenticator key ID
authenticatorData.set(CKEY_ID, offset);
offset += CKEY_ID.length;
// Variable length credential ID
authenticatorData.set(credentialID, offset);
offset += credentialID.length;

// Variable length public key
authenticatorData.set(encodedKey, offset);

return authenticatorData;
}

public async sign(data: string): Promise<any> {
public async sign(data: Uint8Array): Promise<ArrayBuffer> {
if (!this.privateKey) {
throw new Error('no private key available for signing');
}
return window.crypto.subtle.sign(
const rawSign = await window.crypto.subtle.sign(
this.getKeyParams(),
this.privateKey,
new TextEncoder().encode(data),
data,
);

const rawSignBuf = new Buffer(rawSign);

// Credit to: https://stackoverflow.com/a/39651457/5333936
const EcdsaDerSig = asn1.define('ECPrivateKey', function() {
return this.seq().obj(
this.key('r').int(),
this.key('s').int()
);
});
const r = new BN(rawSignBuf.slice(0, 32).toString('hex'), 16, 'be');
const s = new BN(rawSignBuf.slice(32).toString('hex'), 16, 'be');
return EcdsaDerSig.encode({r, s}, 'der');
}

private getKeyParams(): EcdsaParams {
Expand All @@ -203,8 +214,6 @@ class ECDSA implements ICOSECompatibleKey {
const defaultPKParams = { alg: -7, type: 'public-key' };
const coseAlgorithmToKeyName = {
[-7]: 'ECDSA',
[-35]: 'ECDSA',
[-36]: 'ECDSA',
};

export const getCompatibleKey = (pkParams: PublicKeyCredentialParameters[]): Promise<ICOSECompatibleKey> => {
Expand Down
10 changes: 6 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,9 @@ export function byteArrayToBase64(arr: Uint8Array, urlEncoded: boolean = false):
const result = btoa(String.fromCharCode(...arr));
if (urlEncoded) {
return result.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
return result;
}
Expand All @@ -79,8 +80,9 @@ export function base64ToByteArray(str: string, urlEncoded: boolean = false): Uin
let rawInput = str;
if (urlEncoded) {
rawInput = padString(rawInput)
.replace(/\-/g, '+')
.replace(/_/g, '/');
.replace(/-/g, '+')
.replace(/_/g, '/')
.replace(/=/g, "");
}
return Uint8Array.from(atob(rawInput), (c) => c.charCodeAt(0));
}
Expand Down
90 changes: 55 additions & 35 deletions src/webauthn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,53 +6,48 @@ import { base64ToByteArray, byteArrayToBase64, getDomainFromOrigin } from './uti

const log = getLogger('webauthn');

export const generateRegistrationKeyAndAttestation = async (
export const generateAttestationResponse = async (
origin: string,
publicKeyCreationOptions: PublicKeyCredentialCreationOptions,
pin: string,
): Promise<PublicKeyCredential> => {
if (publicKeyCreationOptions.attestation === 'direct') {
log.warn('We are being requested to create a key with "direct" attestation');
log.warn(`We can only perform self-attestation, therefore we will not be provisioning any keys`);
if (publicKeyCreationOptions.attestation !== 'none') {
log.warn(`We are being requested to create a credential with ${publicKeyCreationOptions.attestation} attestation`);
log.warn(`We can only perform none attestation, therefore we will not be provisioning any credentials`);
return null;
}
const rp = publicKeyCreationOptions.rp;
const rpID = rp.id || getDomainFromOrigin(origin);
const user = publicKeyCreationOptions.user;
const userID = byteArrayToBase64(new Uint8Array(user.id as ArrayBuffer));
const keyID = window.btoa(`${userID}@${rpID}`);
const credId = createCredentialId();
const encCredId = byteArrayToBase64(credId, true);

// First check if there is already a key for this rp ID
if (await keyExists(keyID)) {
throw new Error(`key with id ${keyID} already exists`);
if (await keyExists(encCredId)) {
throw new Error(`credential with id ${encCredId} already exists`);
}
log.debug('key ID', keyID);
log.debug('key ID', encCredId);
const compatibleKey = await getCompatibleKey(publicKeyCreationOptions.pubKeyCredParams);

// TODO Increase key counter
const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0);
const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, credId);
const clientData = await compatibleKey.generateClientData(
publicKeyCreationOptions.challenge as ArrayBuffer,
{ origin, type: 'webauthn.create' },
);
const signature = await compatibleKey.sign(clientData);

const attestationObject = CBOR.encodeCanonical({
attStmt: {
alg: compatibleKey.algorithm,
sig: signature,
},
attStmt: new Map(),
authData: authenticatorData,
fmt: 'packed',
fmt: 'none',
}).buffer;

// Now that we have built all we need, let's save the key
await saveKey(keyID, compatibleKey.privateKey, pin);
// Now that we have built all we need, let's save the private key
await saveKey(encCredId, compatibleKey.privateKey, pin);

return {
getClientExtensionResults: () => ({}),
id: keyID,
rawId: base64ToByteArray(keyID),
id: encCredId,
rawId: credId,
response: {
attestationObject,
clientDataJSON: base64ToByteArray(window.btoa(clientData)),
Expand All @@ -61,47 +56,72 @@ export const generateRegistrationKeyAndAttestation = async (
} as PublicKeyCredential;
};

export const generateKeyRequestAndAttestation = async (
export const generateAssertionResponse = async (
origin: string,
publicKeyRequestOptions: PublicKeyCredentialRequestOptions,
pin: string,
): Promise<Credential> => {
if (!publicKeyRequestOptions.allowCredentials) {
log.debug('No keys requested');
log.debug('No credentials requested');
return null;
}

// For now we will only worry about the first entry
const requestedCredential = publicKeyRequestOptions.allowCredentials[0];
const keyIDArray: ArrayBuffer = requestedCredential.id as ArrayBuffer;
const keyID = byteArrayToBase64(new Uint8Array(keyIDArray));
const key = await fetchKey(keyID, pin);
const credId: ArrayBuffer = requestedCredential.id as ArrayBuffer;
const endCredId = byteArrayToBase64(new Uint8Array(credId), true);
const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin);

log.debug('credential ID', endCredId);

const key = await fetchKey(endCredId, pin);

if (!key) {
throw new Error(`key with id ${keyID} not found`);
throw new Error(`credentials with id ${endCredId} not found`);
}
const compatibleKey = await getCompatibleKeyFromCryptoKey(key);

const clientData = await compatibleKey.generateClientData(
publicKeyRequestOptions.challenge as ArrayBuffer,
{
origin,
tokenBinding: {
status: 'not-supported',
},
type: 'webauthn.create',
type: 'webauthn.get',
},
);
const signature = await compatibleKey.sign(clientData);
const rpID = publicKeyRequestOptions.rpId || getDomainFromOrigin(origin);
const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0);
const authenticatorData = await compatibleKey.generateAuthenticatorData(rpID, 0, new Uint8Array());
const clientDataJSON = base64ToByteArray(window.btoa(clientData));
const clientDataHash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', clientDataJSON));

const concatData = new Uint8Array(authenticatorData.length + clientDataHash.length);
concatData.set(authenticatorData);
concatData.set(clientDataHash, authenticatorData.length);


const signature = await compatibleKey.sign(concatData);

return {
id: keyID,
rawId: keyIDArray,
id: endCredId,
rawId: credId,
response: {
authenticatorData: authenticatorData.buffer,
clientDataJSON: base64ToByteArray(window.btoa(clientData)),
signature,
clientDataJSON: clientDataJSON,
signature: (new Uint8Array(signature)).buffer,
userHandle: new ArrayBuffer(0), // This should be nullable
},
type: 'public-key',
} as Credential;
};

function createCredentialId(): Uint8Array{
let enc = new TextEncoder();
let dt = new Date().getTime();
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = (dt + Math.random()*16)%16 | 0;
dt = Math.floor(dt/16);
return (c=='x' ? r :(r&0x3|0x8)).toString(16);
});
return base64ToByteArray(byteArrayToBase64(enc.encode(uuid), true), true);
}

0 comments on commit 6dd6ede

Please sign in to comment.