mirror of
https://github.com/bitwarden/browser.git
synced 2025-01-31 22:51:28 +01:00
[EC-598] feat: implement assertion
This commit is contained in:
parent
5bf4156fc6
commit
151afeb241
@ -98,9 +98,9 @@ export interface Fido2AuthenticatorGetAssertionParams {
|
||||
}
|
||||
|
||||
export interface Fido2AuthenticatorGetAssertionResult {
|
||||
selectedCredential?: {
|
||||
selectedCredential: {
|
||||
id: string;
|
||||
userHandle: Uint8Array;
|
||||
userHandle?: Uint8Array;
|
||||
};
|
||||
authenticatorData: Uint8Array;
|
||||
signature: Uint8Array;
|
||||
|
@ -629,50 +629,85 @@ describe("FidoAuthenticatorService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("assertion of non-discoverable credential", () => {
|
||||
let credentialIds: string[];
|
||||
let ciphers: CipherView[];
|
||||
let params: Fido2AuthenticatorGetAssertionParams;
|
||||
for (const residentKey of [true, false]) {
|
||||
describe(`assertion of ${
|
||||
residentKey ? "discoverable" : "non-discoverable"
|
||||
} credential`, () => {
|
||||
let credentialIds: string[];
|
||||
let selectedCredentialId: string;
|
||||
let ciphers: CipherView[];
|
||||
let params: Fido2AuthenticatorGetAssertionParams;
|
||||
|
||||
beforeEach(async () => {
|
||||
credentialIds = [Utils.newGuid(), Utils.newGuid()];
|
||||
ciphers = await Promise.all(
|
||||
credentialIds.map((id) =>
|
||||
createCipherView(
|
||||
{ type: CipherType.Login },
|
||||
{ nonDiscoverableId: id, rpId: RpId, counter: 9000 }
|
||||
)
|
||||
)
|
||||
);
|
||||
params = await createParams({
|
||||
allowCredentialDescriptorList: credentialIds.map((credentialId) => ({
|
||||
id: Utils.guidToRawFormat(credentialId),
|
||||
type: "public-key",
|
||||
})),
|
||||
rpId: RpId,
|
||||
beforeEach(async () => {
|
||||
credentialIds = [Utils.newGuid(), Utils.newGuid()];
|
||||
if (residentKey) {
|
||||
ciphers = credentialIds.map((id) =>
|
||||
createCipherView({ type: CipherType.Fido2Key }, { rpId: RpId, counter: 9000 })
|
||||
);
|
||||
selectedCredentialId = ciphers[0].id;
|
||||
params = await createParams({
|
||||
allowCredentialDescriptorList: undefined,
|
||||
rpId: RpId,
|
||||
});
|
||||
} else {
|
||||
ciphers = credentialIds.map((id) =>
|
||||
createCipherView(
|
||||
{ type: CipherType.Login },
|
||||
{ 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(
|
||||
params: Partial<Fido2AuthenticatorGetAssertionParams> = {}
|
||||
@ -713,6 +748,7 @@ function createCipherView(
|
||||
cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId;
|
||||
cipher.fido2Key.rpId = fido2Key.rpId ?? RpId;
|
||||
cipher.fido2Key.counter = fido2Key.counter ?? 0;
|
||||
cipher.fido2Key.userHandle = Fido2Utils.bufferToString(randomBytes(16));
|
||||
return cipher;
|
||||
}
|
||||
|
||||
@ -729,6 +765,7 @@ async function createClientDataHash() {
|
||||
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) {
|
||||
return new Uint8Array(Array.from({ length }, () => Math.floor(Math.random() * 255)));
|
||||
return new Uint8Array(Array.from({ length }, (_, k) => k % 255));
|
||||
}
|
||||
|
@ -146,35 +146,63 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr
|
||||
throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint);
|
||||
}
|
||||
|
||||
let credentialOptions: CipherView[];
|
||||
let cipherOptions: CipherView[];
|
||||
|
||||
// eslint-disable-next-line no-empty
|
||||
if (params.allowCredentialDescriptorList?.length > 0) {
|
||||
credentialOptions = await this.findNonDiscoverableCredentials(
|
||||
cipherOptions = await this.findNonDiscoverableCredentials(
|
||||
params.allowCredentialDescriptorList,
|
||||
params.rpId
|
||||
);
|
||||
} 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);
|
||||
}
|
||||
|
||||
const selectedCredentialId = await this.userInterface.pickCredential(
|
||||
credentialOptions.map((cipher) => cipher.id)
|
||||
const selectedCipherId = await this.userInterface.pickCredential(
|
||||
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);
|
||||
}
|
||||
|
||||
++selectedCredential.fido2Key.counter;
|
||||
selectedCredential.localData.lastUsedDate = new Date().getTime();
|
||||
const encrypted = await this.cipherService.encrypt(selectedCredential);
|
||||
const selectedCredentialId =
|
||||
params.allowCredentialDescriptorList?.length > 0
|
||||
? 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);
|
||||
|
||||
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(
|
||||
@ -305,18 +333,18 @@ async function generateAuthData(params: AuthDataParams) {
|
||||
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) {
|
||||
// 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);
|
||||
// COSE format of the EC256 key
|
||||
const keyX = Utils.fromUrlB64ToArray(publicKeyJwk.x);
|
||||
|
Loading…
Reference in New Issue
Block a user