From 4a5c1badca7e78c0707128fbfcaa1c6c3ee25997 Mon Sep 17 00:00:00 2001 From: Eric Heikes Date: Mon, 13 Jan 2020 23:00:37 -0800 Subject: [PATCH 1/3] Add support for Android SafetyNet attestation format --- README.md | 5 +- example/api/index.js | 20 ++- packages/client/README.md | 5 +- packages/server/README.md | 5 +- packages/server/package.json | 4 +- packages/server/src/authenticatorKey/index.js | 7 +- .../parseAndroidSafetyNetKey.js | 145 ++++++++++++++++++ packages/server/src/login.js | 10 ++ packages/server/src/registration.js | 12 +- packages/server/src/utils.js | 17 +- yarn.lock | 67 +++++++- 11 files changed, 273 insertions(+), 24 deletions(-) create mode 100644 packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js diff --git a/README.md b/README.md index 8fd33da..af58b89 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ import { } from '@webauthn/server'; ``` -- `parseRegisterRequest`: +- `parseRegisterRequest` (async): Extract challenge and key from the register request body. The challenge allow to retrieve the user, and the key must be stored server side linked to the user. - `generateRegistrationChallenge`: Generate a challenge from a relying party and a user `{ relyingParty, user }` to be sent back to the client, in order to register @@ -63,10 +63,9 @@ See an example in example/server ## Roadmap -For now only fido-u2f and packed format are implemented +For now only android-safetynet, fido-u2f, and packed formats are implemented - Implement android-key format -- Implement android-safetynet format - Implement tpm format diff --git a/example/api/index.js b/example/api/index.js index 20ee657..d24784a 100644 --- a/example/api/index.js +++ b/example/api/index.js @@ -35,18 +35,22 @@ app.post('/request-register', (req, res) => { res.send(challengeResponse); }); -app.post('/register', (req, res) => { - const { key, challenge } = parseRegisterRequest(req.body); +app.post('/register', async (req, res) => { + try { + const { key, challenge } = await parseRegisterRequest(req.body); - const user = userRepository.findByChallenge(challenge); + const user = userRepository.findByChallenge(challenge); - if (!user) { - return res.sendStatus(400); - } + if (!user) { + return res.sendStatus(400); + } - userRepository.addKeyToUser(user, key); + userRepository.addKeyToUser(user, key); - return res.send({ loggedIn: true }); + return res.send({ loggedIn: true }); + } catch (err) { + next(err); + } }); app.post('/login', (req, res) => { diff --git a/packages/client/README.md b/packages/client/README.md index 8fd33da..af58b89 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -47,7 +47,7 @@ import { } from '@webauthn/server'; ``` -- `parseRegisterRequest`: +- `parseRegisterRequest` (async): Extract challenge and key from the register request body. The challenge allow to retrieve the user, and the key must be stored server side linked to the user. - `generateRegistrationChallenge`: Generate a challenge from a relying party and a user `{ relyingParty, user }` to be sent back to the client, in order to register @@ -63,10 +63,9 @@ See an example in example/server ## Roadmap -For now only fido-u2f and packed format are implemented +For now only android-safetynet, fido-u2f, and packed formats are implemented - Implement android-key format -- Implement android-safetynet format - Implement tpm format diff --git a/packages/server/README.md b/packages/server/README.md index 8fd33da..af58b89 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -47,7 +47,7 @@ import { } from '@webauthn/server'; ``` -- `parseRegisterRequest`: +- `parseRegisterRequest` (async): Extract challenge and key from the register request body. The challenge allow to retrieve the user, and the key must be stored server side linked to the user. - `generateRegistrationChallenge`: Generate a challenge from a relying party and a user `{ relyingParty, user }` to be sent back to the client, in order to register @@ -63,10 +63,9 @@ See an example in example/server ## Roadmap -For now only fido-u2f and packed format are implemented +For now only android-safetynet, fido-u2f, and packed formats are implemented - Implement android-key format -- Implement android-safetynet format - Implement tpm format diff --git a/packages/server/package.json b/packages/server/package.json index dabc221..77c68ef 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -13,6 +13,8 @@ }, "dependencies": { "cbor": "^5.0.1", - "jsrsasign": "^8.0.12" + "jsrsasign": "^8.0.12", + "jws": "^3.2.2", + "pem": "^1.14.3" } } diff --git a/packages/server/src/authenticatorKey/index.js b/packages/server/src/authenticatorKey/index.js index faf3a5d..7b79eaa 100644 --- a/packages/server/src/authenticatorKey/index.js +++ b/packages/server/src/authenticatorKey/index.js @@ -1,4 +1,5 @@ const { decodeAllSync } = require('cbor'); +const { parseAndroidSafetyNetKey } = require('./parseAndroidSafetyNetKey'); const { parseFidoU2FKey } = require('./parseFidoU2FKey'); const { parseFidoPackedKey } = require('./parseFidoPackedKey'); @@ -7,11 +8,15 @@ exports.getAuthenticatorKeyId = key_id => { return buffer.toString('base64'); }; -exports.parseAuthenticatorKey = credentials => { +exports.parseAuthenticatorKey = async (credentials) => { const authenticatorKeyBuffer = Buffer.from(credentials.attestationObject, 'base64'); const authenticatorKey = decodeAllSync(authenticatorKeyBuffer)[0]; + if (authenticatorKey.fmt === 'android-safetynet') { + return parseAndroidSafetyNetKey(authenticatorKey, credentials.clientDataJSON); + } + if (authenticatorKey.fmt === 'fido-u2f') { return parseFidoU2FKey(authenticatorKey, credentials.clientDataJSON); } diff --git a/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js b/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js new file mode 100644 index 0000000..0a79576 --- /dev/null +++ b/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js @@ -0,0 +1,145 @@ +const { + base64ToPem, + hash, + convertASN1toPEM, + convertCOSEPublicKeyToRawPKCSECDHAKey +} = require('../utils'); +const { decode, verify } = require('jws'); +const pem = require('pem'); + +exports.parseAndroidSafetyNetKey = async (authenticatorKey, clientDataJSON) => { + const encodedJws = authenticatorKey.attStmt.response.toString(); + const jws = decode(authenticatorKey.attStmt.response); + const payload = JSON.parse(jws.payload); + + // Check device integrity. + if (!payload.ctsProfileMatch && !payload.basicIntegrity) { + return undefined; + } + + // Verify that the nonce is identical to the hash of authenticatorData + clientDataHash. + const clientDataHash = hash( + 'SHA256', + Buffer.from(clientDataJSON, 'base64') + ); + const authAndClientData = Buffer.concat([authenticatorKey.authData, clientDataHash]); + const expectedNonce = hash('SHA256', authAndClientData).toString('base64'); + if (expectedNonce !== payload.nonce) { + return undefined; + } + + // Verify that the SafetyNet response actually came from the SafetyNet service. + const formattedCerts = jws.header.x5c.map(cert => base64ToPem(cert, 'CERTIFICATE')); + const certChain = formattedCerts.slice(0).reverse() + const leafCert = formattedCerts[0]; + try { + const leafCertInfo = await pem.promisified.readCertificateInfo(leafCert) + if (leafCertInfo.commonName !== 'attest.android.com') { + throw new Error('Certificate was not issued to attest.android.com'); + } + const verified = await pem.promisified.verifySigningChain(certChain) + if (!verified) { + throw new Error('Could not verifiy certificate signing chain') + } + } catch (err) { + return undefined; + } + + // Verify the signature of the JWS message. + try { + const leafKeyInfo = await pem.promisified.getPublicKey(leafCert) + const publicKey = leafKeyInfo.publicKey + if (!verify(encodedJws, jws.header.alg, publicKey)) { + throw new Error('Could not verify JWS signature') + } + } catch (err) { + return undefined; + } + + const authenticatorData = parseAttestationData(authenticatorKey.authData); + + const publicKey = convertCOSEPublicKeyToRawPKCSECDHAKey( + authenticatorData.COSEPublicKey + ); + + return { + fmt: 'android-safetynet', + publicKey: publicKey.toString('base64'), + counter: authenticatorData.counter, + credID: authenticatorData.credID.toString('base64'), + }; +}; + +exports.validateAndroidSafetyNetKey = ( + authenticatorDataBuffer, + key, + clientDataJSON, + base64Signature +) => { + const authenticatorData = parseAttestationData(authenticatorDataBuffer); + + if (!(authenticatorData.flags.up)) { + throw new Error('User was NOT presented durring authentication!'); + } + + const clientDataHash = hash( + 'SHA256', + Buffer.from(clientDataJSON, 'base64') + ); + const signatureBaseBuffer = Buffer.concat([ + authenticatorDataBuffer, + clientDataHash, + ]); + + const publicKey = convertASN1toPEM(Buffer.from(key.publicKey, 'base64')); + const signatureBuffer = Buffer.from(base64Signature, 'base64'); + + return createVerify('sha256') + .update(signatureBaseBuffer) + .verify(publicKey, signatureBuffer); +}; + +const parseAttestationData = buffer => { + const rpIdHash = buffer.slice(0, 32); + buffer = buffer.slice(32); + const flagsBuf = buffer.slice(0, 1); + buffer = buffer.slice(1); + const flagsInt = flagsBuf[0]; + const flags = { + up: !!(flagsInt & 0x01), + uv: !!(flagsInt & 0x04), + at: !!(flagsInt & 0x40), + ed: !!(flagsInt & 0x80), + flagsInt, + }; + + const counterBuf = buffer.slice(0, 4); + buffer = buffer.slice(4); + const counter = counterBuf.readUInt32BE(0); + + let aaguid; + let credID; + let COSEPublicKey; + + if (flags.at) { + aaguid = buffer.slice(0, 16); + buffer = buffer.slice(16); + const credIDLenBuf = buffer.slice(0, 2); + buffer = buffer.slice(2); + const credIDLen = credIDLenBuf.readUInt16BE(0); + credID = buffer.slice(0, credIDLen); + buffer = buffer.slice(credIDLen); + COSEPublicKey = buffer; + } + + return { + rpIdHash, + flagsBuf, + flags, + counter, + counterBuf, + aaguid, + credID, + COSEPublicKey, + }; +}; diff --git a/packages/server/src/login.js b/packages/server/src/login.js index fe57776..9c3254a 100644 --- a/packages/server/src/login.js +++ b/packages/server/src/login.js @@ -1,5 +1,6 @@ const { randomBase64Buffer, parseBrowserBufferString } = require('./utils'); const { getChallengeFromClientData } = require('./getChallengeFromClientData'); +const { validateAndroidSafetyNetKey } = require('./authenticatorKey/parseAndroidSafetyNetKey'); const { validateFidoPackedKey } = require('./authenticatorKey/parseFidoPackedKey'); const { validateFidoU2FKey } = require('./authenticatorKey/parseFidoU2FKey'); const { validateLoginCredentials } = require('./validation'); @@ -37,6 +38,15 @@ exports.verifyAuthenticatorAssertion = (data, key) => { 'base64' ); + if (key.fmt === 'android-safetynet') { + return validateAndroidSafetyNetKey( + authenticatorDataBuffer, + key, + data.response.clientDataJSON, + data.response.signature + ); + } + if (key.fmt === 'fido-u2f') { return validateFidoU2FKey( authenticatorDataBuffer, diff --git a/packages/server/src/registration.js b/packages/server/src/registration.js index 696c19c..84067fe 100644 --- a/packages/server/src/registration.js +++ b/packages/server/src/registration.js @@ -2,6 +2,7 @@ const { decodeAllSync } = require('cbor'); const { randomBase64Buffer } = require('./utils'); const { getChallengeFromClientData } = require('./getChallengeFromClientData'); +const { parseAndroidSafetyNetKey } = require('./authenticatorKey/parseAndroidSafetyNetKey'); const { parseFidoU2FKey } = require('./authenticatorKey/parseFidoU2FKey'); const { parseFidoPackedKey } = require('./authenticatorKey/parseFidoPackedKey'); const { validateRegistrationCredentials } = require('./validation'); @@ -13,6 +14,13 @@ const parseAuthenticatorKey = (webAuthnResponse) => { ); const authenticatorKey = decodeAllSync(authenticatorKeyBuffer)[0]; + if (authenticatorKey.fmt === 'android-safetynet') { + return parseAndroidSafetyNetKey( + authenticatorKey, + webAuthnResponse.clientDataJSON + ); + } + if (authenticatorKey.fmt === 'fido-u2f') { return parseFidoU2FKey( authenticatorKey, @@ -30,12 +38,12 @@ const parseAuthenticatorKey = (webAuthnResponse) => { return undefined; }; -exports.parseRegisterRequest = (body) => { +exports.parseRegisterRequest = async (body) => { if (!validateRegistrationCredentials(body)) { return {}; } const challenge = getChallengeFromClientData(body.response.clientDataJSON); - const key = parseAuthenticatorKey(body.response); + const key = await parseAuthenticatorKey(body.response); return { challenge, diff --git a/packages/server/src/utils.js b/packages/server/src/utils.js index 5cd831d..c910015 100644 --- a/packages/server/src/utils.js +++ b/packages/server/src/utils.js @@ -68,7 +68,7 @@ exports.convertASN1toPEM = pkBuffer => { * @return {Buffer} - RAW PKCS encoded public key */ exports.convertCOSEPublicKeyToRawPKCSECDHAKey = cosePublicKey => { - /* + /* +------+-------+-------+---------+----------------------------------+ | name | key | label | type | description | | | type | | | | @@ -117,3 +117,18 @@ exports.parseBrowserBufferString = (key_id) => { const buffer = Buffer.from(key_id, 'base64'); return buffer.toString('base64'); }; + +/** + * Convert raw base64-encoded string to multiline PEM. + */ +exports.base64ToPem = (str, type = 'CERTIFICATE') => { + const split = str.match(/.{1,65}/g).join('\n'); // split into 65-character lines + return `-----BEGIN ${type}-----\n${split}\n-----END ${type}-----\n`; +}; + +/** + * Convert multiline PEM to raw base64-encoded string. + */ +exports.pemToBase64 = (pem) => { + return pem.replace(/-----(BEGIN|END) [A-Z ]+-----/g, '').replace(/\n/g, ''); +}; diff --git a/yarn.lock b/yarn.lock index 9a18f89..265f45a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2104,6 +2104,11 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= +buffer-equal-constant-time@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" + integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= + buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -2277,6 +2282,11 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +charenc@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" + integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= + chokidar@^2.0.2, chokidar@^2.1.5: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2739,6 +2749,11 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" +crypt@~0.0.1: + version "0.0.2" + resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" + integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= + crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -3018,6 +3033,13 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" +ecdsa-sig-formatter@1.0.11: + version "1.0.11" + resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" + integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== + dependencies: + safe-buffer "^5.0.1" + ecstatic@^3.0.0: version "3.3.2" resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.2.tgz#6d1dd49814d00594682c652adb66076a69d46c48" @@ -3159,6 +3181,11 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" +es6-promisify@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.0.2.tgz#525c23725b8510f5f1f2feb5a1fbad93a93e29b4" + integrity sha512-eO6vFm0JvqGzjWIQA6QVKjxpmELfhWbDUWHm1rPfIbn55mhKPiAa5xpLmQWJrNa629ZIeQ8ZvMAi13kvrjK6Mg== + escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -4219,7 +4246,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5: +is-buffer@^1.1.5, is-buffer@~1.1.1: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -4580,6 +4607,23 @@ jsrsasign@^8.0.12: resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.12.tgz#22abb9656d34a30b9530436720835e89c2e5c316" integrity sha1-Iqu5ZW00owuVMENnIINeicLlwxY= +jwa@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" + integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== + dependencies: + buffer-equal-constant-time "1.0.1" + ecdsa-sig-formatter "1.0.11" + safe-buffer "^5.0.1" + +jws@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" + integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== + dependencies: + jwa "^1.4.1" + safe-buffer "^5.0.1" + kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" @@ -4873,6 +4917,15 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" +md5@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" + integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= + dependencies: + charenc "~0.0.1" + crypt "~0.0.1" + is-buffer "~1.1.1" + media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5556,7 +5609,7 @@ os-name@^3.1.0: macos-release "^2.2.0" windows-release "^3.1.0" -os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -5821,6 +5874,16 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" +pem@^1.14.3: + version "1.14.3" + resolved "https://registry.yarnpkg.com/pem/-/pem-1.14.3.tgz#347e5a5c194a5f7612b88083e45042fcc4fb4901" + integrity sha512-Q+AMVMD3fzeVvZs5PHeI+pVt0hgZY2fjhkliBW43qyONLgCXPVk1ryim43F9eupHlNGLJNT5T/NNrzhUdiC5Zg== + dependencies: + es6-promisify "^6.0.0" + md5 "^2.2.1" + os-tmpdir "^1.0.1" + which "^1.3.1" + performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" From 266d262984892683b17db20172985d795f6b52a9 Mon Sep 17 00:00:00 2001 From: Eric Heikes Date: Tue, 14 Jan 2020 23:25:52 -0800 Subject: [PATCH 2/3] Make parseAuthenticatorKey synchronous again Credit to @tnokin for the code to make this possible. --- README.md | 2 +- example/api/index.js | 20 ++--- packages/client/README.md | 2 +- packages/server/README.md | 2 +- packages/server/package.json | 3 +- packages/server/src/authenticatorKey/index.js | 2 +- .../parseAndroidSafetyNetKey.js | 88 +++++++++++++++---- packages/server/src/registration.js | 4 +- yarn.lock | 40 +-------- 9 files changed, 87 insertions(+), 76 deletions(-) diff --git a/README.md b/README.md index af58b89..41cd455 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ import { } from '@webauthn/server'; ``` -- `parseRegisterRequest` (async): +- `parseRegisterRequest`: Extract challenge and key from the register request body. The challenge allow to retrieve the user, and the key must be stored server side linked to the user. - `generateRegistrationChallenge`: Generate a challenge from a relying party and a user `{ relyingParty, user }` to be sent back to the client, in order to register diff --git a/example/api/index.js b/example/api/index.js index d24784a..20ee657 100644 --- a/example/api/index.js +++ b/example/api/index.js @@ -35,22 +35,18 @@ app.post('/request-register', (req, res) => { res.send(challengeResponse); }); -app.post('/register', async (req, res) => { - try { - const { key, challenge } = await parseRegisterRequest(req.body); +app.post('/register', (req, res) => { + const { key, challenge } = parseRegisterRequest(req.body); - const user = userRepository.findByChallenge(challenge); + const user = userRepository.findByChallenge(challenge); - if (!user) { - return res.sendStatus(400); - } + if (!user) { + return res.sendStatus(400); + } - userRepository.addKeyToUser(user, key); + userRepository.addKeyToUser(user, key); - return res.send({ loggedIn: true }); - } catch (err) { - next(err); - } + return res.send({ loggedIn: true }); }); app.post('/login', (req, res) => { diff --git a/packages/client/README.md b/packages/client/README.md index af58b89..41cd455 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -47,7 +47,7 @@ import { } from '@webauthn/server'; ``` -- `parseRegisterRequest` (async): +- `parseRegisterRequest`: Extract challenge and key from the register request body. The challenge allow to retrieve the user, and the key must be stored server side linked to the user. - `generateRegistrationChallenge`: Generate a challenge from a relying party and a user `{ relyingParty, user }` to be sent back to the client, in order to register diff --git a/packages/server/README.md b/packages/server/README.md index af58b89..41cd455 100644 --- a/packages/server/README.md +++ b/packages/server/README.md @@ -47,7 +47,7 @@ import { } from '@webauthn/server'; ``` -- `parseRegisterRequest` (async): +- `parseRegisterRequest`: Extract challenge and key from the register request body. The challenge allow to retrieve the user, and the key must be stored server side linked to the user. - `generateRegistrationChallenge`: Generate a challenge from a relying party and a user `{ relyingParty, user }` to be sent back to the client, in order to register diff --git a/packages/server/package.json b/packages/server/package.json index 77c68ef..fdd053d 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -14,7 +14,6 @@ "dependencies": { "cbor": "^5.0.1", "jsrsasign": "^8.0.12", - "jws": "^3.2.2", - "pem": "^1.14.3" + "jws": "^3.2.2" } } diff --git a/packages/server/src/authenticatorKey/index.js b/packages/server/src/authenticatorKey/index.js index 7b79eaa..c3f0921 100644 --- a/packages/server/src/authenticatorKey/index.js +++ b/packages/server/src/authenticatorKey/index.js @@ -8,7 +8,7 @@ exports.getAuthenticatorKeyId = key_id => { return buffer.toString('base64'); }; -exports.parseAuthenticatorKey = async (credentials) => { +exports.parseAuthenticatorKey = credentials => { const authenticatorKeyBuffer = Buffer.from(credentials.attestationObject, 'base64'); const authenticatorKey = decodeAllSync(authenticatorKeyBuffer)[0]; diff --git a/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js b/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js index 0a79576..36bae41 100644 --- a/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js +++ b/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js @@ -4,10 +4,66 @@ const { convertASN1toPEM, convertCOSEPublicKeyToRawPKCSECDHAKey } = require('../utils'); +const { createVerify } = require('crypto'); +const jsrsasign = require('jsrsasign'); const { decode, verify } = require('jws'); -const pem = require('pem'); -exports.parseAndroidSafetyNetKey = async (authenticatorKey, clientDataJSON) => { +const gsr2 = 'MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg=='; + + +const getCertificateSubject = (certificate) => { + const subjectCert = new jsrsasign.X509(); + subjectCert.readCertPEM(certificate); + + const subjectString = subjectCert.getSubjectString(); + const subjectFields = subjectString.slice(1).split('/'); + + let fields = {}; + for (let field of subjectFields) { + const [ key, val ] = field.split('='); + fields[key] = val; + } + + return fields; +} + +const verifySigningChain = (certificates) => { + if ((new Set(certificates)).size !== certificates.length) { + throw new Error('Failed to validate certificates path! Duplicate certificates detected!'); + } + + certificates.forEach((subjectPem, i) => { + const subjectCert = new jsrsasign.X509(); + subjectCert.readCertPEM(subjectPem); + + let issuerPem = ''; + if (i + 1 >= certificates.length) + issuerPem = subjectPem; + else + issuerPem = certificates[i + 1]; + + const issuerCert = new jsrsasign.X509(); + issuerCert.readCertPEM(issuerPem); + + if (subjectCert.getIssuerString() !== issuerCert.getSubjectString()) { + throw new Error(`Failed to validate certificate path! Issuers don't match!`); + } + + const subjectCertStruct = jsrsasign.ASN1HEX.getTLVbyList(subjectCert.hex, 0, [0]); + const algorithm = subjectCert.getSignatureAlgorithmField(); + const signatureHex = subjectCert.getSignatureValueHex(); + + const signature = new jsrsasign.crypto.Signature({ alg: algorithm }); + signature.init(issuerPem); + signature.updateHex(subjectCertStruct); + + if (!signature.verify(signatureHex)) { + throw new Error('Failed to validate certificate path! Signature is not valid!'); + } + }) +} + +exports.parseAndroidSafetyNetKey = (authenticatorKey, clientDataJSON) => { const encodedJws = authenticatorKey.attStmt.response.toString(); const jws = decode(authenticatorKey.attStmt.response); const payload = JSON.parse(jws.payload); @@ -29,30 +85,24 @@ exports.parseAndroidSafetyNetKey = async (authenticatorKey, clientDataJSON) => { } // Verify that the SafetyNet response actually came from the SafetyNet service. - const formattedCerts = jws.header.x5c.map(cert => base64ToPem(cert, 'CERTIFICATE')); - const certChain = formattedCerts.slice(0).reverse() + const formattedCerts = jws.header.x5c.concat([gsr2]).map(cert => base64ToPem(cert, 'CERTIFICATE')); const leafCert = formattedCerts[0]; + const subject = getCertificateSubject(leafCert); + if (subject.CN !== 'attest.android.com') { + return undefined; + } try { - const leafCertInfo = await pem.promisified.readCertificateInfo(leafCert) - if (leafCertInfo.commonName !== 'attest.android.com') { - throw new Error('Certificate was not issued to attest.android.com'); - } - const verified = await pem.promisified.verifySigningChain(certChain) - if (!verified) { - throw new Error('Could not verifiy certificate signing chain') - } + verifySigningChain(formattedCerts); } catch (err) { return undefined; } // Verify the signature of the JWS message. - try { - const leafKeyInfo = await pem.promisified.getPublicKey(leafCert) - const publicKey = leafKeyInfo.publicKey - if (!verify(encodedJws, jws.header.alg, publicKey)) { - throw new Error('Could not verify JWS signature') - } - } catch (err) { + const leafCertX509 = new jsrsasign.X509(); + leafCertX509.readCertPEM(leafCert); + const leafPublicKey = Buffer.from(leafCertX509.getPublicKeyHex(), 'hex').toString('base64'); + const leafPublicKeyPem = base64ToPem(leafPublicKey, 'PUBLIC KEY'); + if (!verify(encodedJws, jws.header.alg, leafPublicKeyPem)) { return undefined; } diff --git a/packages/server/src/registration.js b/packages/server/src/registration.js index 84067fe..fe5e64f 100644 --- a/packages/server/src/registration.js +++ b/packages/server/src/registration.js @@ -38,12 +38,12 @@ const parseAuthenticatorKey = (webAuthnResponse) => { return undefined; }; -exports.parseRegisterRequest = async (body) => { +exports.parseRegisterRequest = (body) => { if (!validateRegistrationCredentials(body)) { return {}; } const challenge = getChallengeFromClientData(body.response.clientDataJSON); - const key = await parseAuthenticatorKey(body.response); + const key = parseAuthenticatorKey(body.response); return { challenge, diff --git a/yarn.lock b/yarn.lock index 265f45a..6312575 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2282,11 +2282,6 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== -charenc@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/charenc/-/charenc-0.0.2.tgz#c0a1d2f3a7092e03774bfa83f14c0fc5790a8667" - integrity sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc= - chokidar@^2.0.2, chokidar@^2.1.5: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -2749,11 +2744,6 @@ cross-spawn@^5.0.1: shebang-command "^1.2.0" which "^1.2.9" -crypt@~0.0.1: - version "0.0.2" - resolved "https://registry.yarnpkg.com/crypt/-/crypt-0.0.2.tgz#88d7ff7ec0dfb86f713dc87bbb42d044d3e6c41b" - integrity sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs= - crypto-browserify@^3.11.0: version "3.12.0" resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.12.0.tgz#396cf9f3137f03e4b8e532c58f698254e00f80ec" @@ -3181,11 +3171,6 @@ es6-promisify@^5.0.0: dependencies: es6-promise "^4.0.3" -es6-promisify@^6.0.0: - version "6.0.2" - resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-6.0.2.tgz#525c23725b8510f5f1f2feb5a1fbad93a93e29b4" - integrity sha512-eO6vFm0JvqGzjWIQA6QVKjxpmELfhWbDUWHm1rPfIbn55mhKPiAa5xpLmQWJrNa629ZIeQ8ZvMAi13kvrjK6Mg== - escape-html@~1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" @@ -4040,7 +4025,7 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM= -https-proxy-agent@^2.2.1, https-proxy-agent@^2.2.3: +https-proxy-agent@^2.2.1: version "2.2.4" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz#4ee7a737abd92678a293d9b34a1af4d0d08c787b" integrity sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg== @@ -4246,7 +4231,7 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" -is-buffer@^1.1.5, is-buffer@~1.1.1: +is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" integrity sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w== @@ -4917,15 +4902,6 @@ md5.js@^1.3.4: inherits "^2.0.1" safe-buffer "^5.1.2" -md5@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/md5/-/md5-2.2.1.tgz#53ab38d5fe3c8891ba465329ea23fac0540126f9" - integrity sha1-U6s41f48iJG6RlMp6iP6wFQBJvk= - dependencies: - charenc "~0.0.1" - crypt "~0.0.1" - is-buffer "~1.1.1" - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -5609,7 +5585,7 @@ os-name@^3.1.0: macos-release "^2.2.0" windows-release "^3.1.0" -os-tmpdir@^1.0.0, os-tmpdir@^1.0.1, os-tmpdir@~1.0.2: +os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= @@ -5874,16 +5850,6 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" -pem@^1.14.3: - version "1.14.3" - resolved "https://registry.yarnpkg.com/pem/-/pem-1.14.3.tgz#347e5a5c194a5f7612b88083e45042fcc4fb4901" - integrity sha512-Q+AMVMD3fzeVvZs5PHeI+pVt0hgZY2fjhkliBW43qyONLgCXPVk1ryim43F9eupHlNGLJNT5T/NNrzhUdiC5Zg== - dependencies: - es6-promisify "^6.0.0" - md5 "^2.2.1" - os-tmpdir "^1.0.1" - which "^1.3.1" - performance-now@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" From a5968e71b1d323bbabe206f204a4b4d5f3fc47da Mon Sep 17 00:00:00 2001 From: Eric Heikes Date: Wed, 15 Jan 2020 21:46:29 -0800 Subject: [PATCH 3/3] Remove jws module for jsrsasign and other methods --- packages/server/package.json | 3 +- .../parseAndroidSafetyNetKey.js | 14 +++++---- yarn.lock | 29 ------------------- 3 files changed, 9 insertions(+), 37 deletions(-) diff --git a/packages/server/package.json b/packages/server/package.json index fdd053d..dabc221 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -13,7 +13,6 @@ }, "dependencies": { "cbor": "^5.0.1", - "jsrsasign": "^8.0.12", - "jws": "^3.2.2" + "jsrsasign": "^8.0.12" } } diff --git a/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js b/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js index 36bae41..f2c478d 100644 --- a/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js +++ b/packages/server/src/authenticatorKey/parseAndroidSafetyNetKey.js @@ -6,7 +6,6 @@ const { } = require('../utils'); const { createVerify } = require('crypto'); const jsrsasign = require('jsrsasign'); -const { decode, verify } = require('jws'); const gsr2 = 'MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPLv4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklqtTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzdC9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pazq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCBmTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IHV2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5nbG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4GsJ0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavSot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxdAfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg=='; @@ -65,11 +64,14 @@ const verifySigningChain = (certificates) => { exports.parseAndroidSafetyNetKey = (authenticatorKey, clientDataJSON) => { const encodedJws = authenticatorKey.attStmt.response.toString(); - const jws = decode(authenticatorKey.attStmt.response); - const payload = JSON.parse(jws.payload); + const jwsParts = encodedJws.split('.'); + const jws = { + header: JSON.parse(Buffer.from(jwsParts[0], 'base64').toString()), + payload: JSON.parse(Buffer.from(jwsParts[1], 'base64').toString()) + } // Check device integrity. - if (!payload.ctsProfileMatch && !payload.basicIntegrity) { + if (!jws.payload.ctsProfileMatch && !jws.payload.basicIntegrity) { return undefined; } @@ -80,7 +82,7 @@ exports.parseAndroidSafetyNetKey = (authenticatorKey, clientDataJSON) => { ); const authAndClientData = Buffer.concat([authenticatorKey.authData, clientDataHash]); const expectedNonce = hash('SHA256', authAndClientData).toString('base64'); - if (expectedNonce !== payload.nonce) { + if (expectedNonce !== jws.payload.nonce) { return undefined; } @@ -102,7 +104,7 @@ exports.parseAndroidSafetyNetKey = (authenticatorKey, clientDataJSON) => { leafCertX509.readCertPEM(leafCert); const leafPublicKey = Buffer.from(leafCertX509.getPublicKeyHex(), 'hex').toString('base64'); const leafPublicKeyPem = base64ToPem(leafPublicKey, 'PUBLIC KEY'); - if (!verify(encodedJws, jws.header.alg, leafPublicKeyPem)) { + if (!jsrsasign.jws.JWS.verify(encodedJws, leafPublicKeyPem)) { return undefined; } diff --git a/yarn.lock b/yarn.lock index 6312575..7ac4d87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2104,11 +2104,6 @@ btoa-lite@^1.0.0: resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= -buffer-equal-constant-time@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819" - integrity sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk= - buffer-from@^1.0.0: version "1.1.1" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" @@ -3023,13 +3018,6 @@ ecc-jsbn@~0.1.1: jsbn "~0.1.0" safer-buffer "^2.1.0" -ecdsa-sig-formatter@1.0.11: - version "1.0.11" - resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf" - integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ== - dependencies: - safe-buffer "^5.0.1" - ecstatic@^3.0.0: version "3.3.2" resolved "https://registry.yarnpkg.com/ecstatic/-/ecstatic-3.3.2.tgz#6d1dd49814d00594682c652adb66076a69d46c48" @@ -4592,23 +4580,6 @@ jsrsasign@^8.0.12: resolved "https://registry.yarnpkg.com/jsrsasign/-/jsrsasign-8.0.12.tgz#22abb9656d34a30b9530436720835e89c2e5c316" integrity sha1-Iqu5ZW00owuVMENnIINeicLlwxY= -jwa@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a" - integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA== - dependencies: - buffer-equal-constant-time "1.0.1" - ecdsa-sig-formatter "1.0.11" - safe-buffer "^5.0.1" - -jws@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304" - integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA== - dependencies: - jwa "^1.4.1" - safe-buffer "^5.0.1" - kind-of@^3.0.2, kind-of@^3.0.3, kind-of@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64"