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

[EC-598] feat: implement assertion

This commit is contained in:
Andreas Coroiu
2023-03-30 09:12:54 +02:00
parent 5bf4156fc6
commit 151afeb241
3 changed files with 131 additions and 66 deletions

View File

@@ -98,9 +98,9 @@ export interface Fido2AuthenticatorGetAssertionParams {
} }
export interface Fido2AuthenticatorGetAssertionResult { export interface Fido2AuthenticatorGetAssertionResult {
selectedCredential?: { selectedCredential: {
id: string; id: string;
userHandle: Uint8Array; userHandle?: Uint8Array;
}; };
authenticatorData: Uint8Array; authenticatorData: Uint8Array;
signature: Uint8Array; signature: Uint8Array;

View File

@@ -629,50 +629,85 @@ describe("FidoAuthenticatorService", () => {
}); });
}); });
describe("assertion of non-discoverable credential", () => { for (const residentKey of [true, false]) {
let credentialIds: string[]; describe(`assertion of ${
let ciphers: CipherView[]; residentKey ? "discoverable" : "non-discoverable"
let params: Fido2AuthenticatorGetAssertionParams; } credential`, () => {
let credentialIds: string[];
let selectedCredentialId: string;
let ciphers: CipherView[];
let params: Fido2AuthenticatorGetAssertionParams;
beforeEach(async () => { beforeEach(async () => {
credentialIds = [Utils.newGuid(), Utils.newGuid()]; credentialIds = [Utils.newGuid(), Utils.newGuid()];
ciphers = await Promise.all( if (residentKey) {
credentialIds.map((id) => ciphers = credentialIds.map((id) =>
createCipherView( createCipherView({ type: CipherType.Fido2Key }, { rpId: RpId, counter: 9000 })
{ type: CipherType.Login }, );
{ nonDiscoverableId: id, rpId: RpId, counter: 9000 } selectedCredentialId = ciphers[0].id;
) params = await createParams({
) allowCredentialDescriptorList: undefined,
); rpId: RpId,
params = await createParams({ });
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({ } else {
id: Utils.guidToRawFormat(credentialId), ciphers = credentialIds.map((id) =>
type: "public-key", createCipherView(
})), { type: CipherType.Login },
rpId: RpId, { nonDiscoverableId: id, rpId: RpId, counter: 9000 }
)
);
selectedCredentialId = credentialIds[0];
params = await createParams({
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
id: Utils.guidToRawFormat(credentialId),
type: "public-key",
})),
rpId: RpId,
});
}
cipherService.getAllDecrypted.mockResolvedValue(ciphers);
userInterface.pickCredential.mockResolvedValue(ciphers[0].id);
});
/** Spec: Increment the credential associated signature counter */
it("should increment counter", async () => {
const encrypted = Symbol();
cipherService.encrypt.mockResolvedValue(encrypted as any);
await authenticator.getAssertion(params);
expect(cipherService.encrypt).toHaveBeenCalledWith(
expect.objectContaining({
id: ciphers[0].id,
fido2Key: expect.objectContaining({
counter: 9001,
}),
})
);
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
});
it("should return an assertion result", async () => {
const result = await authenticator.getAssertion(params);
const encAuthData = result.authenticatorData;
const rpIdHash = encAuthData.slice(0, 32);
const flags = encAuthData.slice(32, 33);
const counter = encAuthData.slice(33, 37);
expect(result.selectedCredential.id).toBe(selectedCredentialId);
expect(rpIdHash).toEqual(
new Uint8Array([
0x22, 0x6b, 0xb3, 0x92, 0x02, 0xff, 0xf9, 0x22, 0xdc, 0x74, 0x05, 0xcd, 0x28, 0xa8,
0x34, 0x5a, 0xc4, 0xf2, 0x64, 0x51, 0xd7, 0x3d, 0x0b, 0x40, 0xef, 0xf3, 0x1d, 0xc1,
0xd0, 0x5c, 0x3d, 0xc3,
])
);
expect(flags).toEqual(new Uint8Array([0b00000001])); // UP = true
expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // 9001 in hex
}); });
cipherService.getAllDecrypted.mockResolvedValue(ciphers);
userInterface.pickCredential.mockResolvedValue(ciphers[0].id);
}); });
}
/** Spec: Increment the credential associated signature counter */
it("should increment counter", async () => {
const encrypted = Symbol();
cipherService.encrypt.mockResolvedValue(encrypted as any);
await authenticator.getAssertion(params);
expect(cipherService.encrypt).toHaveBeenCalledWith(
expect.objectContaining({
id: ciphers[0].id,
fido2Key: expect.objectContaining({
counter: 9001,
}),
})
);
expect(cipherService.updateWithServer).toHaveBeenCalledWith(encrypted);
});
});
async function createParams( async function createParams(
params: Partial<Fido2AuthenticatorGetAssertionParams> = {} params: Partial<Fido2AuthenticatorGetAssertionParams> = {}
@@ -713,6 +748,7 @@ 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));
return cipher; return cipher;
} }
@@ -729,6 +765,7 @@ async function createClientDataHash() {
return await crypto.subtle.digest({ name: "SHA-256" }, clientData); return await crypto.subtle.digest({ name: "SHA-256" }, clientData);
} }
/** This is a fake function that always returns the same byte sequence */
function randomBytes(length: number) { function randomBytes(length: number) {
return new Uint8Array(Array.from({ length }, () => Math.floor(Math.random() * 255))); return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
} }

