diff --git a/interop-test-files/crypto/signature-fixtures.json b/interop-test-files/crypto/signature-fixtures.json index 917c6d0245..7cdeb55ea7 100644 --- a/interop-test-files/crypto/signature-fixtures.json +++ b/interop-test-files/crypto/signature-fixtures.json @@ -7,7 +7,8 @@ "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoJWExHptCfduPleDbG3rko3YZnn9Lw0IjpixVmexJDegg", - "validSignature": true + "validSignature": true, + "tags": [] }, { "comment": "valid K-256 key and signature, with low-S signature", @@ -17,7 +18,8 @@ "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdUn/FEznOndsz/qgiYb89zwxYCbB71f7yQK5Lr7NasfoA", - "validSignature": true + "validSignature": true, + "tags": [] }, { "comment": "P-256 key and signature, with non-low-S signature which is invalid in atproto", @@ -27,7 +29,8 @@ "publicKeyDid": "did:key:zDnaembgSGUhZULN2Caob4HLJPaxBh92N7rtH21TErzqf8HQo", "publicKeyMultibase": "zxdM8dSstjrpZaRUwBmDvjGXweKuEMVN95A9oJBFjkWMh", "signatureBase64": "2vZNsG3UKvvO/CDlrdvyZRISOFylinBh0Jupc6KcWoKp7O4VS9giSAah8k5IUbXIW00SuOrjfEqQ9HEkN9JGzw", - "validSignature": false + "validSignature": false, + "tags": ["high-s"] }, { "comment": "K-256 key and signature, with non-low-S signature which is invalid in atproto", @@ -37,6 +40,7 @@ "publicKeyDid": "did:key:zQ3shqwJEJyMBsBXCWyCBpUBMqxcon9oHB7mCvx4sSpMdLJwc", "publicKeyMultibase": "z25z9DTpsiYYJKGsWmSPJK2NFN8PcJtZig12K59UgW7q5t", "signatureBase64": "5WpdIuEUUfVUYaozsi8G0B3cWO09cgZbIIwg1t2YKdXYA67MYxYiTMAVfdnkDCMN9S5B3vHosRe07aORmoshoQ", - "validSignature": false + "validSignature": false, + "tags": ["high-s"] } ] diff --git a/packages/bsky/src/auth.ts b/packages/bsky/src/auth.ts index 290ef3c7a4..b19e6860e5 100644 --- a/packages/bsky/src/auth.ts +++ b/packages/bsky/src/auth.ts @@ -14,10 +14,17 @@ export const authVerifier = if (!jwtStr) { throw new AuthRequiredError('missing jwt', 'MissingJwt') } - const payload = await verifyJwt(jwtStr, opts.aud, async (did: string) => { - const atprotoData = await idResolver.did.resolveAtprotoData(did) - return atprotoData.signingKey - }) + const payload = await verifyJwt( + jwtStr, + opts.aud, + async (did, forceRefresh) => { + const atprotoData = await idResolver.did.resolveAtprotoData( + did, + forceRefresh, + ) + return atprotoData.signingKey + }, + ) return { credentials: { did: payload.iss }, artifacts: { aud: opts.aud } } } diff --git a/packages/bsky/tests/auth.test.ts b/packages/bsky/tests/auth.test.ts new file mode 100644 index 0000000000..a3ac3d1d9f --- /dev/null +++ b/packages/bsky/tests/auth.test.ts @@ -0,0 +1,64 @@ +import AtpAgent from '@atproto/api' +import { SeedClient, TestNetwork } from '@atproto/dev-env' +import usersSeed from './seeds/users' +import { createServiceJwt } from '@atproto/xrpc-server' +import { Keypair, Secp256k1Keypair } from '@atproto/crypto' + +describe('auth', () => { + let network: TestNetwork + let agent: AtpAgent + let sc: SeedClient + + beforeAll(async () => { + network = await TestNetwork.create({ + dbPostgresSchema: 'bsky_auth', + }) + agent = network.bsky.getClient() + sc = network.getSeedClient() + await usersSeed(sc) + await network.processAll() + }) + + afterAll(async () => { + await network.close() + }) + + it('handles signing key change for service auth.', async () => { + const issuer = sc.dids.alice + const attemptWithKey = async (keypair: Keypair) => { + const jwt = await createServiceJwt({ + iss: issuer, + aud: network.bsky.ctx.cfg.serverDid, + keypair, + }) + return agent.api.app.bsky.actor.getProfile( + { actor: sc.dids.carol }, + { headers: { authorization: `Bearer ${jwt}` } }, + ) + } + const origSigningKey = network.pds.ctx.repoSigningKey + const newSigningKey = await Secp256k1Keypair.create({ exportable: true }) + // confirm original signing key works + await expect(attemptWithKey(origSigningKey)).resolves.toBeDefined() + // confirm next signing key doesn't work yet + await expect(attemptWithKey(newSigningKey)).rejects.toThrow( + 'jwt signature does not match jwt issuer', + ) + // update to new signing key + await network.plc + .getClient() + .updateAtprotoKey( + issuer, + network.pds.ctx.plcRotationKey, + newSigningKey.did(), + ) + // old signing key still works due to did doc cache + await expect(attemptWithKey(origSigningKey)).resolves.toBeDefined() + // new signing key works + await expect(attemptWithKey(newSigningKey)).resolves.toBeDefined() + // old signing key no longer works after cache is updated + await expect(attemptWithKey(origSigningKey)).rejects.toThrow( + 'jwt signature does not match jwt issuer', + ) + }) +}) diff --git a/packages/crypto/src/p256/operations.ts b/packages/crypto/src/p256/operations.ts index f5292f4bd8..6f81b0371a 100644 --- a/packages/crypto/src/p256/operations.ts +++ b/packages/crypto/src/p256/operations.ts @@ -2,24 +2,29 @@ import { p256 } from '@noble/curves/p256' import { sha256 } from '@noble/hashes/sha256' import { P256_JWT_ALG } from '../const' import { parseDidKey } from '../did' +import { VerifyOptions } from '../types' export const verifyDidSig = async ( did: string, data: Uint8Array, sig: Uint8Array, + opts?: VerifyOptions, ): Promise => { const { jwtAlg, keyBytes } = parseDidKey(did) if (jwtAlg !== P256_JWT_ALG) { throw new Error(`Not a P-256 did:key: ${did}`) } - return verifySig(keyBytes, data, sig) + return verifySig(keyBytes, data, sig, opts) } export const verifySig = async ( publicKey: Uint8Array, data: Uint8Array, sig: Uint8Array, + opts?: VerifyOptions, ): Promise => { const msgHash = await sha256(data) - return p256.verify(sig, msgHash, publicKey, { lowS: true }) + return p256.verify(sig, msgHash, publicKey, { + lowS: opts?.lowS ?? true, + }) } diff --git a/packages/crypto/src/secp256k1/operations.ts b/packages/crypto/src/secp256k1/operations.ts index 5d31a81250..f470c2da54 100644 --- a/packages/crypto/src/secp256k1/operations.ts +++ b/packages/crypto/src/secp256k1/operations.ts @@ -2,24 +2,29 @@ import { secp256k1 as k256 } from '@noble/curves/secp256k1' import { sha256 } from '@noble/hashes/sha256' import { SECP256K1_JWT_ALG } from '../const' import { parseDidKey } from '../did' +import { VerifyOptions } from '../types' export const verifyDidSig = async ( did: string, data: Uint8Array, sig: Uint8Array, + opts?: VerifyOptions, ): Promise => { const { jwtAlg, keyBytes } = parseDidKey(did) if (jwtAlg !== SECP256K1_JWT_ALG) { throw new Error(`Not a secp256k1 did:key: ${did}`) } - return verifySig(keyBytes, data, sig) + return verifySig(keyBytes, data, sig, opts) } export const verifySig = async ( publicKey: Uint8Array, data: Uint8Array, sig: Uint8Array, + opts?: VerifyOptions, ): Promise => { const msgHash = await sha256(data) - return k256.verify(sig, msgHash, publicKey, { lowS: true }) + return k256.verify(sig, msgHash, publicKey, { + lowS: opts?.lowS ?? true, + }) } diff --git a/packages/crypto/src/types.ts b/packages/crypto/src/types.ts index e8cbdc57b6..a1089134f0 100644 --- a/packages/crypto/src/types.ts +++ b/packages/crypto/src/types.ts @@ -16,5 +16,10 @@ export type DidKeyPlugin = { did: string, msg: Uint8Array, data: Uint8Array, + opts?: VerifyOptions, ) => Promise } + +export type VerifyOptions = { + lowS?: boolean +} diff --git a/packages/crypto/src/verify.ts b/packages/crypto/src/verify.ts index 43b2670c7c..50ba87aba2 100644 --- a/packages/crypto/src/verify.ts +++ b/packages/crypto/src/verify.ts @@ -1,26 +1,29 @@ import * as uint8arrays from 'uint8arrays' import { parseDidKey } from './did' import plugins from './plugins' +import { VerifyOptions } from './types' export const verifySignature = ( didKey: string, data: Uint8Array, sig: Uint8Array, + opts?: VerifyOptions, ): Promise => { const parsed = parseDidKey(didKey) const plugin = plugins.find((p) => p.jwtAlg === parsed.jwtAlg) if (!plugin) { - throw new Error(`Unsupported signature alg: :${parsed.jwtAlg}`) + throw new Error(`Unsupported signature alg: ${parsed.jwtAlg}`) } - return plugin.verifySignature(didKey, data, sig) + return plugin.verifySignature(didKey, data, sig, opts) } export const verifySignatureUtf8 = async ( didKey: string, data: string, sig: string, + opts?: VerifyOptions, ): Promise => { const dataBytes = uint8arrays.fromString(data, 'utf8') const sigBytes = uint8arrays.fromString(sig, 'base64url') - return verifySignature(didKey, dataBytes, sigBytes) + return verifySignature(didKey, dataBytes, sigBytes, opts) } diff --git a/packages/crypto/tests/signatures.test.ts b/packages/crypto/tests/signatures.test.ts index cebc8126b3..83d2b6b72f 100644 --- a/packages/crypto/tests/signatures.test.ts +++ b/packages/crypto/tests/signatures.test.ts @@ -57,6 +57,45 @@ describe('signatures', () => { } } }) + + it('verifies high-s signatures with explicit option', async () => { + const highSVectors = vectors.filter((vec) => vec.tags.includes('high-s')) + expect(highSVectors.length).toBeGreaterThanOrEqual(2) + for (const vector of highSVectors) { + const messageBytes = uint8arrays.fromString( + vector.messageBase64, + 'base64', + ) + const signatureBytes = uint8arrays.fromString( + vector.signatureBase64, + 'base64', + ) + const keyBytes = multibaseToBytes(vector.publicKeyMultibase) + const didKey = parseDidKey(vector.publicKeyDid) + expect(uint8arrays.equals(keyBytes, didKey.keyBytes)) + if (vector.algorithm === P256_JWT_ALG) { + const verified = await p256.verifySig( + keyBytes, + messageBytes, + signatureBytes, + { lowS: false }, + ) + expect(verified).toEqual(true) + expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement + } else if (vector.algorithm === SECP256K1_JWT_ALG) { + const verified = await secp.verifySig( + keyBytes, + messageBytes, + signatureBytes, + { lowS: false }, + ) + expect(verified).toEqual(true) + expect(vector.validSignature).toEqual(false) // otherwise would fail per low-s requirement + } else { + throw new Error('Unsupported test vector') + } + } + }) }) // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -79,6 +118,7 @@ async function generateTestVectors(): Promise { 'base64', ), validSignature: true, + tags: [], }, { messageBase64, @@ -93,6 +133,7 @@ async function generateTestVectors(): Promise { 'base64', ), validSignature: true, + tags: [], }, // these vectors test to ensure we don't allow high-s signatures { @@ -109,6 +150,7 @@ async function generateTestVectors(): Promise { P256_JWT_ALG, ), validSignature: false, + tags: ['high-s'], }, { messageBase64, @@ -124,6 +166,7 @@ async function generateTestVectors(): Promise { SECP256K1_JWT_ALG, ), validSignature: false, + tags: ['high-s'], }, ] } @@ -159,4 +202,5 @@ type TestVector = { messageBase64: string signatureBase64: string validSignature: boolean + tags: string[] } diff --git a/packages/xrpc-server/package.json b/packages/xrpc-server/package.json index d32319f301..21589c26f6 100644 --- a/packages/xrpc-server/package.json +++ b/packages/xrpc-server/package.json @@ -45,6 +45,8 @@ "@types/http-errors": "^2.0.1", "@types/ws": "^8.5.4", "get-port": "^6.1.2", + "jose": "^4.15.4", + "key-encoder": "^2.0.3", "multiformats": "^9.9.0" } } diff --git a/packages/xrpc-server/src/auth.ts b/packages/xrpc-server/src/auth.ts index 9283b13815..0b0cbe0312 100644 --- a/packages/xrpc-server/src/auth.ts +++ b/packages/xrpc-server/src/auth.ts @@ -48,7 +48,7 @@ const jsonToB64Url = (json: Record): string => { export const verifyJwt = async ( jwtStr: string, ownDid: string | null, // null indicates to skip the audience check - getSigningKey: (did: string) => Promise, + getSigningKey: (did: string, forceRefresh: boolean) => Promise, ): Promise => { const parts = jwtStr.split('.') if (parts.length !== 3) { @@ -69,18 +69,40 @@ export const verifyJwt = async ( const msgBytes = ui8.fromString(parts.slice(0, 2).join('.'), 'utf8') const sigBytes = ui8.fromString(sig, 'base64url') + const verifySignatureWithKey = (key: string) => { + return crypto.verifySignature(key, msgBytes, sigBytes, { + lowS: false, + }) + } - const signingKey = await getSigningKey(payload.iss) + const signingKey = await getSigningKey(payload.iss, false) let validSig: boolean try { - validSig = await crypto.verifySignature(signingKey, msgBytes, sigBytes) + validSig = await verifySignatureWithKey(signingKey) } catch (err) { throw new AuthRequiredError( 'could not verify jwt signature', 'BadJwtSignature', ) } + + if (!validSig) { + // get fresh signing key in case it failed due to a recent rotation + const freshSigningKey = await getSigningKey(payload.iss, true) + try { + validSig = + freshSigningKey !== signingKey + ? await verifySignatureWithKey(freshSigningKey) + : false + } catch (err) { + throw new AuthRequiredError( + 'could not verify jwt signature', + 'BadJwtSignature', + ) + } + } + if (!validSig) { throw new AuthRequiredError( 'jwt signature does not match jwt issuer', diff --git a/packages/xrpc-server/tests/auth.test.ts b/packages/xrpc-server/tests/auth.test.ts index d36c05b6c3..53f3a6c6d2 100644 --- a/packages/xrpc-server/tests/auth.test.ts +++ b/packages/xrpc-server/tests/auth.test.ts @@ -1,5 +1,11 @@ -import * as http from 'http' +import * as http from 'node:http' +import { KeyObject, createPrivateKey } from 'node:crypto' import getPort from 'get-port' +import * as jose from 'jose' +import KeyEncoder from 'key-encoder' +import * as ui8 from 'uint8arrays' +import { MINUTE } from '@atproto/common' +import { Secp256k1Keypair } from '@atproto/crypto' import xrpc, { ServiceClient, XRPCError } from '@atproto/xrpc' import * as xrpcServer from '../src' import { @@ -131,4 +137,104 @@ describe('Auth', () => { original: 'YWRtaW46cGFzc3dvcmQ=', }) }) + + describe('verifyJwt()', () => { + it('fails on expired jwt.', async () => { + const keypair = await Secp256k1Keypair.create() + const jwt = await xrpcServer.createServiceJwt({ + aud: 'did:example:aud', + iss: 'did:example:iss', + keypair, + exp: Math.floor((Date.now() - MINUTE) / 1000), + }) + const tryVerify = xrpcServer.verifyJwt( + jwt, + 'did:example:aud', + async () => { + return keypair.did() + }, + ) + await expect(tryVerify).rejects.toThrow('jwt expired') + }) + + it('fails on bad audience.', async () => { + const keypair = await Secp256k1Keypair.create() + const jwt = await xrpcServer.createServiceJwt({ + aud: 'did:example:aud1', + iss: 'did:example:iss', + keypair, + }) + const tryVerify = xrpcServer.verifyJwt( + jwt, + 'did:example:aud2', + async () => { + return keypair.did() + }, + ) + await expect(tryVerify).rejects.toThrow( + 'jwt audience does not match service did', + ) + }) + + it('refreshes key on verification failure.', async () => { + const keypair1 = await Secp256k1Keypair.create() + const keypair2 = await Secp256k1Keypair.create() + const jwt = await xrpcServer.createServiceJwt({ + aud: 'did:example:aud', + iss: 'did:example:iss', + keypair: keypair2, + }) + let usedKeypair1 = false + let usedKeypair2 = false + const tryVerify = xrpcServer.verifyJwt( + jwt, + 'did:example:aud', + async (_did, forceRefresh) => { + if (forceRefresh) { + usedKeypair2 = true + return keypair2.did() + } else { + usedKeypair1 = true + return keypair1.did() + } + }, + ) + await expect(tryVerify).resolves.toMatchObject({ + aud: 'did:example:aud', + iss: 'did:example:iss', + }) + expect(usedKeypair1).toBe(true) + expect(usedKeypair2).toBe(true) + }) + + it('interoperates with jwts signed by other libraries.', async () => { + const keypair = await Secp256k1Keypair.create({ exportable: true }) + const signingKey = await createPrivateKeyObject(keypair) + const payload = { + aud: 'did:example:aud', + iss: 'did:example:iss', + exp: Math.floor((Date.now() + MINUTE) / 1000), + } + const jwt = await new jose.SignJWT(payload) + .setProtectedHeader({ typ: 'JWT', alg: keypair.jwtAlg }) + .sign(signingKey) + const tryVerify = xrpcServer.verifyJwt( + jwt, + 'did:example:aud', + async () => { + return keypair.did() + }, + ) + await expect(tryVerify).resolves.toEqual(payload) + }) + }) }) + +const createPrivateKeyObject = async ( + privateKey: Secp256k1Keypair, +): Promise => { + const raw = await privateKey.export() + const encoder = new KeyEncoder('secp256k1') + const key = encoder.encodePrivate(ui8.toString(raw, 'hex'), 'raw', 'pem') + return createPrivateKey({ format: 'pem', key }) +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1500b3ece5..aac3ef2bcb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -734,6 +734,12 @@ importers: get-port: specifier: ^6.1.2 version: 6.1.2 + jose: + specifier: ^4.15.4 + version: 4.15.4 + key-encoder: + specifier: ^2.0.3 + version: 2.0.3 multiformats: specifier: ^9.9.0 version: 9.9.0 @@ -5294,7 +5300,6 @@ packages: resolution: {integrity: sha512-qNrYbZqMx0uJAfKnKclPh+dTwK33KfLHYqtyODwd5HnXOjnkhc4qgn3BrK6RWyGZm5+sIFE7Q7Vz6QQtJB7w7g==} dependencies: '@types/node': 18.17.8 - dev: false /@types/body-parser@1.19.2: resolution: {integrity: sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==} @@ -5321,7 +5326,6 @@ packages: resolution: {integrity: sha512-z4OBcDAU0GVwDTuwJzQCiL6188QvZMkvoERgcVjq0/mPM8jCfdwZ3x5zQEVoL9WCAru3aG5wl3Z5Ww5wBWn7ZQ==} dependencies: '@types/bn.js': 5.1.1 - dev: false /@types/express-serve-static-core@4.17.36: resolution: {integrity: sha512-zbivROJ0ZqLAtMzgzIUC4oNqDG9iF0lSsAqpOD9kbs5xcIM3dTiyuHvBc7R8MtWBp3AAWGaovJa+wzWPjLYW7Q==} @@ -5833,7 +5837,6 @@ packages: inherits: 2.0.4 minimalistic-assert: 1.0.1 safer-buffer: 2.1.2 - dev: false /asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} @@ -6031,7 +6034,6 @@ packages: /bn.js@4.12.0: resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==} - dev: false /body-parser@1.20.1: resolution: {integrity: sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==} @@ -6085,7 +6087,6 @@ packages: /brorand@1.1.0: resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==} - dev: false /browserslist@4.21.10: resolution: {integrity: sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==} @@ -6739,7 +6740,6 @@ packages: inherits: 2.0.4 minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - dev: false /emittery@0.10.2: resolution: {integrity: sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==} @@ -7849,7 +7849,6 @@ packages: dependencies: inherits: 2.0.4 minimalistic-assert: 1.0.1 - dev: false /he@1.2.0: resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} @@ -7869,7 +7868,6 @@ packages: hash.js: 1.1.7 minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 - dev: false /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} @@ -8735,6 +8733,10 @@ packages: - ts-node dev: true + /jose@4.15.4: + resolution: {integrity: sha512-W+oqK4H+r5sITxfxpSU+MMdr/YSWGvgZMQDIsNoBDGGy4i7GBPTtvFKibQzW06n3U3TqHjhvBJsirShsEJ6eeQ==} + dev: true + /joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -8856,7 +8858,6 @@ packages: asn1.js: 5.4.1 bn.js: 4.12.0 elliptic: 6.5.4 - dev: false /kind-of@6.0.3: resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} @@ -9150,11 +9151,9 @@ packages: /minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - dev: false /minimalistic-crypto-utils@1.0.1: resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==} - dev: false /minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}