1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-16 16:23:44 +00:00

[EC-598] feat: initial version of credential creation

This commit is contained in:
Andreas Coroiu
2022-12-16 10:45:50 +01:00
parent 0b5422b9e6
commit e4bbb173b4
3 changed files with 385 additions and 1 deletions

View File

@@ -0,0 +1,42 @@
import { Fido2Utils } from "../../abstractions/fido2/fido2-utils";
export class InvalidCredentialIdEncodingError extends Error {
constructor(readonly input: unknown) {
super("Could not create instance of credentialId: Input has unknown encoding");
}
}
export class CredentialId {
readonly raw: Uint8Array;
readonly encoded: string;
constructor(rawOrEncoded: BufferSource | Uint8Array | string) {
if (rawOrEncoded instanceof Uint8Array) {
this.raw = rawOrEncoded;
this.encoded = CredentialId.encode(this.raw);
} else if (typeof rawOrEncoded === "string") {
this.encoded = rawOrEncoded;
this.raw = CredentialId.decode(this.encoded);
} else if (rawOrEncoded instanceof window.ArrayBuffer) {
this.raw = new Uint8Array(rawOrEncoded);
this.encoded = CredentialId.encode(this.raw);
} else if (rawOrEncoded.buffer) {
this.raw = new Uint8Array(rawOrEncoded.buffer);
this.encoded = CredentialId.encode(this.raw);
} else {
throw new InvalidCredentialIdEncodingError(rawOrEncoded);
}
}
equals(other: CredentialId) {
return this.encoded === other.encoded;
}
private static encode(raw: Uint8Array): string {
return Fido2Utils.bufferToString(raw);
}
private static decode(encoded: string): Uint8Array {
return Fido2Utils.stringToBuffer(encoded);
}
}

View File

@@ -0,0 +1,124 @@
/*
Copyright 2015 D2L Corporation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License. */
// Changes:
// - Cherry-pick the methods that we have a need for.
// - Add typings.
// - Original code is made for running in node, this version is adapted to work in the browser.
// https://github.com/Brightspace/node-ecdsa-sig-formatter/blob/master/src/param-bytes-for-alg.js
function getParamSize(keySize: number) {
const result = ((keySize / 8) | 0) + (keySize % 8 === 0 ? 0 : 1);
return result;
}
const paramBytesForAlg = {
ES256: getParamSize(256),
ES384: getParamSize(384),
ES512: getParamSize(521),
};
type Alg = keyof typeof paramBytesForAlg;
function getParamBytesForAlg(alg: Alg) {
const paramBytes = paramBytesForAlg[alg];
if (paramBytes) {
return paramBytes;
}
throw new Error('Unknown algorithm "' + alg + '"');
}
// https://github.com/Brightspace/node-ecdsa-sig-formatter/blob/master/src/ecdsa-sig-formatter.js
const MAX_OCTET = 0x80,
CLASS_UNIVERSAL = 0,
PRIMITIVE_BIT = 0x20,
TAG_SEQ = 0x10,
TAG_INT = 0x02,
ENCODED_TAG_SEQ = TAG_SEQ | PRIMITIVE_BIT | (CLASS_UNIVERSAL << 6),
ENCODED_TAG_INT = TAG_INT | (CLASS_UNIVERSAL << 6);
function countPadding(buf: Uint8Array, start: number, stop: number) {
let padding = 0;
while (start + padding < stop && buf[start + padding] === 0) {
++padding;
}
const needsSign = buf[start + padding] >= MAX_OCTET;
if (needsSign) {
--padding;
}
return padding;
}
export function joseToDer(signature: Uint8Array, alg: Alg) {
const paramBytes = getParamBytesForAlg(alg);
const signatureBytes = signature.length;
if (signatureBytes !== paramBytes * 2) {
throw new TypeError(
'"' +
alg +
'" signatures must be "' +
paramBytes * 2 +
'" bytes, saw "' +
signatureBytes +
'"'
);
}
const rPadding = countPadding(signature, 0, paramBytes);
const sPadding = countPadding(signature, paramBytes, signature.length);
const rLength = paramBytes - rPadding;
const sLength = paramBytes - sPadding;
const rsBytes = 1 + 1 + rLength + 1 + 1 + sLength;
const shortLength = rsBytes < MAX_OCTET;
const dst = new Uint8Array((shortLength ? 2 : 3) + rsBytes);
let offset = 0;
dst[offset++] = ENCODED_TAG_SEQ;
if (shortLength) {
dst[offset++] = rsBytes;
} else {
dst[offset++] = MAX_OCTET | 1;
dst[offset++] = rsBytes & 0xff;
}
dst[offset++] = ENCODED_TAG_INT;
dst[offset++] = rLength;
if (rPadding < 0) {
dst[offset++] = 0;
dst.set(signature.subarray(0, paramBytes), offset);
offset += paramBytes;
} else {
dst.set(signature.subarray(rPadding, paramBytes), offset);
offset += paramBytes;
}
dst[offset++] = ENCODED_TAG_INT;
dst[offset++] = sLength;
if (sPadding < 0) {
dst[offset++] = 0;
dst.set(signature.subarray(paramBytes), offset);
} else {
dst.set(signature.subarray(paramBytes + sPadding), offset);
}
return dst;
}