View File

@@ -146,35 +146,63 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint);
} }
let credentialOptions: CipherView[]; let cipherOptions: CipherView[];
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
if (params.allowCredentialDescriptorList?.length > 0) { if (params.allowCredentialDescriptorList?.length > 0) {
credentialOptions = await this.findNonDiscoverableCredentials( cipherOptions = await this.findNonDiscoverableCredentials(
params.allowCredentialDescriptorList, params.allowCredentialDescriptorList,
params.rpId params.rpId
); );
} else { } else {
credentialOptions = await this.findDiscoverableCredentials(params.rpId); cipherOptions = await this.findDiscoverableCredentials(params.rpId);
} }
if (credentialOptions.length === 0) { if (cipherOptions.length === 0) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
} }
const selectedCredentialId = await this.userInterface.pickCredential( const selectedCipherId = await this.userInterface.pickCredential(
credentialOptions.map((cipher) => cipher.id) cipherOptions.map((cipher) => cipher.id)
); );
const selectedCredential = credentialOptions.find((c) => c.id === selectedCredentialId); const selectedCipher = cipherOptions.find((c) => c.id === selectedCipherId);
if (selectedCredential === undefined) { if (selectedCipher === undefined) {
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed); throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotAllowed);
} }
++selectedCredential.fido2Key.counter; const selectedCredentialId =
selectedCredential.localData.lastUsedDate = new Date().getTime(); params.allowCredentialDescriptorList?.length > 0
const encrypted = await this.cipherService.encrypt(selectedCredential); ? selectedCipher.fido2Key.nonDiscoverableId
: selectedCipher.id;
++selectedCipher.fido2Key.counter;
selectedCipher.localData.lastUsedDate = new Date().getTime();
const encrypted = await this.cipherService.encrypt(selectedCipher);
await this.cipherService.updateWithServer(encrypted); await this.cipherService.updateWithServer(encrypted);
const authenticatorData = await generateAuthData({
rpId: selectedCipher.fido2Key.rpId,
credentialId: selectedCredentialId,
counter: selectedCipher.fido2Key.counter,
userPresence: true,
userVerification: false,
});
// const signature = await generateSignature({
// authData,
// clientData,
// privateKey: credential.keyValue,
// });
return {
authenticatorData,
selectedCredential: {
id: selectedCredentialId,
userHandle: Fido2Utils.stringToBuffer(selectedCipher.fido2Key.userHandle),
},
signature: null,
};
} }
private async vaultContainsCredentials( private async vaultContainsCredentials(
@@ -305,18 +333,18 @@ async function generateAuthData(params: AuthDataParams) {
counter & 0x000000ff counter & 0x000000ff
); );
// attestedCredentialData
const attestedCredentialData: Array<number> = [];
attestedCredentialData.push(...AAGUID);
// credentialIdLength (2 bytes) and credential Id
const rawId = Utils.guidToRawFormat(params.credentialId);
const credentialIdLength = [(rawId.length - (rawId.length & 0xff)) / 256, rawId.length & 0xff];
attestedCredentialData.push(...credentialIdLength);
attestedCredentialData.push(...rawId);
if (params.keyPair) { if (params.keyPair) {
// attestedCredentialData
const attestedCredentialData: Array<number> = [];
attestedCredentialData.push(...AAGUID);
// credentialIdLength (2 bytes) and credential Id
const rawId = Utils.guidToRawFormat(params.credentialId);
const credentialIdLength = [(rawId.length - (rawId.length & 0xff)) / 256, rawId.length & 0xff];
attestedCredentialData.push(...credentialIdLength);
attestedCredentialData.push(...rawId);
const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey); const publicKeyJwk = await crypto.subtle.exportKey("jwk", params.keyPair.publicKey);
// COSE format of the EC256 key // COSE format of the EC256 key
const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x); const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);