| 'use strict'; |
| |
| // See https://www.iana.org/assignments/cose/cose.xhtml#key-type |
| const cose_key_type_ec2 = 2; |
| const cose_key_type_rsa = 3; |
| |
| // Decode |encoded| using a base64url decoding. |
| function base64urlToUint8Array(encoded) { |
| return Uint8Array.from(base64urlDecode(encoded), c => c.charCodeAt(0)); |
| } |
| |
| // The result of a browser bound key verification. |
| const BrowserBoundKeyVerificationResult = Object.freeze({ |
| // No browser bound key was included. |
| NoBrowserBoundKey: 'NoBrowserBoundKey', |
| // A browser bound key was included and the cryptographic signature verifies. |
| BrowserBoundKeySignatureVerified: 'BrowserBoundKeySignatureVerified', |
| }); |
| |
| // This function takes a credential and verifies either that no BBK was |
| // included (no browser bound public key, and no browser bound signature) |
| // or that the BBK was included (browser bound public key, browser bound |
| // signature, and the signature cryptographically verifies). |
| // |
| // Returns a BrowserBoundKeyVerificationResult informing the conditions of |
| // successful verification. |
| async function verifyBrowserBoundKey(credential, expectedKeyTypes) { |
| const clientExtensionResults = credential.getClientExtensionResults(); |
| const signatureArray = |
| clientExtensionResults?.payment?.browserBoundSignature?.signature; |
| const clientData = JSON.parse(String.fromCharCode.apply( |
| null, new Uint8Array(credential.response.clientDataJSON))); |
| const publicKeyCoseKeyEncoded = clientData?.payment?.browserBoundPublicKey; |
| assert_equals( |
| signatureArray !== undefined, publicKeyCoseKeyEncoded !== undefined, |
| 'Either both or none of the browser bound public key and signature must ' + |
| 'be present, but only one was present.') |
| if (signatureArray == undefined) { |
| return BrowserBoundKeyVerificationResult.NoBrowserBoundKey; |
| } |
| assertBrowserBoundSignatureInClientExtensionResults(clientExtensionResults); |
| await assertBrowserBoundKeySignature( |
| credential.response.clientDataJSON, signatureArray, expectedKeyTypes); |
| return BrowserBoundKeyVerificationResult.BrowserBoundKeySignatureVerified; |
| } |
| |
| function getBrowserBoundPublicKeyFromCredential(credential) { |
| const clientData = JSON.parse(String.fromCharCode.apply( |
| null, new Uint8Array(credential.response.clientDataJSON))); |
| return clientData?.payment?.browserBoundPublicKey; |
| } |
| |
| function assertNoBrowserBoundPublicKeyInCredential(credential, message) { |
| const clientData = JSON.parse(String.fromCharCode.apply( |
| null, new Uint8Array(credential.response.clientDataJSON))); |
| assert_equals(clientData?.payment?.browserBoundPublicKey, undefined, message); |
| } |
| |
| function assertBrowserBoundSignatureInClientExtensionResults( |
| clientExtensionResults) { |
| assert_not_equals( |
| clientExtensionResults.payment, undefined, |
| 'getClientExtensionResults().payment is not undefined'); |
| assert_not_equals( |
| clientExtensionResults.payment.browserBoundSignature, undefined, |
| 'getClientExtensionResults().payment is not undefined'); |
| assert_not_equals( |
| clientExtensionResults.payment.browserBoundSignature.signature, undefined, |
| 'getClientExtensionResults().payment.signature is not undefined'); |
| } |
| |
| async function assertBrowserBoundKeySignature( |
| clientDataJSON, signatureArray, expectedKeyTypes) { |
| const clientData = JSON.parse( |
| String.fromCharCode.apply(null, new Uint8Array(clientDataJSON))); |
| assert_not_equals( |
| clientData.payment, undefined, |
| `Deserialized clientData, ${ |
| JSON.stringify(clientData)}, should contain a 'payment' member`); |
| assert_not_equals( |
| clientData.payment.browserBoundPublicKey, undefined, |
| `ClientData['payment'] should contain a 'browserBoundPublicKey' member.`); |
| const browserBoundPublicKeyCoseKeyBase64 = |
| clientData.payment.browserBoundPublicKey; |
| const browserBoundPublicKeyCoseKeyEncoded = |
| base64urlToUint8Array(browserBoundPublicKeyCoseKeyBase64); |
| const keyType = getCoseKeyType(browserBoundPublicKeyCoseKeyEncoded); |
| assert_true( |
| expectedKeyTypes.includes(keyType), |
| `KeyType, ${keyType}, was not one of the expected key types, ${ |
| expectedKeyTypes}`); |
| if (keyType == cose_key_type_ec2) { |
| // Verify the signature for a ES256 signature scheme. |
| const browserBoundPublicKeyCoseKey = |
| parseCosePublicKey(browserBoundPublicKeyCoseKeyEncoded); |
| const jwkPublicKey = coseObjectToJWK(browserBoundPublicKeyCoseKey); |
| const key = await crypto.subtle.importKey( |
| 'jwk', jwkPublicKey, {name: 'ECDSA', namedCurve: 'P-256'}, |
| /*extractable=*/ false, ['verify']); |
| const signature = |
| convertDERSignatureToSubtle(new Uint8Array(signatureArray)); |
| assert_true(await crypto.subtle.verify( |
| {name: 'ECDSA', hash: 'SHA-256'}, key, signature, clientDataJSON)); |
| } |
| // TODO: Verify the signature in case of an RS256 signature scheme. |
| } |