View File

@@ -1,20 +1,238 @@
import { CBOR } from "cbor-redux";
import { Fido2UserInterfaceService } from "../../abstractions/fido2/fido2-user-interface.service.abstraction";
import { Fido2Utils } from "../../abstractions/fido2/fido2-utils";
import {
CredentialRegistrationParams,
Fido2Service as Fido2ServiceAbstraction,
} from "../../abstractions/fido2/fido2.service.abstraction";
import { Utils } from "../../misc/utils";
import { CredentialId } from "./credential-id";
import { joseToDer } from "./ecdsa-utils";
const STANDARD_ATTESTATION_FORMAT = "packed";
interface BitCredential {
credentialId: CredentialId;
keyPair: CryptoKeyPair;
rpId: string;
origin: string;
userHandle: Uint8Array;
}
export class Fido2Service implements Fido2ServiceAbstraction {
private credentials = new Map<string, BitCredential>();
constructor(private fido2UserInterfaceService: Fido2UserInterfaceService) {}
async createCredential(params: CredentialRegistrationParams): Promise<unknown> {
await this.fido2UserInterfaceService.confirmNewCredential();
// eslint-disable-next-line no-console
console.log("Fido2Service.registerCredential", params);
return "createCredential response";
const attestationFormat = STANDARD_ATTESTATION_FORMAT;
const encoder = new TextEncoder();
const credentialId = new CredentialId(Utils.newGuid());
const clientData = encoder.encode(
JSON.stringify({
type: "webauthn.create",
challenge: params.challenge,
origin,
})
);
const keyPair = await crypto.subtle.generateKey(
{
name: "ECDSA",
namedCurve: "P-256",
},
true,
["sign", "verify"]
);
const authData = await generateAuthData({
rpId: params.rp.id,
credentialId,
userPresence: true,
userVerification: false,
keyPair,
attestationFormat: STANDARD_ATTESTATION_FORMAT,
});
const asn1Der_signature = await generateSignature({
authData,
clientData,
keyPair,
});
const attestationObject = new Uint8Array(
CBOR.encode({
fmt: attestationFormat,
attStmt: {
alg: -7,
sig: asn1Der_signature,
},
authData,
})
);
this.credentials.set(credentialId.encoded, {
credentialId,
keyPair,
origin,
rpId: params.rp.id,
userHandle: Fido2Utils.stringToBuffer(params.user.id),
});
return {
id: credentialId.encoded,
rawId: credentialId.raw,
type: "public-key",
response: {
clientDataJSON: clientData,
attestationObject: attestationObject,
} as AuthenticatorAttestationResponse,
getClientExtensionResults: () => ({}),
};
}
assertCredential(): unknown {
return undefined;
}
}
interface AuthDataParams {
rpId: string;
credentialId: CredentialId;
userPresence: boolean;
userVerification: boolean;
keyPair?: CryptoKeyPair;
attestationFormat?: "packed" | "fido-u2f";
}
async function generateAuthData(params: AuthDataParams) {
const encoder = new TextEncoder();
const authData: Array<number> = [];
const rpIdHash = new Uint8Array(
await crypto.subtle.digest({ name: "SHA-256" }, encoder.encode(params.rpId))
);
authData.push(...rpIdHash);
const flags = authDataFlags({
extensionData: false,
attestationData: params.keyPair !== undefined,
userVerification: params.userVerification,
userPresence: params.userPresence,
});
authData.push(flags);
// add 4 bytes of counter - we use time in epoch seconds as monotonic counter
const now = new Date().getTime() / 1000;
authData.push(
((now & 0xff000000) >> 24) & 0xff,
((now & 0x00ff0000) >> 16) & 0xff,
((now & 0x0000ff00) >> 8) & 0xff,
now & 0x000000ff
);
// attestedCredentialData
const attestedCredentialData: Array<number> = [];
// Use 0 because we're self-signing at the moment
const aaguid = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
attestedCredentialData.push(...aaguid);
// credentialIdLength (2 bytes) and credential Id
const rawId = params.credentialId.raw;
const credentialIdLength = [(rawId.length - (rawId.length & 0xff)) / 256, rawId.length & 0xff];
attestedCredentialData.push(...credentialIdLength);
attestedCredentialData.push(...rawId);
if (params.keyPair) {
const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
// COSE format of the EC256 key
const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);
const keyY = Utils.fromUrlB64ToArray(publicKeyJwk.y);
// const credPublicKeyCOSE = {
// "1": 2, // kty
// "3": -7, // alg
// "-1": 1, // crv
// "-2": keyX,
// "-3": keyY,
// };
// const coseBytes = new Uint8Array(cbor.encode(credPublicKeyCOSE));
// Can't get `cbor-redux` to encode in CTAP2 canonical CBOR. So we do it manually:
const coseBytes = new Uint8Array(77);
coseBytes.set([0xa5, 0x01, 0x02, 0x03, 0x26, 0x20, 0x01, 0x21, 0x58, 0x20], 0);
coseBytes.set(keyX, 10);
coseBytes.set([0x22, 0x58, 0x20], 10 + 32);
coseBytes.set(keyY, 10 + 32 + 3);
// credential public key - convert to array from CBOR encoded COSE key
const credPublicKeyBytes = coseBytes.subarray(0, -1);
attestedCredentialData.push(...credPublicKeyBytes);
authData.push(...attestedCredentialData);
}
return new Uint8Array(authData);
}
interface SignatureParams {
authData: Uint8Array;
clientData: Uint8Array;
keyPair: CryptoKeyPair;
}
async function generateSignature(params: SignatureParams) {
const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, params.clientData);
const sigBase = new Uint8Array([...params.authData, ...new Uint8Array(clientDataHash)]);
const p1336_signature = new Uint8Array(
await window.crypto.subtle.sign(
{
name: "ECDSA",
hash: { name: "SHA-256" },
},
params.keyPair.privateKey,
sigBase
)
);
const asn1Der_signature = joseToDer(p1336_signature, "ES256");
return asn1Der_signature;
}
interface Flags {
extensionData: boolean;
attestationData: boolean;
userVerification: boolean;
userPresence: boolean;
}
function authDataFlags(options: Flags): number {
let flags = 0;
if (options.extensionData) {
flags |= 0b1000000;
}
if (options.attestationData) {
flags |= 0b01000000;
}
if (options.userVerification) {
flags |= 0b00000100;
}
if (options.userPresence) {
flags |= 0b00000001;
}
return flags;
}