mirror of
https://github.com/bitwarden/browser
synced 2025-12-17 08:43:33 +00:00
[EC-598] feat: initial version of credential creation
This commit is contained in:
42
libs/common/src/services/fido2/credential-id.ts
Normal file
42
libs/common/src/services/fido2/credential-id.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
124
libs/common/src/services/fido2/ecdsa-utils.ts
Normal file
124
libs/common/src/services/fido2/ecdsa-utils.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -1,20 +1,238 @@
|
|||||||
|
import { CBOR } from "cbor-redux";
|
||||||
|
|
||||||
import { Fido2UserInterfaceService } from "../../abstractions/fido2/fido2-user-interface.service.abstraction";
|
import { Fido2UserInterfaceService } from "../../abstractions/fido2/fido2-user-interface.service.abstraction";
|
||||||
|
import { Fido2Utils } from "../../abstractions/fido2/fido2-utils";
|
||||||
import {
|
import {
|
||||||
CredentialRegistrationParams,
|
CredentialRegistrationParams,
|
||||||
Fido2Service as Fido2ServiceAbstraction,
|
Fido2Service as Fido2ServiceAbstraction,
|
||||||
} from "../../abstractions/fido2/fido2.service.abstraction";
|
} 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 {
|
export class Fido2Service implements Fido2ServiceAbstraction {
|
||||||
|
private credentials = new Map<string, BitCredential>();
|
||||||
|
|
||||||
constructor(private fido2UserInterfaceService: Fido2UserInterfaceService) {}
|
constructor(private fido2UserInterfaceService: Fido2UserInterfaceService) {}
|
||||||
|
|
||||||
async createCredential(params: CredentialRegistrationParams): Promise<unknown> {
|
async createCredential(params: CredentialRegistrationParams): Promise<unknown> {
|
||||||
await this.fido2UserInterfaceService.confirmNewCredential();
|
await this.fido2UserInterfaceService.confirmNewCredential();
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.log("Fido2Service.registerCredential", params);
|
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 {
|
assertCredential(): unknown {
|
||||||
return undefined;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user