mirror of
https://github.com/bitwarden/browser.git
synced 2025-02-08 00:01:28 +01:00
[EC-598] feat: add signatures to attestation
This commit is contained in:
parent
151afeb241
commit
b3d5ab4472
@ -11,11 +11,16 @@ export class Fido2Utils {
|
|||||||
return Utils.fromUrlB64ToArray(str);
|
return Utils.fromUrlB64ToArray(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bufferSourceToUint8Array(bufferSource: BufferSource) {
|
static bufferSourceToUint8Array(bufferSource: BufferSource) {
|
||||||
if (bufferSource instanceof ArrayBuffer) {
|
if (Fido2Utils.isArrayBuffer(bufferSource)) {
|
||||||
return new Uint8Array(bufferSource);
|
return new Uint8Array(bufferSource);
|
||||||
} else {
|
} else {
|
||||||
return new Uint8Array(bufferSource.buffer);
|
return new Uint8Array(bufferSource.buffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Utility function to identify type of bufferSource. Necessary because of differences between runtimes */
|
||||||
|
static isArrayBuffer(bufferSource: BufferSource): bufferSource is ArrayBuffer {
|
||||||
|
return bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,10 +30,23 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
let userInterface!: MockProxy<Fido2UserInterfaceService>;
|
let userInterface!: MockProxy<Fido2UserInterfaceService>;
|
||||||
let authenticator!: Fido2AuthenticatorService;
|
let authenticator!: Fido2AuthenticatorService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
cipherService = mock<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
userInterface = mock<Fido2UserInterfaceService>();
|
userInterface = mock<Fido2UserInterfaceService>();
|
||||||
authenticator = new Fido2AuthenticatorService(cipherService, userInterface);
|
authenticator = new Fido2AuthenticatorService(cipherService, userInterface);
|
||||||
|
|
||||||
|
// crypto.subtle.importKey doesn't work properly in jest, so we need to mock it with new keys.
|
||||||
|
const privateKey = (
|
||||||
|
await crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "ECDSA",
|
||||||
|
namedCurve: "P-256",
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
["sign"]
|
||||||
|
)
|
||||||
|
).privateKey;
|
||||||
|
crypto.subtle.importKey = jest.fn().mockResolvedValue(privateKey);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("makeCredential", () => {
|
describe("makeCredential", () => {
|
||||||
@ -696,6 +709,9 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
const counter = encAuthData.slice(33, 37);
|
const counter = encAuthData.slice(33, 37);
|
||||||
|
|
||||||
expect(result.selectedCredential.id).toBe(selectedCredentialId);
|
expect(result.selectedCredential.id).toBe(selectedCredentialId);
|
||||||
|
expect(result.selectedCredential.userHandle).toEqual(
|
||||||
|
Fido2Utils.stringToBuffer(ciphers[0].fido2Key.userHandle)
|
||||||
|
);
|
||||||
expect(rpIdHash).toEqual(
|
expect(rpIdHash).toEqual(
|
||||||
new Uint8Array([
|
new Uint8Array([
|
||||||
0x22, 0x6b, 0xb3, 0x92, 0x02, 0xff, 0xf9, 0x22, 0xdc, 0x74, 0x05, 0xcd, 0x28, 0xa8,
|
0x22, 0x6b, 0xb3, 0x92, 0x02, 0xff, 0xf9, 0x22, 0xdc, 0x74, 0x05, 0xcd, 0x28, 0xa8,
|
||||||
@ -705,6 +721,8 @@ describe("FidoAuthenticatorService", () => {
|
|||||||
);
|
);
|
||||||
expect(flags).toEqual(new Uint8Array([0b00000001])); // UP = true
|
expect(flags).toEqual(new Uint8Array([0b00000001])); // UP = true
|
||||||
expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // 9001 in hex
|
expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // 9001 in hex
|
||||||
|
// Signatures are non-deterministic, and webcrypto can't verify DER signature format
|
||||||
|
// expect(result.signature).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -748,7 +766,13 @@ function createCipherView(
|
|||||||
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId;
|
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId;
|
||||||
cipher.fido2Key.rpId = fido2Key.rpId ?? RpId;
|
cipher.fido2Key.rpId = fido2Key.rpId ?? RpId;
|
||||||
cipher.fido2Key.counter = fido2Key.counter ?? 0;
|
cipher.fido2Key.counter = fido2Key.counter ?? 0;
|
||||||
cipher.fido2Key.userHandle = Fido2Utils.bufferToString(randomBytes(16));
|
cipher.fido2Key.userHandle = fido2Key.userHandle ?? Fido2Utils.bufferToString(randomBytes(16));
|
||||||
|
cipher.fido2Key.keyAlgorithm = fido2Key.keyAlgorithm ?? "ECDSA";
|
||||||
|
cipher.fido2Key.keyCurve = fido2Key.keyCurve ?? "P-256";
|
||||||
|
cipher.fido2Key.keyValue =
|
||||||
|
fido2Key.keyValue ??
|
||||||
|
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgTC-7XDZipXbaVBlnkjlBgO16ZmqBZWejK2iYo6lV0dehRANCAASOcM2WduNq1DriRYN7ZekvZz-bRhA-qNT4v0fbp5suUFJyWmgOQ0bybZcLXHaerK5Ep1JiSrQcewtQNgLtry7f";
|
||||||
|
|
||||||
return cipher;
|
return cipher;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,6 +17,8 @@ import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface.
|
|||||||
import { Fido2Utils } from "../abstractions/fido2-utils";
|
import { Fido2Utils } from "../abstractions/fido2-utils";
|
||||||
import { Fido2KeyView } from "../models/view/fido2-key.view";
|
import { Fido2KeyView } from "../models/view/fido2-key.view";
|
||||||
|
|
||||||
|
import { joseToDer } from "./ecdsa-utils";
|
||||||
|
|
||||||
// AAGUID: 6e8248d5-b479-40db-a3d8-11116f7e8349
|
// AAGUID: 6e8248d5-b479-40db-a3d8-11116f7e8349
|
||||||
export const AAGUID = new Uint8Array([
|
export const AAGUID = new Uint8Array([
|
||||||
0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49,
|
0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49,
|
||||||
@ -79,12 +81,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
keyPair = await this.createKeyPair();
|
keyPair = await createKeyPair();
|
||||||
|
|
||||||
cipher = new CipherView();
|
cipher = new CipherView();
|
||||||
cipher.type = CipherType.Fido2Key;
|
cipher.type = CipherType.Fido2Key;
|
||||||
cipher.name = params.rpEntity.name;
|
cipher.name = params.rpEntity.name;
|
||||||
cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey);
|
cipher.fido2Key = await createKeyView(params, keyPair.privateKey);
|
||||||
const encrypted = await this.cipherService.encrypt(cipher);
|
const encrypted = await this.cipherService.encrypt(cipher);
|
||||||
await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here
|
await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here
|
||||||
cipher.id = encrypted.id;
|
cipher.id = encrypted.id;
|
||||||
@ -102,11 +104,11 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
keyPair = await this.createKeyPair();
|
keyPair = await createKeyPair();
|
||||||
|
|
||||||
const encrypted = await this.cipherService.get(cipherId);
|
const encrypted = await this.cipherService.get(cipherId);
|
||||||
cipher = await encrypted.decrypt();
|
cipher = await encrypted.decrypt();
|
||||||
cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey);
|
cipher.fido2Key = await createKeyView(params, keyPair.privateKey);
|
||||||
const reencrypted = await this.cipherService.encrypt(cipher);
|
const reencrypted = await this.cipherService.encrypt(cipher);
|
||||||
await this.cipherService.updateWithServer(reencrypted);
|
await this.cipherService.updateWithServer(reencrypted);
|
||||||
} catch {
|
} catch {
|
||||||
@ -189,11 +191,11 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
userVerification: false,
|
userVerification: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// const signature = await generateSignature({
|
const signature = await generateSignature({
|
||||||
// authData,
|
authData: authenticatorData,
|
||||||
// clientData,
|
clientData: params.hash,
|
||||||
// privateKey: credential.keyValue,
|
privateKey: await getPrivateKeyFromCipher(selectedCipher),
|
||||||
// });
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authenticatorData,
|
authenticatorData,
|
||||||
@ -201,7 +203,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
id: selectedCredentialId,
|
id: selectedCredentialId,
|
||||||
userHandle: Fido2Utils.stringToBuffer(selectedCipher.fido2Key.userHandle),
|
userHandle: Fido2Utils.stringToBuffer(selectedCipher.fido2Key.userHandle),
|
||||||
},
|
},
|
||||||
signature: null,
|
signature,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,38 +266,55 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
|||||||
(cipher) => cipher.type === CipherType.Fido2Key && cipher.fido2Key.rpId === rpId
|
(cipher) => cipher.type === CipherType.Fido2Key && cipher.fido2Key.rpId === rpId
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async createKeyPair() {
|
async function createKeyPair() {
|
||||||
return await crypto.subtle.generateKey(
|
return await crypto.subtle.generateKey(
|
||||||
{
|
{
|
||||||
name: "ECDSA",
|
name: "ECDSA",
|
||||||
namedCurve: "P-256",
|
namedCurve: "P-256",
|
||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
KeyUsages
|
KeyUsages
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createKeyView(
|
||||||
|
params: Fido2AuthenticatorMakeCredentialsParams,
|
||||||
|
keyValue: CryptoKey
|
||||||
|
): Promise<Fido2KeyView> {
|
||||||
|
if (keyValue.algorithm.name !== "ECDSA" && (keyValue.algorithm as any).namedCurve !== "P-256") {
|
||||||
|
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async createKeyView(
|
const pkcs8Key = await crypto.subtle.exportKey("pkcs8", keyValue);
|
||||||
params: Fido2AuthenticatorMakeCredentialsParams,
|
const fido2Key = new Fido2KeyView();
|
||||||
keyValue: CryptoKey
|
fido2Key.nonDiscoverableId = params.requireResidentKey ? null : Utils.newGuid();
|
||||||
): Promise<Fido2KeyView> {
|
fido2Key.keyType = "public-key";
|
||||||
const pcks8Key = await crypto.subtle.exportKey("pkcs8", keyValue);
|
fido2Key.keyAlgorithm = "ECDSA";
|
||||||
|
fido2Key.keyCurve = "P-256";
|
||||||
|
fido2Key.keyValue = Fido2Utils.bufferToString(pkcs8Key);
|
||||||
|
fido2Key.rpId = params.rpEntity.id;
|
||||||
|
fido2Key.userHandle = Fido2Utils.bufferToString(params.userEntity.id);
|
||||||
|
fido2Key.counter = 0;
|
||||||
|
fido2Key.rpName = params.rpEntity.name;
|
||||||
|
fido2Key.userName = params.userEntity.name;
|
||||||
|
|
||||||
const fido2Key = new Fido2KeyView();
|
return fido2Key;
|
||||||
fido2Key.nonDiscoverableId = params.requireResidentKey ? null : Utils.newGuid();
|
}
|
||||||
fido2Key.keyType = "public-key";
|
|
||||||
fido2Key.keyAlgorithm = "ECDSA";
|
|
||||||
fido2Key.keyCurve = "P-256";
|
|
||||||
fido2Key.keyValue = Fido2Utils.bufferToString(pcks8Key);
|
|
||||||
fido2Key.rpId = params.rpEntity.id;
|
|
||||||
fido2Key.userHandle = Fido2Utils.bufferToString(params.userEntity.id);
|
|
||||||
fido2Key.counter = 0;
|
|
||||||
fido2Key.rpName = params.rpEntity.name;
|
|
||||||
fido2Key.userName = params.userEntity.name;
|
|
||||||
|
|
||||||
return fido2Key;
|
async function getPrivateKeyFromCipher(cipher: CipherView): Promise<CryptoKey> {
|
||||||
}
|
const keyBuffer = Fido2Utils.stringToBuffer(cipher.fido2Key.keyValue);
|
||||||
|
return await crypto.subtle.importKey(
|
||||||
|
"pkcs8",
|
||||||
|
keyBuffer,
|
||||||
|
{
|
||||||
|
name: cipher.fido2Key.keyType,
|
||||||
|
namedCurve: cipher.fido2Key.keyCurve,
|
||||||
|
} as EcKeyImportParams,
|
||||||
|
true,
|
||||||
|
KeyUsages
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthDataParams {
|
interface AuthDataParams {
|
||||||
@ -366,6 +385,32 @@ async function generateAuthData(params: AuthDataParams) {
|
|||||||
return new Uint8Array(authData);
|
return new Uint8Array(authData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SignatureParams {
|
||||||
|
authData: Uint8Array;
|
||||||
|
clientData: BufferSource;
|
||||||
|
privateKey: CryptoKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateSignature(params: SignatureParams) {
|
||||||
|
const clientData = Fido2Utils.bufferSourceToUint8Array(params.clientData);
|
||||||
|
const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, clientData);
|
||||||
|
const sigBase = new Uint8Array([...params.authData, ...new Uint8Array(clientDataHash)]);
|
||||||
|
const p1336_signature = new Uint8Array(
|
||||||
|
await crypto.subtle.sign(
|
||||||
|
{
|
||||||
|
name: "ECDSA",
|
||||||
|
hash: { name: "SHA-256" },
|
||||||
|
},
|
||||||
|
params.privateKey,
|
||||||
|
sigBase
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const asn1Der_signature = joseToDer(p1336_signature, "ES256");
|
||||||
|
|
||||||
|
return asn1Der_signature;
|
||||||
|
}
|
||||||
|
|
||||||
interface Flags {
|
interface Flags {
|
||||||
extensionData: boolean;
|
extensionData: boolean;
|
||||||
attestationData: boolean;
|
attestationData: boolean;
|
||||||
|
Loading…
Reference in New Issue
Block a user