From 35fbb6e0310e280595788c3166f9277ae9185328 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 21 Sep 2026 10:42:46 +0100 Subject: [PATCH 01/33] feat: update docs on KMS certificate verification --- .../internal-plugin-encryption/README.md | 44 +++++++++++++++++++ .../internal-plugin-encryption/src/config.js | 11 ++++- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/README.md b/packages/@webex/internal-plugin-encryption/README.md index 8bdaa174665..890ee63b4b4 100644 --- a/packages/@webex/internal-plugin-encryption/README.md +++ b/packages/@webex/internal-plugin-encryption/README.md @@ -29,6 +29,50 @@ const webex = new WebexCore(); webex.internal.encryption.WHATEVER; ``` +## KMS certificate validation + +When the SDK negotiates an ECDH key with the KMS, it can validate the KMS +certificate chain against a set of trusted CA roots. This is controlled by two +configuration options on the `encryption` config: + +- `caroots` — an array of PEM-encoded CA root certificates. When provided, the + KMS certificate chain must validate against this bundle or the ECDH + negotiation fails. When omitted, the chain signature is **not** verified. +- `carootsReportOnly` — an additional array of PEM-encoded CA roots validated + alongside `caroots`. A failure here is only reported as a metric instead of + failing the negotiation, which lets a new bundle be trialled in parallel with + the enforced `caroots`. + +Supplying the CA roots is the responsibility of the consuming application. The +SDK does not ship a bundle. Cisco first-party clients should source their roots +from the Cisco Trusted Root Store, using the **Union** bundle: + + +The referenced page is authoritative for how to download, verify, and extract +the bundle; follow it for current instructions. In short, the application +downloads the signed Union bundle (`ios_union.p7b`), extracts each certificate, +and passes them as the `caroots` array. Each entry is the raw base64-encoded +certificate (the DER body, without the `-----BEGIN/END CERTIFICATE-----` lines +or newlines): + +```js +import '@webex/internal-plugin-encryption'; + +import WebexCore from '@webex/webex-core'; + +const webex = new WebexCore({ + config: { + encryption: { + // Raw base64-encoded certificates extracted from the Cisco Union bundle + caroots: [ + 'MIIF8TCCA9mgAwIBAgIIVE2lvEA1VlowDQYJKoZIhvcNAQELBQAw...', + // ...additional roots + ], + }, + }, +}); +``` + ## Maintainers This package is maintained by [Cisco Webex for Developers](https://developer.webex.com/). diff --git a/packages/@webex/internal-plugin-encryption/src/config.js b/packages/@webex/internal-plugin-encryption/src/config.js index 989188e177f..2ebff47fb19 100644 --- a/packages/@webex/internal-plugin-encryption/src/config.js +++ b/packages/@webex/internal-plugin-encryption/src/config.js @@ -48,8 +48,15 @@ export default { batcherMaxWait: 150, /** - * PEM encoded CA root bundle used to validate the KMS certificate chain. - * When omitted, the KMS certificate chain signature is not verified. + * CA root bundle used to validate the KMS certificate chain, as an array of + * raw base64-encoded certificates (the DER body, without the + * -----BEGIN/END CERTIFICATE----- lines). When omitted, the KMS certificate + * chain signature is not verified. + * + * Supplied by the consuming application; the SDK does not ship a bundle. + * Cisco first-party clients should source these roots from the Cisco Trusted + * Root Store Union bundle. See the plugin README and + * https://www.cisco.com/security/pki/trs/readme.html for details. * @type {?string[]} */ caroots: undefined, From a2d47cf6cbe2d0f74b3caf60dd8a589400f71529 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 21 Sep 2026 11:56:21 +0100 Subject: [PATCH 02/33] feat(internal-plugin-encryption): validate KMS cert safe default and docs update --- .../internal-plugin-encryption/README.md | 23 +++++++----- .../internal-plugin-encryption/src/config.js | 14 ++++++-- .../src/kms-certificate-validation.js | 35 ++++++++++++------- .../internal-plugin-encryption/src/kms.js | 20 ++++++----- .../unit/spec/kms-certificate-validation.js | 14 ++++++-- .../test/unit/spec/kms.js | 22 ++++++++++++ 6 files changed, 93 insertions(+), 35 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/README.md b/packages/@webex/internal-plugin-encryption/README.md index 890ee63b4b4..0bcd810bd52 100644 --- a/packages/@webex/internal-plugin-encryption/README.md +++ b/packages/@webex/internal-plugin-encryption/README.md @@ -31,17 +31,22 @@ webex.internal.encryption.WHATEVER; ## KMS certificate validation -When the SDK negotiates an ECDH key with the KMS, it can validate the KMS -certificate chain against a set of trusted CA roots. This is controlled by two +When the SDK negotiates an ECDH key with the KMS, it validates the KMS +certificate chain against a set of trusted CA roots. This is controlled by these configuration options on the `encryption` config: -- `caroots` — an array of PEM-encoded CA root certificates. When provided, the - KMS certificate chain must validate against this bundle or the ECDH - negotiation fails. When omitted, the chain signature is **not** verified. -- `carootsReportOnly` — an additional array of PEM-encoded CA roots validated - alongside `caroots`. A failure here is only reported as a metric instead of - failing the negotiation, which lets a new bundle be trialled in parallel with - the enforced `caroots`. +- `shouldValidateKMSCertificate` — whether to validate the KMS certificate + chain. Defaults to `true` as a secure default. When enabled the SDK **fails + closed**: a `caroots` bundle must be configured and the chain must validate + against it, otherwise the ECDH negotiation fails. Set to `false` to + temporarily opt out of validation, for example while upgrading and wiring up + the CA root bundle. +- `caroots` — an array of raw base64-encoded CA root certificates. Required when + `shouldValidateKMSCertificate` is `true`. +- `carootsReportOnly` — an additional array of CA roots validated alongside + `caroots`. A failure here is only reported as a metric instead of failing the + negotiation, which lets a new bundle be trialled in parallel with the enforced + `caroots`. Supplying the CA roots is the responsibility of the consuming application. The SDK does not ship a bundle. Cisco first-party clients should source their roots diff --git a/packages/@webex/internal-plugin-encryption/src/config.js b/packages/@webex/internal-plugin-encryption/src/config.js index 2ebff47fb19..40b67c40348 100644 --- a/packages/@webex/internal-plugin-encryption/src/config.js +++ b/packages/@webex/internal-plugin-encryption/src/config.js @@ -47,11 +47,21 @@ export default { */ batcherMaxWait: 150, + /** + * Whether to validate the KMS certificate chain against `caroots`. Defaults + * to true as a secure default: when enabled the KMS certificate must + * validate against a configured `caroots` bundle, and a missing bundle + * fails closed. Set to false to temporarily opt out of validation, e.g. + * while upgrading and wiring up the CA root bundle. + * @type {boolean} + */ + shouldValidateKMSCertificate: true, + /** * CA root bundle used to validate the KMS certificate chain, as an array of * raw base64-encoded certificates (the DER body, without the - * -----BEGIN/END CERTIFICATE----- lines). When omitted, the KMS certificate - * chain signature is not verified. + * -----BEGIN/END CERTIFICATE----- lines). Required when + * `shouldValidateKMSCertificate` is true. * * Supplied by the consuming application; the SDK does not ship a bundle. * Cisco first-party clients should source these roots from the Cisco Trusted diff --git a/packages/@webex/internal-plugin-encryption/src/kms-certificate-validation.js b/packages/@webex/internal-plugin-encryption/src/kms-certificate-validation.js index 9d62f509b79..13ee6d9dac0 100644 --- a/packages/@webex/internal-plugin-encryption/src/kms-certificate-validation.js +++ b/packages/@webex/internal-plugin-encryption/src/kms-certificate-validation.js @@ -198,16 +198,21 @@ const validateCertificatesSignature = (certificates, caroots = []) => { /** * Validates the information provided by the KMS. This is a curried function. - * The first function takes the caroots param and returns a second function. - * The second function takes the credentials of the KMS and validates it - * @param {string[]} caroots PEM encoded certificates that will be used - * as Certificate Authorities - * @param {Object} jwt Object containing the fields necessary to - * validate the KMS - * @returns {Promise} when resolved will return the jwt + * The first function takes the validation options and returns a second + * function. The second function takes the credentials of the KMS and validates + * it + * @param {Object} [options] + * @param {string[]} [options.caroots] base64-encoded certificates that will be + * used as Certificate Authorities + * @param {boolean} [options.validateSignature=true] when true, the KMS + * certificate chain must validate against `caroots`; if no `caroots` are + * provided the validation fails closed. Set to false to skip signature + * validation entirely. + * @returns {Function} function that takes the jwt and returns a Promise which, + * when resolved, returns the jwt */ const validateKMS = - (caroots) => + ({caroots, validateSignature = true} = {}) => (jwt = {}) => Promise.resolve().then(() => { validateKtyHeader(jwt); @@ -221,12 +226,16 @@ const validateKMS = validateCommonName(certificates, jwt); validatePublicCertificate(certificates, jwt); - // Skip validating signatures if no CA roots were provided - const promise = caroots - ? validateCertificatesSignature(certificates, caroots) - : Promise.resolve(); + if (!validateSignature) { + return jwt; + } + + // Fail closed: signature validation is required but no CA roots exist + if (!(isArray(caroots) && caroots.length > 0)) { + throwError('no CA roots configured to validate the KMS certificate against'); + } - return promise.then(() => jwt); + return validateCertificatesSignature(certificates, caroots).then(() => jwt); }); export default validateKMS; diff --git a/packages/@webex/internal-plugin-encryption/src/kms.js b/packages/@webex/internal-plugin-encryption/src/kms.js index 5a188403f76..5695b5f34b0 100644 --- a/packages/@webex/internal-plugin-encryption/src/kms.js +++ b/packages/@webex/internal-plugin-encryption/src/kms.js @@ -784,24 +784,28 @@ const KMS = WebexPlugin.extend({ }, /** - * Validates the KMS static public key against the configured CA roots. The - * enforced `caroots` bundle rejects on failure. When a `carootsReportOnly` - * bundle is also configured, it is validated in addition to `caroots`, but a - * failure against it is only reported as a metric so a new bundle can be - * trialled without risking failure. + * Validates the KMS static public key against the configured CA roots. + * Validation is enabled by default (`shouldValidateKMSCertificate`) and fails + * closed: when enabled the enforced `caroots` bundle must be configured and + * the chain must validate against it. When a `carootsReportOnly` bundle is + * also configured, it is validated in addition to `caroots`, but a failure + * against it is only reported as a metric so a new bundle can be trialled + * without risking failure. * @private * @param {Object} kmsStaticPubKey * @returns {Promise} the KMS static public key */ _validateKMSStaticPubKey(kmsStaticPubKey) { - const {caroots, carootsReportOnly} = this.config; + const {caroots, carootsReportOnly, shouldValidateKMSCertificate} = this.config; - return validateKMS(caroots)(kmsStaticPubKey).then((jwt) => { + return validateKMS({caroots, validateSignature: shouldValidateKMSCertificate})( + kmsStaticPubKey + ).then((jwt) => { if (!carootsReportOnly) { return jwt; } - return validateKMS(carootsReportOnly)(kmsStaticPubKey) + return validateKMS({caroots: carootsReportOnly, validateSignature: true})(kmsStaticPubKey) .catch((reason) => { this.logger.warn('kms: report-only certificate validation failed', reason); diff --git a/packages/@webex/internal-plugin-encryption/test/unit/spec/kms-certificate-validation.js b/packages/@webex/internal-plugin-encryption/test/unit/spec/kms-certificate-validation.js index 9397abc2d0f..96dbacca2a1 100644 --- a/packages/@webex/internal-plugin-encryption/test/unit/spec/kms-certificate-validation.js +++ b/packages/@webex/internal-plugin-encryption/test/unit/spec/kms-certificate-validation.js @@ -40,7 +40,7 @@ const VALID_JWT_SAN = { e: 'AQAB', }; -const validate = validateCert(caroots); +const validate = validateCert({caroots}); describe('internal-plugin-encryption', () => { describe('kms-certificate-validation', () => { @@ -152,14 +152,22 @@ describe('internal-plugin-encryption', () => { return assert.isRejected(validate(jwt), KMSError); }); - it('accepts self signed certificate if no CA roots.', () => { + it('rejects when validation is required but no CA roots are configured', () => + assert.isRejected(validateCert()(VALID_JWT), KMSError)); + + it('rejects when validation is required and CA roots are empty', () => + assert.isRejected(validateCert({caroots: []})(VALID_JWT), KMSError)); + + it('accepts self signed certificate when signature validation is disabled', () => { const jwt = { ...VALID_JWT, x5c: x5cSelfSigned, n: x5cSelfSignedModulus, }; - return validateCert()(jwt).then((results) => assert.equal(results, jwt)); + return validateCert({validateSignature: false})(jwt).then((results) => + assert.equal(results, jwt) + ); }); }); }); diff --git a/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js index 67c080f97c8..9f2787fb24a 100644 --- a/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js @@ -263,6 +263,7 @@ describe('internal-plugin-encryption', () => { beforeEach(() => { webex.internal.metrics = {submitClientMetrics: sinon.stub()}; + webex.internal.encryption.config.shouldValidateKMSCertificate = true; webex.internal.encryption.config.caroots = caroots; webex.internal.encryption.config.carootsReportOnly = undefined; }); @@ -276,6 +277,27 @@ describe('internal-plugin-encryption', () => { assert.notCalled(webex.internal.metrics.submitClientMetrics); }); + it('rejects when validation is enabled but no caroots are configured', async () => { + webex.internal.encryption.config.caroots = undefined; + + await assert.isRejected( + webex.internal.encryption.kms._validateKMSStaticPubKey(validKey), + /INVALID KMS/ + ); + + assert.notCalled(webex.internal.metrics.submitClientMetrics); + }); + + it('resolves without validating when shouldValidateKMSCertificate is false', async () => { + webex.internal.encryption.config.shouldValidateKMSCertificate = false; + webex.internal.encryption.config.caroots = undefined; + + const result = await webex.internal.encryption.kms._validateKMSStaticPubKey(validKey); + + assert.equal(result, validKey); + assert.notCalled(webex.internal.metrics.submitClientMetrics); + }); + it('resolves without a metric when no report-only bundle is configured', async () => { const result = await webex.internal.encryption.kms._validateKMSStaticPubKey(validKey); From 8a4f4d336bcb67d8c73bbccf3498a47369b387c7 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 22 Sep 2026 13:20:30 +0100 Subject: [PATCH 03/33] feat(internal-plugin-encryption): add KMS CA roots tooling and env-based config - add tooling/generate-kms-caroots.js to download, verify (against pinned Cisco trust anchors), and decode the Cisco Trusted Root Store Union bundle into the caroots format - add tooling/with-kms-caroots.sh wrapper and caroots:generate script - default encryption.caroots from the WEBEX_KMS_CAROOTS env var - run browser and integration CI jobs with generated CA roots so validation is exercised against the real KMS - disable KMS cert validation in sample apps that do not ship a bundle - document the tooling and env var in the plugin README --- .github/workflows/pull-request.yml | 4 +- .gitignore | 1 + docs/samples/browser-plugin-meetings/app.js | 4 + docs/samples/browser-read-status/app.js | 5 +- docs/samples/browser-socket/app.js | 5 +- docs/samples/calling/app.js | 1 + docs/samples/contact-center/app.js | 4 + docs/samples/plugin-encryption/app.js | 4 + package.json | 1 + .../internal-plugin-encryption/README.md | 46 ++- .../internal-plugin-encryption/src/config.js | 30 +- tooling/generate-kms-caroots.js | 295 ++++++++++++++++++ tooling/with-kms-caroots.sh | 24 ++ 13 files changed, 408 insertions(+), 16 deletions(-) create mode 100755 tooling/generate-kms-caroots.js create mode 100755 tooling/with-kms-caroots.sh diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ff35895439a..8f454601c56 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -144,7 +144,7 @@ jobs: run: yarn workspaces foreach --parallel --topological --verbose run build:src - name: Test - Browser - run: yarn workspaces foreach --verbose --exclude webex-js-sdk --include '${{ needs.generate-package-matrix.outputs.changed }}' run test:browser + run: tooling/with-kms-caroots.sh yarn workspaces foreach --verbose --exclude webex-js-sdk --include '${{ needs.generate-package-matrix.outputs.changed }}' run test:browser test-coverage: @@ -235,7 +235,7 @@ jobs: run: yarn workspaces foreach --parallel --topological --verbose run build:src - name: Test - Integration - run: yarn workspaces foreach --verbose --exclude webex-js-sdk --include '${{ needs.generate-package-matrix.outputs.changed }}' run test:integration + run: tooling/with-kms-caroots.sh yarn workspaces foreach --verbose --exclude webex-js-sdk --include '${{ needs.generate-package-matrix.outputs.changed }}' run test:integration test-style: diff --git a/.gitignore b/.gitignore index 0e098a4004a..f8c0d8352a8 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ tasks/selenium .grunt .github-publish .idea +.kms-caroots.json .npm .nvm .python-version diff --git a/docs/samples/browser-plugin-meetings/app.js b/docs/samples/browser-plugin-meetings/app.js index ddcf1dc7fd6..214db5c4992 100644 --- a/docs/samples/browser-plugin-meetings/app.js +++ b/docs/samples/browser-plugin-meetings/app.js @@ -141,6 +141,10 @@ function generateWebexConfig({credentials}) { }, enableAutomaticLLM: enableLLM.checked, }, + // Samples don't ship a KMS CA root bundle, so disable cert validation. + encryption: { + shouldValidateKMSCertificate: false, + }, credentials, // Any other sdk config we need }; diff --git a/docs/samples/browser-read-status/app.js b/docs/samples/browser-read-status/app.js index 5b333c1478b..83e88f11e24 100644 --- a/docs/samples/browser-read-status/app.js +++ b/docs/samples/browser-read-status/app.js @@ -29,7 +29,10 @@ let haveFetchedAll = false; function authorize() { webex = Webex.init({ config: { - + // Samples don't ship a KMS CA root bundle, so disable cert validation. + encryption: { + shouldValidateKMSCertificate: false, + }, }, credentials: { access_token: document.getElementById('access-token').value diff --git a/docs/samples/browser-socket/app.js b/docs/samples/browser-socket/app.js index 41c86154d28..c1ba777b417 100644 --- a/docs/samples/browser-socket/app.js +++ b/docs/samples/browser-socket/app.js @@ -26,7 +26,10 @@ function authorize() { // eslint-disable-next-line no-multi-assign webex = window.webex = Webex.init({ config: { - + // Samples don't ship a KMS CA root bundle, so disable cert validation. + encryption: { + shouldValidateKMSCertificate: false, + }, }, credentials: { access_token: document.getElementById('access-token').value diff --git a/docs/samples/calling/app.js b/docs/samples/calling/app.js index fe8646af8cd..a7bb0d80ce0 100644 --- a/docs/samples/calling/app.js +++ b/docs/samples/calling/app.js @@ -284,6 +284,7 @@ async function initCalling(e) { kmsMaxTimeout: 40000, batcherMaxCalls: 30, caroots: null, + shouldValidateKMSCertificate: false, }, dss: {}, }, diff --git a/docs/samples/contact-center/app.js b/docs/samples/contact-center/app.js index 21aa15c934e..afe86c7f252 100644 --- a/docs/samples/contact-center/app.js +++ b/docs/samples/contact-center/app.js @@ -3096,6 +3096,10 @@ function generateWebexConfig({credentials}) { disableWebRTCRegistration: isWebRTCRegistrationDisabled, enableWxBetterTogether: isWxBetterTogetherEnabled, }, + // Samples don't ship a KMS CA root bundle, so disable cert validation. + encryption: { + shouldValidateKMSCertificate: false, + }, credentials, }; } diff --git a/docs/samples/plugin-encryption/app.js b/docs/samples/plugin-encryption/app.js index 54a43ca222f..1667cdd7996 100644 --- a/docs/samples/plugin-encryption/app.js +++ b/docs/samples/plugin-encryption/app.js @@ -68,6 +68,10 @@ async function initWebex(e) { logger: { level: 'debug', // set the desired log level }, + // Samples don't ship a KMS CA root bundle, so disable cert validation. + encryption: { + shouldValidateKMSCertificate: false, + }, }, credentials: { access_token: tokenElm.value diff --git a/package.json b/package.json index eaf83c5f1f9..e08e0c57533 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "test:integration": "node ./tooling/index.js test --integration --browser", "test:ci:github": "node ./tooling/index.js ci --github", "test:ci:integration": "node ./tooling/index.js ci --integration", + "caroots:generate": "node ./tooling/generate-kms-caroots.js", "distsrc": "find ./packages -name 'package.json' -print0 | xargs -0 sed -ibak 's#\"main\": \"dist/index.js#\"main\": \"src/index.js#' && find ./packages -name '*bak' -print0 | xargs -0 rimraf", "srcdist": "find ./packages -name 'package.json' -print0 | xargs -0 sed -ibak 's#\"main\": \"src/index.js#\"main\": \"dist/index.js#' && find ./packages -name '*bak' -print0 | xargs -0 rimraf", "get-current-version": "node ./tooling/index.js version current", diff --git a/packages/@webex/internal-plugin-encryption/README.md b/packages/@webex/internal-plugin-encryption/README.md index 0bcd810bd52..82f978dd014 100644 --- a/packages/@webex/internal-plugin-encryption/README.md +++ b/packages/@webex/internal-plugin-encryption/README.md @@ -49,16 +49,46 @@ configuration options on the `encryption` config: `caroots`. Supplying the CA roots is the responsibility of the consuming application. The -SDK does not ship a bundle. Cisco first-party clients should source their roots -from the Cisco Trusted Root Store, using the **Union** bundle: +SDK does not ship a bundle, so that certificate updates don't require an SDK +upgrade. Cisco first-party clients should source their roots from the Cisco +Trusted Root Store, using the **Union** bundle: -The referenced page is authoritative for how to download, verify, and extract -the bundle; follow it for current instructions. In short, the application -downloads the signed Union bundle (`ios_union.p7b`), extracts each certificate, -and passes them as the `caroots` array. Each entry is the raw base64-encoded -certificate (the DER body, without the `-----BEGIN/END CERTIFICATE-----` lines -or newlines): +### Generating the CA roots + +This repo ships a tool that downloads the Cisco Union bundle, verifies its +signature against the pinned Cisco trust anchors, and decodes it into the +`caroots` format (an array of raw base64-encoded certificates): + +```bash +# Write ./.kms-caroots.json +yarn caroots:generate + +# Or print the JSON array to stdout +node tooling/generate-kms-caroots.js --stdout +``` + +It requires the `openssl` binary on `PATH`. The output can be passed directly as +`config.encryption.caroots`, or exposed via the `WEBEX_KMS_CAROOTS` environment +variable (a JSON array), which the SDK reads as the default for `caroots`: + +```bash +export WEBEX_KMS_CAROOTS="$(node tooling/generate-kms-caroots.js --stdout)" +``` + +The `tooling/with-kms-caroots.sh` wrapper generates the roots and sets that +environment variable for a command, which is how CI runs the integration tests +against the real KMS with validation enabled: + +```bash +tooling/with-kms-caroots.sh yarn workspace @webex/internal-plugin-encryption test:integration +``` + +### Configuring manually + +The referenced Cisco page is authoritative for how to download, verify, and +extract the bundle. Each `caroots` entry is the raw base64-encoded certificate +(the DER body, without the `-----BEGIN/END CERTIFICATE-----` lines or newlines): ```js import '@webex/internal-plugin-encryption'; diff --git a/packages/@webex/internal-plugin-encryption/src/config.js b/packages/@webex/internal-plugin-encryption/src/config.js index 40b67c40348..46b779e220a 100644 --- a/packages/@webex/internal-plugin-encryption/src/config.js +++ b/packages/@webex/internal-plugin-encryption/src/config.js @@ -2,6 +2,27 @@ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. */ +/** + * Reads a base64-encoded CA root bundle (JSON array) from the + * `WEBEX_KMS_CAROOTS` environment variable, if present. This lets consuming + * apps and CI supply roots without baking them into the package. See + * tooling/generate-kms-caroots.js and the plugin README. + * @returns {Array} the parsed CA roots, or undefined when the variable is unset + */ +const getCarootsFromEnv = () => { + const raw = process.env.WEBEX_KMS_CAROOTS; + + if (!raw) { + return undefined; + } + + try { + return JSON.parse(raw); + } catch (error) { + return undefined; + } +}; + export default { encryption: { joseOptions: { @@ -64,12 +85,13 @@ export default { * `shouldValidateKMSCertificate` is true. * * Supplied by the consuming application; the SDK does not ship a bundle. - * Cisco first-party clients should source these roots from the Cisco Trusted - * Root Store Union bundle. See the plugin README and - * https://www.cisco.com/security/pki/trs/readme.html for details. + * Defaults to the `WEBEX_KMS_CAROOTS` environment variable when set (see + * tooling/generate-kms-caroots.js). Cisco first-party clients should source + * these roots from the Cisco Trusted Root Store Union bundle. See the plugin + * README and https://www.cisco.com/security/pki/trs/readme.html for details. * @type {?string[]} */ - caroots: undefined, + caroots: getCarootsFromEnv(), /** * An additional CA root bundle validated alongside `caroots`. Unlike diff --git a/tooling/generate-kms-caroots.js b/tooling/generate-kms-caroots.js new file mode 100755 index 00000000000..37dd14fdac4 --- /dev/null +++ b/tooling/generate-kms-caroots.js @@ -0,0 +1,295 @@ +#!/usr/bin/env node +/*! + * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. + */ + +/* eslint-disable no-console */ + +/** + * Downloads, verifies, and decodes the Cisco Trusted Root Store "Union" bundle + * into the format expected by the `encryption.caroots` SDK config option: an + * array of raw base64-encoded (DER) certificates. + * + * The bundle is intentionally NOT committed to the package. Consuming apps (and + * this repo's CI) run this tool to produce a fresh list so certificate updates + * don't require an SDK upgrade. See the Cisco Trusted Root Store for details: + * https://www.cisco.com/security/pki/trs/readme.html + * + * Usage: + * node tooling/generate-kms-caroots.js # writes ./.kms-caroots.json + * node tooling/generate-kms-caroots.js --stdout # prints JSON to stdout + * node tooling/generate-kms-caroots.js --out roots.json + * node tooling/generate-kms-caroots.js --url # override bundle URL + * + * Requires the `openssl` binary on PATH (used to verify the signed PKCS#7 + * bundle and extract its certificates, as documented by Cisco). + */ + +const crypto = require('crypto'); +const {execFileSync} = require('child_process'); +const fs = require('fs'); +const https = require('https'); +const os = require('os'); +const path = require('path'); +const {URL} = require('url'); + +const DEFAULT_BUNDLE_URL = + 'https://www.cisco.com/security/pki/trs/current/ios_union/ios_union.p7b'; + +// Trust anchors used to verify the signed bundle. Pinned by SHA-256 of the DER +// certificate so a compromised or swapped anchor is rejected. New-style bundles +// chain to the TRS Bundle Root CA; older bundles chain to Cisco Root CA M1. +const TRUST_ANCHORS = [ + { + name: 'Cisco TRS Bundle Root CA', + url: 'https://www.cisco.com/security/pki/certs/tbrca.pem', + fingerprint: + 'DE:C6:69:E3:22:E0:7C:D7:C6:0A:56:90:4B:F5:0C:29:FA:1E:75:07:17:23:FC:10:35:77:E2:7B:22:69:68:D5', + }, + { + name: 'Cisco Root CA M1', + url: 'https://www.cisco.com/security/pki/certs/crcam1.pem', + fingerprint: + '70:5E:AA:FC:3F:F4:88:03:00:17:D5:98:32:60:3E:EF:AD:51:41:71:B5:83:80:86:75:F4:5C:19:0E:63:78:F8', + }, +]; + +const PEM_CERT_RE = /-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/g; + +function parseArgs(argv) { + const options = {bundleUrl: DEFAULT_BUNDLE_URL, out: '.kms-caroots.json', stdout: false, verbose: false}; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + + switch (arg) { + case '--stdout': + options.stdout = true; + break; + case '--out': + options.out = argv[(i += 1)]; + break; + case '--url': + options.bundleUrl = argv[(i += 1)]; + break; + case '-v': + case '--verbose': + options.verbose = true; + break; + case '-h': + case '--help': + options.help = true; + break; + default: + throw new Error(`Unknown argument: ${arg}`); + } + } + + return options; +} + +// Log to stderr so `--stdout` emits only the JSON payload. +function log(verbose, ...args) { + if (verbose) { + console.error(...args); + } +} + +function download(url) { + return new Promise((resolve, reject) => { + https + .get(url, {headers: {'user-agent': 'webex-js-sdk-kms-caroots'}}, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + res.resume(); + resolve(download(new URL(res.headers.location, url).toString())); + + return; + } + + if (res.statusCode !== 200) { + res.resume(); + reject(new Error(`Failed to download ${url}: HTTP ${res.statusCode}`)); + + return; + } + + const chunks = []; + + res.on('data', (chunk) => chunks.push(chunk)); + res.on('end', () => resolve(Buffer.concat(chunks))); + }) + .on('error', reject); + }); +} + +function pemToDer(pem) { + return Buffer.from(pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''), 'base64'); +} + +function fingerprintOf(der) { + return crypto + .createHash('sha256') + .update(der) + .digest('hex') + .toUpperCase() + .match(/../g) + .join(':'); +} + +function ensureOpenssl() { + try { + execFileSync('openssl', ['version'], {stdio: 'ignore'}); + } catch (error) { + throw new Error( + "The 'openssl' binary is required to verify and decode the Cisco bundle but was not found on PATH." + ); + } +} + +async function fetchVerifiedAnchors(verbose) { + const pems = []; + + for (const anchor of TRUST_ANCHORS) { + const pem = (await download(anchor.url)).toString('utf8'); + const actual = fingerprintOf(pemToDer(pem)); + + if (actual !== anchor.fingerprint) { + throw new Error( + `Trust anchor fingerprint mismatch for ${anchor.name}.\n expected: ${anchor.fingerprint}\n actual: ${actual}` + ); + } + + log(verbose, `Verified trust anchor: ${anchor.name}`); + pems.push(pem.trim()); + } + + return pems.join('\n'); +} + +// Verifies the signed bundle against the pinned anchors and returns the +// contained certificates as PEM using the openssl pipeline documented by Cisco. +function verifyAndExtract(workDir, bundle, anchorsPem) { + const bundlePath = path.join(workDir, 'bundle.p7b'); + const anchorsPath = path.join(workDir, 'anchors.pem'); + const contentPath = path.join(workDir, 'content.der'); + + fs.writeFileSync(bundlePath, bundle); + fs.writeFileSync(anchorsPath, anchorsPem); + + execFileSync('openssl', [ + 'cms', + '-verify', + '-inform', + 'DER', + '-purpose', + 'any', + '-in', + bundlePath, + '-CAfile', + anchorsPath, + '-out', + contentPath, + ]); + + return execFileSync('openssl', [ + 'pkcs7', + '-inform', + 'DER', + '-print_certs', + '-outform', + 'PEM', + '-in', + contentPath, + ]).toString('utf8'); +} + +function decodeCertificates(certsPem) { + const seen = new Set(); + const caroots = []; + let match; + + // eslint-disable-next-line no-cond-assign + while ((match = PEM_CERT_RE.exec(certsPem))) { + const base64 = match[1].replace(/\s+/g, ''); + + if (!base64 || seen.has(base64)) { + continue; + } + + const der = Buffer.from(base64, 'base64'); + + // A valid X.509 certificate is a DER SEQUENCE (tag 0x30). + if (der.length === 0 || der[0] !== 0x30) { + throw new Error('Extracted a certificate that is not valid DER'); + } + + seen.add(base64); + caroots.push(base64); + } + + if (caroots.length === 0) { + throw new Error('No certificates were extracted from the bundle'); + } + + return caroots; +} + +async function generate(options) { + ensureOpenssl(); + + log(options.verbose, `Downloading bundle: ${options.bundleUrl}`); + const bundle = await download(options.bundleUrl); + + const anchorsPem = await fetchVerifiedAnchors(options.verbose); + + const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kms-caroots-')); + + try { + const certsPem = verifyAndExtract(workDir, bundle, anchorsPem); + const caroots = decodeCertificates(certsPem); + + log(options.verbose, `Verified bundle and extracted ${caroots.length} certificates`); + + return caroots; + } finally { + fs.rmSync(workDir, {recursive: true, force: true}); + } +} + +const HELP = `Generate the KMS CA roots (Cisco Trusted Root Store Union bundle). + +Usage: + node tooling/generate-kms-caroots.js [options] + +Options: + --stdout Print the JSON array to stdout instead of writing a file + --out Output file path (default: .kms-caroots.json) + --url Override the bundle URL + -v, --verbose Log progress to stderr + -h, --help Show this help +`; + +async function main() { + const options = parseArgs(process.argv.slice(2)); + + if (options.help) { + process.stdout.write(HELP); + + return; + } + + const caroots = await generate(options); + const json = JSON.stringify(caroots); + + if (options.stdout) { + process.stdout.write(json); + } else { + fs.writeFileSync(options.out, json); + console.error(`Wrote ${caroots.length} CA roots to ${options.out}`); + } +} + +main().catch((error) => { + console.error(`generate-kms-caroots: ${error.message}`); + process.exit(1); +}); diff --git a/tooling/with-kms-caroots.sh b/tooling/with-kms-caroots.sh new file mode 100755 index 00000000000..e248974282a --- /dev/null +++ b/tooling/with-kms-caroots.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# Generates the KMS CA roots (Cisco Trusted Root Store Union bundle) and exposes +# them to the SDK/tests via the WEBEX_KMS_CAROOTS environment variable, then runs +# the given command. Used by CI and locally to run integration tests against the +# real KMS with certificate validation enabled. +# +# Usage: +# tooling/with-kms-caroots.sh yarn workspace @webex/internal-plugin-encryption test:integration + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +if [ "$#" -eq 0 ]; then + echo "Usage: tooling/with-kms-caroots.sh [args...]" >&2 + exit 1 +fi + +echo "Generating KMS CA roots..." >&2 +WEBEX_KMS_CAROOTS="$(node "${SCRIPT_DIR}/generate-kms-caroots.js" --stdout)" +export WEBEX_KMS_CAROOTS + +exec "$@" From 5f6c3a67ac106e4f0fdee5acff8df22b6f201fb9 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 22 Sep 2026 14:00:47 +0100 Subject: [PATCH 04/33] fix(internal-plugin-encryption): deliver KMS CA roots via test fixture, not env The previous env-var delivery was unusable: a ~362KB single environment variable exceeds the per-string exec limit (MAX_ARG_STRLEN), and reading the bundle from a file in config.js would break consuming browser app builds. - keep the shipped config a pure library: caroots defaults to undefined with no file or environment I/O - deliver generated roots to the encryption integration/browser tests via a committed test-only fixture that the wrapper populates and then restores - disable KMS cert validation in plugin-messages integration tests (they test messaging, not certificate validation) - retry transient downloads in the generator --- .../internal-plugin-encryption/README.md | 21 ++++++------ .../internal-plugin-encryption/src/config.js | 33 ++++--------------- .../test/integration/spec/encryption.js | 11 +++++++ .../integration/spec/kms-caroots.fixture.json | 1 + .../test/integration/spec/kms.js | 15 +++++++++ .../test/integration/spec/payload-transfom.js | 9 +++++ .../test/integration/spec/messages.js | 7 ++-- tooling/generate-kms-caroots.js | 21 ++++++++++-- tooling/with-kms-caroots.sh | 21 ++++++++---- 9 files changed, 90 insertions(+), 49 deletions(-) create mode 100644 packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json diff --git a/packages/@webex/internal-plugin-encryption/README.md b/packages/@webex/internal-plugin-encryption/README.md index 82f978dd014..a4730753b54 100644 --- a/packages/@webex/internal-plugin-encryption/README.md +++ b/packages/@webex/internal-plugin-encryption/README.md @@ -68,17 +68,16 @@ yarn caroots:generate node tooling/generate-kms-caroots.js --stdout ``` -It requires the `openssl` binary on `PATH`. The output can be passed directly as -`config.encryption.caroots`, or exposed via the `WEBEX_KMS_CAROOTS` environment -variable (a JSON array), which the SDK reads as the default for `caroots`: - -```bash -export WEBEX_KMS_CAROOTS="$(node tooling/generate-kms-caroots.js --stdout)" -``` - -The `tooling/with-kms-caroots.sh` wrapper generates the roots and sets that -environment variable for a command, which is how CI runs the integration tests -against the real KMS with validation enabled: +It requires the `openssl` binary on `PATH`. The output is a JSON array that you +pass to `config.encryption.caroots` when constructing the SDK. The SDK itself +does no file or network I/O to obtain roots — supplying them is a build/config +concern for the consuming application (which is important since a prebuilt +library cannot read files in the browser). + +For this repo's own tests, the `tooling/with-kms-caroots.sh` wrapper generates +the roots into a test-only fixture that the encryption integration/browser tests +bundle, then restores the placeholder afterwards. This is how CI runs those +tests against the real KMS with validation enabled: ```bash tooling/with-kms-caroots.sh yarn workspace @webex/internal-plugin-encryption test:integration diff --git a/packages/@webex/internal-plugin-encryption/src/config.js b/packages/@webex/internal-plugin-encryption/src/config.js index 46b779e220a..8b5dd708e84 100644 --- a/packages/@webex/internal-plugin-encryption/src/config.js +++ b/packages/@webex/internal-plugin-encryption/src/config.js @@ -2,27 +2,6 @@ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. */ -/** - * Reads a base64-encoded CA root bundle (JSON array) from the - * `WEBEX_KMS_CAROOTS` environment variable, if present. This lets consuming - * apps and CI supply roots without baking them into the package. See - * tooling/generate-kms-caroots.js and the plugin README. - * @returns {Array} the parsed CA roots, or undefined when the variable is unset - */ -const getCarootsFromEnv = () => { - const raw = process.env.WEBEX_KMS_CAROOTS; - - if (!raw) { - return undefined; - } - - try { - return JSON.parse(raw); - } catch (error) { - return undefined; - } -}; - export default { encryption: { joseOptions: { @@ -84,14 +63,14 @@ export default { * -----BEGIN/END CERTIFICATE----- lines). Required when * `shouldValidateKMSCertificate` is true. * - * Supplied by the consuming application; the SDK does not ship a bundle. - * Defaults to the `WEBEX_KMS_CAROOTS` environment variable when set (see - * tooling/generate-kms-caroots.js). Cisco first-party clients should source - * these roots from the Cisco Trusted Root Store Union bundle. See the plugin - * README and https://www.cisco.com/security/pki/trs/readme.html for details. + * Supplied by the consuming application at build/config time; the SDK does + * not ship a bundle and does no file/network I/O to obtain one. Cisco + * first-party clients should source these roots from the Cisco Trusted Root + * Store Union bundle. See the plugin README, tooling/generate-kms-caroots.js, + * and https://www.cisco.com/security/pki/trs/readme.html for details. * @type {?string[]} */ - caroots: getCarootsFromEnv(), + caroots: undefined, /** * An additional CA root bundle validated alongside `caroots`. Unlike diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js index 310c1d7e659..df992e169e3 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js @@ -13,6 +13,14 @@ import WebexCore from '@webex/webex-core'; import testUsers from '@webex/test-helper-test-users'; import makeLocalUrl from '@webex/test-helper-make-local-url'; +// CA roots are generated by tooling/generate-kms-caroots.js (see the wrapper +// tooling/with-kms-caroots.sh). When the fixture is empty, validation is skipped. +// eslint-disable-next-line import/no-unresolved, global-require +const caroots = require('./kms-caroots.fixture.json'); +const webexTestConfig = caroots.length + ? {encryption: {caroots, shouldValidateKMSCertificate: true}} + : {encryption: {shouldValidateKMSCertificate: false}}; + describe('Encryption', function () { this.timeout(30000); @@ -26,6 +34,7 @@ describe('Encryption', function () { testUsers.create({count: 1}).then((users) => { user = users[0]; webex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: user.token, }, @@ -184,6 +193,7 @@ describe('Encryption', function () { testUsers.create({count: 1}).then((users) => { otherUser = users[0]; otherWebex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: otherUser.token, }, @@ -495,6 +505,7 @@ describe('Encryption', function () { .then((users) => { complianceUser = users[0]; complianceUser.webex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: complianceUser.token, }, diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json new file mode 100644 index 00000000000..fe51488c706 --- /dev/null +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json @@ -0,0 +1 @@ +[] diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 5aec174abfb..9b8a668d14f 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -14,6 +14,14 @@ import {browserOnly} from '@webex/test-helper-mocha'; const debug = require('debug')('kms'); +// CA roots are generated by tooling/generate-kms-caroots.js (see the wrapper +// tooling/with-kms-caroots.sh). When the fixture is empty, validation is skipped. +// eslint-disable-next-line import/no-unresolved, global-require +const caroots = require('./kms-caroots.fixture.json'); +const webexTestConfig = caroots.length + ? {encryption: {caroots, shouldValidateKMSCertificate: true}} + : {encryption: {shouldValidateKMSCertificate: false}}; + describe('Encryption', function () { this.timeout(30000); describe('KMS', () => { @@ -46,6 +54,7 @@ describe('Encryption', function () { testUsers.create({count: 2, config: {roles: [{name: 'id_full_admin'}]}}).then((users) => { spock = users[0]; webex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: spock.token, }, @@ -55,6 +64,7 @@ describe('Encryption', function () { mccoy = users[1]; mccoy.webex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: mccoy.token, }, @@ -77,6 +87,7 @@ describe('Encryption', function () { .then((users) => { [expiredUser] = users; expiredUser.webex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: 'invalidToken', }, @@ -242,6 +253,7 @@ describe('Encryption', function () { it('rejects normally for users that are not authorized', () => testUsers.create({count: 1}).then(([user]) => { const us = new WebexCore({ + config: webexTestConfig, credentials: { authorization: user.token, }, @@ -533,6 +545,7 @@ describe('Encryption', function () { jim = users[0]; jim.webex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: jim.token, }, @@ -668,6 +681,7 @@ describe('Encryption', function () { before('create test user', () => testUsers.create({count: 1}).then(([u]) => { webex2 = new WebexCore({ + config: webexTestConfig, credentials: { authorization: u.token, }, @@ -744,6 +758,7 @@ describe('Encryption', function () { assert.notEqual(fedUser.orgId, spock.orgId); fedWebex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: fedUser.token, }, diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js index 4a58dd3485a..e4c6e110cdb 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js @@ -8,6 +8,14 @@ import {base64} from '@webex/common'; import WebexCore from '@webex/webex-core'; import testUsers from '@webex/test-helper-test-users'; +// CA roots are generated by tooling/generate-kms-caroots.js (see the wrapper +// tooling/with-kms-caroots.sh). When the fixture is empty, validation is skipped. +// eslint-disable-next-line import/no-unresolved, global-require +const caroots = require('./kms-caroots.fixture.json'); +const webexTestConfig = caroots.length + ? {encryption: {caroots, shouldValidateKMSCertificate: true}} + : {encryption: {shouldValidateKMSCertificate: false}}; + describe('plugin-encryption', () => { let other, webex; @@ -16,6 +24,7 @@ describe('plugin-encryption', () => { other = o; console.log(o); webex = new WebexCore({ + config: webexTestConfig, credentials: { authorization: user.token, }, diff --git a/packages/@webex/plugin-messages/test/integration/spec/messages.js b/packages/@webex/plugin-messages/test/integration/spec/messages.js index 62ec86eee2f..f1228b5a9fe 100644 --- a/packages/@webex/plugin-messages/test/integration/spec/messages.js +++ b/packages/@webex/plugin-messages/test/integration/spec/messages.js @@ -36,8 +36,11 @@ describe.skip('plugin-messages', function () { [actor] = user; [actorEU] = usersEU; - webex = new WebexCore({ credentials: actor.token }); - webexEU = new WebexCore({ credentials: actorEU.token }); + // These tests exercise messaging, not KMS certificate validation. + const config = {encryption: {shouldValidateKMSCertificate: false}}; + + webex = new WebexCore({ config, credentials: actor.token }); + webexEU = new WebexCore({ config, credentials: actorEU.token }); webex.people.get('me').then((person) => { actor = person; diff --git a/tooling/generate-kms-caroots.js b/tooling/generate-kms-caroots.js index 37dd14fdac4..78cec9dddc7 100755 --- a/tooling/generate-kms-caroots.js +++ b/tooling/generate-kms-caroots.js @@ -122,6 +122,23 @@ function download(url) { }); } +// Downloads with a few retries to tolerate transient network errors in CI. +async function downloadWithRetry(url, attempts = 4) { + for (let attempt = 1; ; attempt += 1) { + try { + // eslint-disable-next-line no-await-in-loop + return await download(url); + } catch (error) { + if (attempt >= attempts) { + throw error; + } + + // eslint-disable-next-line no-await-in-loop + await new Promise((resolve) => setTimeout(resolve, attempt * 1000)); + } + } +} + function pemToDer(pem) { return Buffer.from(pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''), 'base64'); } @@ -150,7 +167,7 @@ async function fetchVerifiedAnchors(verbose) { const pems = []; for (const anchor of TRUST_ANCHORS) { - const pem = (await download(anchor.url)).toString('utf8'); + const pem = (await downloadWithRetry(anchor.url)).toString('utf8'); const actual = fingerprintOf(pemToDer(pem)); if (actual !== anchor.fingerprint) { @@ -238,7 +255,7 @@ async function generate(options) { ensureOpenssl(); log(options.verbose, `Downloading bundle: ${options.bundleUrl}`); - const bundle = await download(options.bundleUrl); + const bundle = await downloadWithRetry(options.bundleUrl); const anchorsPem = await fetchVerifiedAnchors(options.verbose); diff --git a/tooling/with-kms-caroots.sh b/tooling/with-kms-caroots.sh index e248974282a..53e305d24cf 100755 --- a/tooling/with-kms-caroots.sh +++ b/tooling/with-kms-caroots.sh @@ -1,9 +1,12 @@ #!/usr/bin/env bash # -# Generates the KMS CA roots (Cisco Trusted Root Store Union bundle) and exposes -# them to the SDK/tests via the WEBEX_KMS_CAROOTS environment variable, then runs -# the given command. Used by CI and locally to run integration tests against the -# real KMS with certificate validation enabled. +# Generates the KMS CA roots (Cisco Trusted Root Store Union bundle) into the +# encryption integration test fixture, runs the given command, then restores the +# committed placeholder. Used by CI and locally to run the encryption +# integration/browser tests against the real KMS with validation enabled. +# +# The fixture is a plain JSON file bundled by the tests; nothing reads it at SDK +# runtime, so the shipped library does no file/network I/O for CA roots. # # Usage: # tooling/with-kms-caroots.sh yarn workspace @webex/internal-plugin-encryption test:integration @@ -11,14 +14,18 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +FIXTURE="${REPO_ROOT}/packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json" if [ "$#" -eq 0 ]; then echo "Usage: tooling/with-kms-caroots.sh [args...]" >&2 exit 1 fi +# Restore the committed placeholder on exit so generated roots are never committed. +trap 'printf "[]\n" > "${FIXTURE}"' EXIT + echo "Generating KMS CA roots..." >&2 -WEBEX_KMS_CAROOTS="$(node "${SCRIPT_DIR}/generate-kms-caroots.js" --stdout)" -export WEBEX_KMS_CAROOTS +node "${SCRIPT_DIR}/generate-kms-caroots.js" --out "${FIXTURE}" >&2 -exec "$@" +"$@" From a53c931e98ef0419d16bfb38e154ff5ad6d54f20 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 22 Sep 2026 15:12:21 +0100 Subject: [PATCH 05/33] fix(internal-plugin-encryption): generate KMS CA roots in test scripts, not CI The pull_request_target workflow runs the base branch's YAML, so a wrapper prefix added in the PR never executes. Move CA-roots generation into the encryption package's test:integration and test:browser scripts (which run from PR code) and revert the redundant workflow wrapper. --- .github/workflows/pull-request.yml | 4 ++-- packages/@webex/internal-plugin-encryption/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 8f454601c56..ff35895439a 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -144,7 +144,7 @@ jobs: run: yarn workspaces foreach --parallel --topological --verbose run build:src - name: Test - Browser - run: tooling/with-kms-caroots.sh yarn workspaces foreach --verbose --exclude webex-js-sdk --include '${{ needs.generate-package-matrix.outputs.changed }}' run test:browser + run: yarn workspaces foreach --verbose --exclude webex-js-sdk --include '${{ needs.generate-package-matrix.outputs.changed }}' run test:browser test-coverage: @@ -235,7 +235,7 @@ jobs: run: yarn workspaces foreach --parallel --topological --verbose run build:src - name: Test - Integration - run: tooling/with-kms-caroots.sh yarn workspaces foreach --verbose --exclude webex-js-sdk --include '${{ needs.generate-package-matrix.outputs.changed }}' run test:integration + run: yarn workspaces foreach --verbose --exclude webex-js-sdk --include '${{ needs.generate-package-matrix.outputs.changed }}' run test:integration test-style: diff --git a/packages/@webex/internal-plugin-encryption/package.json b/packages/@webex/internal-plugin-encryption/package.json index 111acda8633..f68c3779685 100644 --- a/packages/@webex/internal-plugin-encryption/package.json +++ b/packages/@webex/internal-plugin-encryption/package.json @@ -62,8 +62,8 @@ "build:src": "webex-legacy-tools build -dest \"./dist\" -src \"./src\" -js -ts -maps", "deploy:npm": "yarn npm publish", "test": "yarn test:style && yarn test:unit && yarn test:integration && yarn test:browser", - "test:browser": "webex-legacy-tools test --integration --runner karma", - "test:integration": "webex-legacy-tools test --integration --runner mocha", + "test:browser": "../../../tooling/with-kms-caroots.sh webex-legacy-tools test --integration --runner karma", + "test:integration": "../../../tooling/with-kms-caroots.sh webex-legacy-tools test --integration --runner mocha", "test:style": "eslint ./src/**/*.*", "test:unit": "webex-legacy-tools test --unit --runner jest" } From 8a2d7ca33ce71e8379285c09af01099d30e46dc6 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Thu, 24 Sep 2026 11:39:43 +0100 Subject: [PATCH 06/33] feat(kms-caroots): add @webex/kms-caroots package and auto-generate CA roots for tests Add a small, publishable @webex/kms-caroots package (openssl + node, with a webex-kms-caroots bin) that downloads, verifies against pinned Cisco anchors, and decodes the Cisco Trusted Root Store Union bundle into the encryption.caroots format, so consumers can supply roots at their own build time. Incorporate generation into the test runner: @webex/legacy-tools now calls the package before integration/browser tests and configures webex-core so the KMS certificate is validated against the real trust store for every plugin, with no per-spec wiring. Falls back to disabling validation if generation fails. Remove the earlier per-package wrapper, spec fixtures, and message-test opt-out. --- .gitignore | 1 + package.json | 2 +- .../internal-plugin-encryption/README.md | 41 ++-- .../internal-plugin-encryption/package.json | 4 +- .../test/integration/spec/encryption.js | 11 - .../integration/spec/kms-caroots.fixture.json | 1 - .../test/integration/spec/kms.js | 15 -- .../test/integration/spec/payload-transfom.js | 9 - packages/@webex/kms-caroots/.eslintrc.js | 17 ++ packages/@webex/kms-caroots/README.md | 43 ++++ packages/@webex/kms-caroots/cli.js | 51 +++++ packages/@webex/kms-caroots/index.d.ts | 18 ++ .../@webex/kms-caroots/index.js | 212 +++++++----------- packages/@webex/kms-caroots/package.json | 30 +++ .../test/integration/spec/messages.js | 7 +- packages/legacy/tools/package.json | 1 + packages/legacy/tools/src/index.ts | 3 +- .../tools/src/models/package/package.ts | 63 ++++-- packages/legacy/tools/src/utils/index.ts | 2 + .../tools/src/utils/kms-caroots/index.ts | 3 + .../src/utils/kms-caroots/kms-caroots.ts | 81 +++++++ tooling/with-kms-caroots.sh | 31 --- yarn.lock | 9 + 23 files changed, 402 insertions(+), 253 deletions(-) delete mode 100644 packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json create mode 100644 packages/@webex/kms-caroots/.eslintrc.js create mode 100644 packages/@webex/kms-caroots/README.md create mode 100755 packages/@webex/kms-caroots/cli.js create mode 100644 packages/@webex/kms-caroots/index.d.ts rename tooling/generate-kms-caroots.js => packages/@webex/kms-caroots/index.js (51%) mode change 100755 => 100644 create mode 100644 packages/@webex/kms-caroots/package.json create mode 100644 packages/legacy/tools/src/utils/kms-caroots/index.ts create mode 100644 packages/legacy/tools/src/utils/kms-caroots/kms-caroots.ts delete mode 100755 tooling/with-kms-caroots.sh diff --git a/.gitignore b/.gitignore index e67e97a8450..dd682920b40 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ tasks/selenium .github-publish .idea .kms-caroots.json +.kms-caroots.bootstrap.js .npm .nvm .python-version diff --git a/package.json b/package.json index e08e0c57533..173caecc75a 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "test:integration": "node ./tooling/index.js test --integration --browser", "test:ci:github": "node ./tooling/index.js ci --github", "test:ci:integration": "node ./tooling/index.js ci --integration", - "caroots:generate": "node ./tooling/generate-kms-caroots.js", + "caroots:generate": "webex-kms-caroots", "distsrc": "find ./packages -name 'package.json' -print0 | xargs -0 sed -ibak 's#\"main\": \"dist/index.js#\"main\": \"src/index.js#' && find ./packages -name '*bak' -print0 | xargs -0 rimraf", "srcdist": "find ./packages -name 'package.json' -print0 | xargs -0 sed -ibak 's#\"main\": \"src/index.js#\"main\": \"dist/index.js#' && find ./packages -name '*bak' -print0 | xargs -0 rimraf", "get-current-version": "node ./tooling/index.js version current", diff --git a/packages/@webex/internal-plugin-encryption/README.md b/packages/@webex/internal-plugin-encryption/README.md index a4730753b54..d5a014da8f7 100644 --- a/packages/@webex/internal-plugin-encryption/README.md +++ b/packages/@webex/internal-plugin-encryption/README.md @@ -56,33 +56,36 @@ Trusted Root Store, using the **Union** bundle: ### Generating the CA roots -This repo ships a tool that downloads the Cisco Union bundle, verifies its -signature against the pinned Cisco trust anchors, and decodes it into the -`caroots` format (an array of raw base64-encoded certificates): +Use the [`@webex/kms-caroots`](https://github.com/webex/webex-js-sdk/tree/master/packages/%40webex/kms-caroots) +package, which downloads the Cisco Union bundle, verifies its signature against +the pinned Cisco trust anchors, and decodes it into the `caroots` format (an +array of raw base64-encoded certificates). It requires the `openssl` binary on +`PATH`. ```bash -# Write ./.kms-caroots.json -yarn caroots:generate +# Print the JSON array to stdout +npx webex-kms-caroots -# Or print the JSON array to stdout -node tooling/generate-kms-caroots.js --stdout +# Or write it to a file +npx webex-kms-caroots --out ./caroots.json ``` -It requires the `openssl` binary on `PATH`. The output is a JSON array that you -pass to `config.encryption.caroots` when constructing the SDK. The SDK itself -does no file or network I/O to obtain roots — supplying them is a build/config -concern for the consuming application (which is important since a prebuilt -library cannot read files in the browser). - -For this repo's own tests, the `tooling/with-kms-caroots.sh` wrapper generates -the roots into a test-only fixture that the encryption integration/browser tests -bundle, then restores the placeholder afterwards. This is how CI runs those -tests against the real KMS with validation enabled: +```js +const {generateKmsCaroots} = require('@webex/kms-caroots'); -```bash -tooling/with-kms-caroots.sh yarn workspace @webex/internal-plugin-encryption test:integration +const caroots = await generateKmsCaroots(); +const webex = new WebexCore({config: {encryption: {caroots}}}); ``` +The SDK itself does no file or network I/O to obtain roots — supplying them is a +build/config concern for the consuming application (important since a prebuilt +library cannot read files in the browser). + +The SDK's own integration/browser tests generate these roots automatically: the +test runner (`@webex/legacy-tools`) calls `@webex/kms-caroots` and configures +webex-core before the tests run, so the KMS certificate is validated against the +real trust store. + ### Configuring manually The referenced Cisco page is authoritative for how to download, verify, and diff --git a/packages/@webex/internal-plugin-encryption/package.json b/packages/@webex/internal-plugin-encryption/package.json index f68c3779685..111acda8633 100644 --- a/packages/@webex/internal-plugin-encryption/package.json +++ b/packages/@webex/internal-plugin-encryption/package.json @@ -62,8 +62,8 @@ "build:src": "webex-legacy-tools build -dest \"./dist\" -src \"./src\" -js -ts -maps", "deploy:npm": "yarn npm publish", "test": "yarn test:style && yarn test:unit && yarn test:integration && yarn test:browser", - "test:browser": "../../../tooling/with-kms-caroots.sh webex-legacy-tools test --integration --runner karma", - "test:integration": "../../../tooling/with-kms-caroots.sh webex-legacy-tools test --integration --runner mocha", + "test:browser": "webex-legacy-tools test --integration --runner karma", + "test:integration": "webex-legacy-tools test --integration --runner mocha", "test:style": "eslint ./src/**/*.*", "test:unit": "webex-legacy-tools test --unit --runner jest" } diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js index df992e169e3..310c1d7e659 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js @@ -13,14 +13,6 @@ import WebexCore from '@webex/webex-core'; import testUsers from '@webex/test-helper-test-users'; import makeLocalUrl from '@webex/test-helper-make-local-url'; -// CA roots are generated by tooling/generate-kms-caroots.js (see the wrapper -// tooling/with-kms-caroots.sh). When the fixture is empty, validation is skipped. -// eslint-disable-next-line import/no-unresolved, global-require -const caroots = require('./kms-caroots.fixture.json'); -const webexTestConfig = caroots.length - ? {encryption: {caroots, shouldValidateKMSCertificate: true}} - : {encryption: {shouldValidateKMSCertificate: false}}; - describe('Encryption', function () { this.timeout(30000); @@ -34,7 +26,6 @@ describe('Encryption', function () { testUsers.create({count: 1}).then((users) => { user = users[0]; webex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: user.token, }, @@ -193,7 +184,6 @@ describe('Encryption', function () { testUsers.create({count: 1}).then((users) => { otherUser = users[0]; otherWebex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: otherUser.token, }, @@ -505,7 +495,6 @@ describe('Encryption', function () { .then((users) => { complianceUser = users[0]; complianceUser.webex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: complianceUser.token, }, diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json deleted file mode 100644 index fe51488c706..00000000000 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 9b8a668d14f..5aec174abfb 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -14,14 +14,6 @@ import {browserOnly} from '@webex/test-helper-mocha'; const debug = require('debug')('kms'); -// CA roots are generated by tooling/generate-kms-caroots.js (see the wrapper -// tooling/with-kms-caroots.sh). When the fixture is empty, validation is skipped. -// eslint-disable-next-line import/no-unresolved, global-require -const caroots = require('./kms-caroots.fixture.json'); -const webexTestConfig = caroots.length - ? {encryption: {caroots, shouldValidateKMSCertificate: true}} - : {encryption: {shouldValidateKMSCertificate: false}}; - describe('Encryption', function () { this.timeout(30000); describe('KMS', () => { @@ -54,7 +46,6 @@ describe('Encryption', function () { testUsers.create({count: 2, config: {roles: [{name: 'id_full_admin'}]}}).then((users) => { spock = users[0]; webex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: spock.token, }, @@ -64,7 +55,6 @@ describe('Encryption', function () { mccoy = users[1]; mccoy.webex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: mccoy.token, }, @@ -87,7 +77,6 @@ describe('Encryption', function () { .then((users) => { [expiredUser] = users; expiredUser.webex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: 'invalidToken', }, @@ -253,7 +242,6 @@ describe('Encryption', function () { it('rejects normally for users that are not authorized', () => testUsers.create({count: 1}).then(([user]) => { const us = new WebexCore({ - config: webexTestConfig, credentials: { authorization: user.token, }, @@ -545,7 +533,6 @@ describe('Encryption', function () { jim = users[0]; jim.webex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: jim.token, }, @@ -681,7 +668,6 @@ describe('Encryption', function () { before('create test user', () => testUsers.create({count: 1}).then(([u]) => { webex2 = new WebexCore({ - config: webexTestConfig, credentials: { authorization: u.token, }, @@ -758,7 +744,6 @@ describe('Encryption', function () { assert.notEqual(fedUser.orgId, spock.orgId); fedWebex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: fedUser.token, }, diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js index e4c6e110cdb..4a58dd3485a 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js @@ -8,14 +8,6 @@ import {base64} from '@webex/common'; import WebexCore from '@webex/webex-core'; import testUsers from '@webex/test-helper-test-users'; -// CA roots are generated by tooling/generate-kms-caroots.js (see the wrapper -// tooling/with-kms-caroots.sh). When the fixture is empty, validation is skipped. -// eslint-disable-next-line import/no-unresolved, global-require -const caroots = require('./kms-caroots.fixture.json'); -const webexTestConfig = caroots.length - ? {encryption: {caroots, shouldValidateKMSCertificate: true}} - : {encryption: {shouldValidateKMSCertificate: false}}; - describe('plugin-encryption', () => { let other, webex; @@ -24,7 +16,6 @@ describe('plugin-encryption', () => { other = o; console.log(o); webex = new WebexCore({ - config: webexTestConfig, credentials: { authorization: user.token, }, diff --git a/packages/@webex/kms-caroots/.eslintrc.js b/packages/@webex/kms-caroots/.eslintrc.js new file mode 100644 index 00000000000..68a71d14cde --- /dev/null +++ b/packages/@webex/kms-caroots/.eslintrc.js @@ -0,0 +1,17 @@ +module.exports = { + root: true, + ignorePatterns: ['*.d.ts'], + env: { + node: true, + es2021: true, + }, + parserOptions: { + ecmaVersion: 2021, + sourceType: 'script', + }, + extends: ['eslint:recommended'], + rules: { + 'no-console': 'off', + 'no-plusplus': 'off', + }, +}; diff --git a/packages/@webex/kms-caroots/README.md b/packages/@webex/kms-caroots/README.md new file mode 100644 index 00000000000..82084408629 --- /dev/null +++ b/packages/@webex/kms-caroots/README.md @@ -0,0 +1,43 @@ +# @webex/kms-caroots + +> Generate the Cisco Trusted Root Store CA roots used to validate the Webex KMS certificate. + +The Webex SDK validates the KMS certificate chain against a set of trusted CA +roots supplied via `config.encryption.caroots`. The SDK does not ship a bundle, +so that certificate updates don't require an SDK upgrade. This tool downloads the +Cisco Trusted Root Store **Union** bundle, verifies its signature against the +pinned Cisco trust anchors, and decodes it into the `caroots` format (an array of +raw base64-encoded certificates). + +See for the Cisco Trusted +Root Store. + +Requires the `openssl` binary on `PATH`. + +## CLI + +```bash +# Print the JSON array to stdout +npx webex-kms-caroots + +# Write it to a file +npx webex-kms-caroots --out ./caroots.json +``` + +## Programmatic + +```js +const {generateKmsCaroots} = require('@webex/kms-caroots'); + +const caroots = await generateKmsCaroots(); + +const webex = new WebexCore({ + config: { + encryption: {caroots}, + }, +}); +``` + +## License + +© 2026 Cisco and/or its affiliates. All Rights Reserved. diff --git a/packages/@webex/kms-caroots/cli.js b/packages/@webex/kms-caroots/cli.js new file mode 100755 index 00000000000..9f7c0bd1489 --- /dev/null +++ b/packages/@webex/kms-caroots/cli.js @@ -0,0 +1,51 @@ +#!/usr/bin/env node +/*! + * Copyright (c) 2026 Cisco Systems, Inc. See LICENSE file. + */ + +/* eslint-disable no-console */ + +const fs = require('fs'); + +const {generateKmsCaroots} = require('./index'); + +const HELP = `Generate the Webex KMS CA roots (Cisco Trusted Root Store Union bundle). + +Usage: + webex-kms-caroots [--out ] + +Options: + --out Write the JSON array to a file instead of stdout + -h, --help Show this help + +Requires the 'openssl' binary on PATH. The output is an array of raw +base64-encoded certificates suitable for the SDK's encryption.caroots config. +`; + +async function main() { + const args = process.argv.slice(2); + + if (args.includes('-h') || args.includes('--help')) { + process.stdout.write(HELP); + + return; + } + + const outIndex = args.indexOf('--out'); + const out = outIndex === -1 ? undefined : args[outIndex + 1]; + + const caroots = await generateKmsCaroots(); + const json = JSON.stringify(caroots); + + if (out) { + fs.writeFileSync(out, json); + console.error(`Wrote ${caroots.length} CA roots to ${out}`); + } else { + process.stdout.write(json); + } +} + +main().catch((error) => { + console.error(`webex-kms-caroots: ${error.message}`); + process.exit(1); +}); diff --git a/packages/@webex/kms-caroots/index.d.ts b/packages/@webex/kms-caroots/index.d.ts new file mode 100644 index 00000000000..f9701bade18 --- /dev/null +++ b/packages/@webex/kms-caroots/index.d.ts @@ -0,0 +1,18 @@ +/*! + * Copyright (c) 2026 Cisco Systems, Inc. See LICENSE file. + */ + +export interface GenerateKmsCarootsOptions { + /** Override the Cisco bundle URL. Defaults to the Union bundle. */ + bundleUrl?: string; +} + +/** + * Downloads, verifies, and decodes the Cisco Trusted Root Store "Union" bundle + * into the `encryption.caroots` format: an array of raw base64-encoded (DER) + * certificates. Requires the `openssl` binary on PATH. + */ +export function generateKmsCaroots(options?: GenerateKmsCarootsOptions): Promise; + +/** The default Cisco Trusted Root Store "Union" bundle URL. */ +export const DEFAULT_BUNDLE_URL: string; diff --git a/tooling/generate-kms-caroots.js b/packages/@webex/kms-caroots/index.js old mode 100755 new mode 100644 similarity index 51% rename from tooling/generate-kms-caroots.js rename to packages/@webex/kms-caroots/index.js index 78cec9dddc7..dfa4d6b385e --- a/tooling/generate-kms-caroots.js +++ b/packages/@webex/kms-caroots/index.js @@ -1,28 +1,5 @@ -#!/usr/bin/env node /*! - * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. - */ - -/* eslint-disable no-console */ - -/** - * Downloads, verifies, and decodes the Cisco Trusted Root Store "Union" bundle - * into the format expected by the `encryption.caroots` SDK config option: an - * array of raw base64-encoded (DER) certificates. - * - * The bundle is intentionally NOT committed to the package. Consuming apps (and - * this repo's CI) run this tool to produce a fresh list so certificate updates - * don't require an SDK upgrade. See the Cisco Trusted Root Store for details: - * https://www.cisco.com/security/pki/trs/readme.html - * - * Usage: - * node tooling/generate-kms-caroots.js # writes ./.kms-caroots.json - * node tooling/generate-kms-caroots.js --stdout # prints JSON to stdout - * node tooling/generate-kms-caroots.js --out roots.json - * node tooling/generate-kms-caroots.js --url # override bundle URL - * - * Requires the `openssl` binary on PATH (used to verify the signed PKCS#7 - * bundle and extract its certificates, as documented by Cisco). + * Copyright (c) 2026 Cisco Systems, Inc. See LICENSE file. */ const crypto = require('crypto'); @@ -36,9 +13,9 @@ const {URL} = require('url'); const DEFAULT_BUNDLE_URL = 'https://www.cisco.com/security/pki/trs/current/ios_union/ios_union.p7b'; -// Trust anchors used to verify the signed bundle. Pinned by SHA-256 of the DER -// certificate so a compromised or swapped anchor is rejected. New-style bundles -// chain to the TRS Bundle Root CA; older bundles chain to Cisco Root CA M1. +// Trust anchors used to verify the signed bundle, pinned by the SHA-256 of the +// DER certificate so a compromised or swapped anchor is rejected. New-style +// bundles chain to the TRS Bundle Root CA; older bundles chain to Cisco Root CA M1. const TRUST_ANCHORS = [ { name: 'Cisco TRS Bundle Root CA', @@ -56,49 +33,15 @@ const TRUST_ANCHORS = [ const PEM_CERT_RE = /-----BEGIN CERTIFICATE-----([\s\S]*?)-----END CERTIFICATE-----/g; -function parseArgs(argv) { - const options = {bundleUrl: DEFAULT_BUNDLE_URL, out: '.kms-caroots.json', stdout: false, verbose: false}; - - for (let i = 0; i < argv.length; i += 1) { - const arg = argv[i]; - - switch (arg) { - case '--stdout': - options.stdout = true; - break; - case '--out': - options.out = argv[(i += 1)]; - break; - case '--url': - options.bundleUrl = argv[(i += 1)]; - break; - case '-v': - case '--verbose': - options.verbose = true; - break; - case '-h': - case '--help': - options.help = true; - break; - default: - throw new Error(`Unknown argument: ${arg}`); - } - } - - return options; -} - -// Log to stderr so `--stdout` emits only the JSON payload. -function log(verbose, ...args) { - if (verbose) { - console.error(...args); - } -} - +/** + * Download a URL, following redirects. + * @param {string} url + * @returns {Promise} + */ function download(url) { return new Promise((resolve, reject) => { https - .get(url, {headers: {'user-agent': 'webex-js-sdk-kms-caroots'}}, (res) => { + .get(url, {headers: {'user-agent': 'webex-kms-caroots'}}, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { res.resume(); resolve(download(new URL(res.headers.location, url).toString())); @@ -122,7 +65,12 @@ function download(url) { }); } -// Downloads with a few retries to tolerate transient network errors in CI. +/** + * Download a URL with retries to tolerate transient network errors. + * @param {string} url + * @param {number} [attempts] + * @returns {Promise} + */ async function downloadWithRetry(url, attempts = 4) { for (let attempt = 1; ; attempt += 1) { try { @@ -139,10 +87,20 @@ async function downloadWithRetry(url, attempts = 4) { } } +/** + * Convert a PEM certificate to its DER Buffer. + * @param {string} pem + * @returns {Buffer} + */ function pemToDer(pem) { return Buffer.from(pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''), 'base64'); } +/** + * Compute the colon-delimited SHA-256 fingerprint of a DER certificate. + * @param {Buffer} der + * @returns {string} + */ function fingerprintOf(der) { return crypto .createHash('sha256') @@ -153,6 +111,10 @@ function fingerprintOf(der) { .join(':'); } +/** + * Ensure the `openssl` binary is available. + * @returns {void} + */ function ensureOpenssl() { try { execFileSync('openssl', ['version'], {stdio: 'ignore'}); @@ -163,10 +125,15 @@ function ensureOpenssl() { } } -async function fetchVerifiedAnchors(verbose) { +/** + * Download the pinned trust anchors and verify their fingerprints. + * @returns {Promise} concatenated anchor PEMs + */ +async function fetchVerifiedAnchors() { const pems = []; for (const anchor of TRUST_ANCHORS) { + // eslint-disable-next-line no-await-in-loop const pem = (await downloadWithRetry(anchor.url)).toString('utf8'); const actual = fingerprintOf(pemToDer(pem)); @@ -176,15 +143,20 @@ async function fetchVerifiedAnchors(verbose) { ); } - log(verbose, `Verified trust anchor: ${anchor.name}`); pems.push(pem.trim()); } return pems.join('\n'); } -// Verifies the signed bundle against the pinned anchors and returns the -// contained certificates as PEM using the openssl pipeline documented by Cisco. +/** + * Verify the signed bundle against the anchors and return the contained + * certificates as PEM, using the openssl pipeline documented by Cisco. + * @param {string} workDir + * @param {Buffer} bundle + * @param {string} anchorsPem + * @returns {string} + */ function verifyAndExtract(workDir, bundle, anchorsPem) { const bundlePath = path.join(workDir, 'bundle.p7b'); const anchorsPath = path.join(workDir, 'anchors.pem'); @@ -193,20 +165,11 @@ function verifyAndExtract(workDir, bundle, anchorsPem) { fs.writeFileSync(bundlePath, bundle); fs.writeFileSync(anchorsPath, anchorsPem); - execFileSync('openssl', [ - 'cms', - '-verify', - '-inform', - 'DER', - '-purpose', - 'any', - '-in', - bundlePath, - '-CAfile', - anchorsPath, - '-out', - contentPath, - ]); + execFileSync( + 'openssl', + ['cms', '-verify', '-inform', 'DER', '-purpose', 'any', '-in', bundlePath, '-CAfile', anchorsPath, '-out', contentPath], + {stdio: ['ignore', 'ignore', 'ignore']} + ); return execFileSync('openssl', [ 'pkcs7', @@ -220,6 +183,11 @@ function verifyAndExtract(workDir, bundle, anchorsPem) { ]).toString('utf8'); } +/** + * Decode extracted PEM certificates into an array of raw base64 (DER) strings. + * @param {string} certsPem + * @returns {string[]} + */ function decodeCertificates(certsPem) { const seen = new Set(); const caroots = []; @@ -229,9 +197,8 @@ function decodeCertificates(certsPem) { while ((match = PEM_CERT_RE.exec(certsPem))) { const base64 = match[1].replace(/\s+/g, ''); - if (!base64 || seen.has(base64)) { - continue; - } + // eslint-disable-next-line no-continue + if (!base64 || seen.has(base64)) continue; const der = Buffer.from(base64, 'base64'); @@ -251,62 +218,35 @@ function decodeCertificates(certsPem) { return caroots; } -async function generate(options) { - ensureOpenssl(); - - log(options.verbose, `Downloading bundle: ${options.bundleUrl}`); - const bundle = await downloadWithRetry(options.bundleUrl); +/** + * Download, verify, and decode the Cisco Trusted Root Store "Union" bundle into + * the format expected by the `encryption.caroots` Webex SDK config option: an + * array of raw base64-encoded (DER) certificates. + * + * Requires the `openssl` binary on PATH. + * + * @param {{bundleUrl?: string}} [options] + * @returns {Promise} + */ +async function generateKmsCaroots(options = {}) { + const bundleUrl = options.bundleUrl || DEFAULT_BUNDLE_URL; - const anchorsPem = await fetchVerifiedAnchors(options.verbose); + ensureOpenssl(); + const bundle = await downloadWithRetry(bundleUrl); + const anchorsPem = await fetchVerifiedAnchors(); const workDir = fs.mkdtempSync(path.join(os.tmpdir(), 'kms-caroots-')); try { const certsPem = verifyAndExtract(workDir, bundle, anchorsPem); - const caroots = decodeCertificates(certsPem); - log(options.verbose, `Verified bundle and extracted ${caroots.length} certificates`); - - return caroots; + return decodeCertificates(certsPem); } finally { fs.rmSync(workDir, {recursive: true, force: true}); } } -const HELP = `Generate the KMS CA roots (Cisco Trusted Root Store Union bundle). - -Usage: - node tooling/generate-kms-caroots.js [options] - -Options: - --stdout Print the JSON array to stdout instead of writing a file - --out Output file path (default: .kms-caroots.json) - --url Override the bundle URL - -v, --verbose Log progress to stderr - -h, --help Show this help -`; - -async function main() { - const options = parseArgs(process.argv.slice(2)); - - if (options.help) { - process.stdout.write(HELP); - - return; - } - - const caroots = await generate(options); - const json = JSON.stringify(caroots); - - if (options.stdout) { - process.stdout.write(json); - } else { - fs.writeFileSync(options.out, json); - console.error(`Wrote ${caroots.length} CA roots to ${options.out}`); - } -} - -main().catch((error) => { - console.error(`generate-kms-caroots: ${error.message}`); - process.exit(1); -}); +module.exports = { + generateKmsCaroots, + DEFAULT_BUNDLE_URL, +}; diff --git a/packages/@webex/kms-caroots/package.json b/packages/@webex/kms-caroots/package.json new file mode 100644 index 00000000000..7b147fe3602 --- /dev/null +++ b/packages/@webex/kms-caroots/package.json @@ -0,0 +1,30 @@ +{ + "name": "@webex/kms-caroots", + "description": "Generate the Cisco Trusted Root Store CA roots used to validate the Webex KMS certificate.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/webex/webex-js-sdk.git", + "directory": "packages/@webex/kms-caroots" + }, + "engines": { + "node": ">=18" + }, + "type": "commonjs", + "main": "./index.js", + "types": "./index.d.ts", + "bin": { + "webex-kms-caroots": "./cli.js" + }, + "files": [ + "index.js", + "index.d.ts", + "cli.js", + "README.md" + ], + "scripts": { + "build:src": "exit 0", + "deploy:npm": "yarn npm publish", + "test:style": "eslint ./*.js" + } +} diff --git a/packages/@webex/plugin-messages/test/integration/spec/messages.js b/packages/@webex/plugin-messages/test/integration/spec/messages.js index f1228b5a9fe..62ec86eee2f 100644 --- a/packages/@webex/plugin-messages/test/integration/spec/messages.js +++ b/packages/@webex/plugin-messages/test/integration/spec/messages.js @@ -36,11 +36,8 @@ describe.skip('plugin-messages', function () { [actor] = user; [actorEU] = usersEU; - // These tests exercise messaging, not KMS certificate validation. - const config = {encryption: {shouldValidateKMSCertificate: false}}; - - webex = new WebexCore({ config, credentials: actor.token }); - webexEU = new WebexCore({ config, credentials: actorEU.token }); + webex = new WebexCore({ credentials: actor.token }); + webexEU = new WebexCore({ credentials: actorEU.token }); webex.people.get('me').then((person) => { actor = person; diff --git a/packages/legacy/tools/package.json b/packages/legacy/tools/package.json index bf6da9ea455..2037774a5b0 100644 --- a/packages/legacy/tools/package.json +++ b/packages/legacy/tools/package.json @@ -75,6 +75,7 @@ "@webex/babel-config-legacy": "workspace:*", "@webex/cli-tools": "workspace:*", "@webex/env-config-legacy": "workspace:*", + "@webex/kms-caroots": "workspace:*", "babelify": "^10.0.0", "browserify": "^17.0.0", "browserify-middleware": "^8.1.1", diff --git a/packages/legacy/tools/src/index.ts b/packages/legacy/tools/src/index.ts index 30039dedc45..61a61aae8ab 100644 --- a/packages/legacy/tools/src/index.ts +++ b/packages/legacy/tools/src/index.ts @@ -7,7 +7,7 @@ import { build, runTests } from './commands'; import { Package, PackageFile } from './models'; import { - Jest, Karma, Mocha, startServer, + Jest, Karma, KmsCaroots, Mocha, startServer, stopServer, findWorkspaceRoot, getServerPath, } from './utils'; @@ -25,6 +25,7 @@ export { runTests, Jest, Karma, + KmsCaroots, Mocha, Package, PackageFile, diff --git a/packages/legacy/tools/src/models/package/package.ts b/packages/legacy/tools/src/models/package/package.ts index 075d575cbea..db1d4747f13 100644 --- a/packages/legacy/tools/src/models/package/package.ts +++ b/packages/legacy/tools/src/models/package/package.ts @@ -1,7 +1,9 @@ import glob from 'glob'; import path from 'path'; -import { Jest, Karma, Mocha } from '../../utils'; +import { + Jest, Karma, KmsCaroots, Mocha, +} from '../../utils'; import PackageFile from '../package-file'; @@ -124,32 +126,49 @@ class Package { return Promise.all([unitTestFileCollector, integrationTestFileCollector]) .then(async ([unitFiles, integrationFiles]) => { - if (config.runner === 'jest') { - const testFiles = [...unitFiles]; - - if (testFiles.length > 0) { - await Jest.test({ files: testFiles }); + // Integration tests validate the KMS certificate chain, so supply the + // real CA roots by configuring webex-core before any test constructs a + // WebexCore instance. Prepended so it runs first under mocha and karma. + const needsCaroots = config.integration + && integrationFiles.length > 0 + && (config.runner === 'mocha' || config.runner === 'karma'); + const carootsBootstrap = needsCaroots + ? await KmsCaroots.prepareTestBootstrap(this.data.packageRoot) + : undefined; + const bootstrapFiles = carootsBootstrap ? [carootsBootstrap.file] : []; + + try { + if (config.runner === 'jest') { + const testFiles = [...unitFiles]; + + if (testFiles.length > 0) { + await Jest.test({ files: testFiles }); + } } - } - if (config.runner === 'mocha') { - const testFiles = [...unitFiles, ...integrationFiles]; + if (config.runner === 'mocha') { + const testFiles = [...unitFiles, ...integrationFiles]; - if (testFiles.length > 0) { - await Mocha.test({ files: testFiles }); + if (testFiles.length > 0) { + await Mocha.test({ files: [...bootstrapFiles, ...testFiles] }); + } } - } - if (config.runner === 'karma') { - const testFiles = [...unitFiles, ...integrationFiles]; - - if (testFiles.length > 0) { - await Karma.test({ - browsers: config.karmaBrowsers, - debug: config.karmaDebug, - files: testFiles, - port: config.karmaPort, - }); + if (config.runner === 'karma') { + const testFiles = [...unitFiles, ...integrationFiles]; + + if (testFiles.length > 0) { + await Karma.test({ + browsers: config.karmaBrowsers, + debug: config.karmaDebug, + files: [...bootstrapFiles, ...testFiles], + port: config.karmaPort, + }); + } + } + } finally { + if (carootsBootstrap) { + carootsBootstrap.cleanup(); } } diff --git a/packages/legacy/tools/src/utils/index.ts b/packages/legacy/tools/src/utils/index.ts index b5bd5c26441..190d7ef3403 100644 --- a/packages/legacy/tools/src/utils/index.ts +++ b/packages/legacy/tools/src/utils/index.ts @@ -1,5 +1,6 @@ import Jest from './jest'; import Karma from './karma'; +import KmsCaroots from './kms-caroots'; import { startServer, stopServer, @@ -11,6 +12,7 @@ import Mocha from './mocha'; export { Jest, Karma, + KmsCaroots, Mocha, startServer, stopServer, diff --git a/packages/legacy/tools/src/utils/kms-caroots/index.ts b/packages/legacy/tools/src/utils/kms-caroots/index.ts new file mode 100644 index 00000000000..86dff502b02 --- /dev/null +++ b/packages/legacy/tools/src/utils/kms-caroots/index.ts @@ -0,0 +1,3 @@ +import KmsCaroots from './kms-caroots'; + +export default KmsCaroots; diff --git a/packages/legacy/tools/src/utils/kms-caroots/kms-caroots.ts b/packages/legacy/tools/src/utils/kms-caroots/kms-caroots.ts new file mode 100644 index 00000000000..25eeed946fa --- /dev/null +++ b/packages/legacy/tools/src/utils/kms-caroots/kms-caroots.ts @@ -0,0 +1,81 @@ +/*! + * Copyright (c) 2026 Cisco Systems, Inc. See LICENSE file. + */ + +/* eslint-disable no-console */ + +import * as fs from 'fs'; +import * as path from 'path'; + +// The generated bootstrap file, required first so it can configure the shared +// webex-core config before any WebexCore instance is constructed. +const BOOTSTRAP_FILENAME = '.kms-caroots.bootstrap.js'; + +/** + * Supplies the KMS CA roots to integration tests. + * + * @remarks + * Generation is delegated to the publishable `@webex/kms-caroots` package (the + * same tool consumers use). The roots are written into a bootstrap file that is + * loaded before the tests so it can configure the shared webex-core config; the + * KMS certificate is then validated against the real trust store. + * + * @public + */ +class KmsCaroots { + /** + * Generates the CA roots and writes a bootstrap file into a package under test + * that configures the shared webex-core config before any WebexCore instance + * is constructed. If generation fails (e.g. no network or openssl), it instead + * disables KMS certificate validation so the suite can still run. + * + * @param packageRoot - The root directory of the package under test. + * @returns - Promise resolving to the bootstrap file path and a cleanup + * function. + */ + public static async prepareTestBootstrap( + packageRoot: string, + ): Promise<{ file: string; cleanup: () => void }> { + const file = path.join(packageRoot, BOOTSTRAP_FILENAME); + let body: string; + + try { + // eslint-disable-next-line global-require, @typescript-eslint/no-var-requires + const { generateKmsCaroots } = require('@webex/kms-caroots'); + const caroots = await generateKmsCaroots(); + + console.log(`KMS CA roots: validating the KMS certificate against ${caroots.length} roots`); + body = `config.encryption.caroots = ${JSON.stringify(caroots)};`; + } catch (error) { + console.warn( + `KMS CA roots: generation failed (${(error as Error).message}); skipping KMS cert validation`, + ); + body = 'config.encryption.shouldValidateKMSCertificate = false;'; + } + + const contents = `/* Generated by @webex/legacy-tools for KMS certificate validation. Do not edit or commit. */ +try { + var config = require('@webex/webex-core/dist/config').default; + config.encryption = config.encryption || {}; + ${body} +} catch (error) { + // webex-core is not part of this package's tests; nothing to configure. +} +`; + + fs.writeFileSync(file, contents); + + return { + file, + cleanup: () => { + try { + fs.unlinkSync(file); + } catch (error) { + // ignore + } + }, + }; + } +} + +export default KmsCaroots; diff --git a/tooling/with-kms-caroots.sh b/tooling/with-kms-caroots.sh deleted file mode 100755 index 53e305d24cf..00000000000 --- a/tooling/with-kms-caroots.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# -# Generates the KMS CA roots (Cisco Trusted Root Store Union bundle) into the -# encryption integration test fixture, runs the given command, then restores the -# committed placeholder. Used by CI and locally to run the encryption -# integration/browser tests against the real KMS with validation enabled. -# -# The fixture is a plain JSON file bundled by the tests; nothing reads it at SDK -# runtime, so the shipped library does no file/network I/O for CA roots. -# -# Usage: -# tooling/with-kms-caroots.sh yarn workspace @webex/internal-plugin-encryption test:integration - -set -euo pipefail - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" -FIXTURE="${REPO_ROOT}/packages/@webex/internal-plugin-encryption/test/integration/spec/kms-caroots.fixture.json" - -if [ "$#" -eq 0 ]; then - echo "Usage: tooling/with-kms-caroots.sh [args...]" >&2 - exit 1 -fi - -# Restore the committed placeholder on exit so generated roots are never committed. -trap 'printf "[]\n" > "${FIXTURE}"' EXIT - -echo "Generating KMS CA roots..." >&2 -node "${SCRIPT_DIR}/generate-kms-caroots.js" --out "${FIXTURE}" >&2 - -"$@" diff --git a/yarn.lock b/yarn.lock index 8af37c019b0..63c6d1e1b15 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8554,6 +8554,14 @@ __metadata: languageName: node linkType: hard +"@webex/kms-caroots@workspace:*, @webex/kms-caroots@workspace:packages/@webex/kms-caroots": + version: 0.0.0-use.local + resolution: "@webex/kms-caroots@workspace:packages/@webex/kms-caroots" + bin: + webex-kms-caroots: ./cli.js + languageName: unknown + linkType: soft + "@webex/ladon-ts@npm:^5.10.0": version: 5.10.0 resolution: "@webex/ladon-ts@npm:5.10.0" @@ -8595,6 +8603,7 @@ __metadata: "@webex/esbuild-config": "workspace:*" "@webex/eslint-config": "workspace:*" "@webex/jasmine-config": "workspace:*" + "@webex/kms-caroots": "workspace:*" "@webex/nyc-config": "workspace:*" "@webex/typescript-config": "workspace:*" babelify: ^10.0.0 From 72a2c7f57c92650a5ca5fecbbd7f034dd2b6711b Mon Sep 17 00:00:00 2001 From: Colin Read Date: Thu, 24 Sep 2026 12:03:49 +0100 Subject: [PATCH 07/33] fix(legacy-tools): await Karma completion before cleaning up KMS bootstrap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Karma.test resolved immediately after server.start() (the completion callback, including its reject-on-failure, was dead code), so the finally block deleted the KMS bootstrap while Karma was still browserifying its files — the first test file could disappear or run without the config mutation. Resolve/reject via Karma's completion callback for single-run (only resolve immediately in watch/debug mode), and skip bootstrap cleanup in debug mode. This also restores propagation of Karma exit codes, which were previously swallowed. --- packages/legacy/tools/src/models/package/package.ts | 5 ++++- packages/legacy/tools/src/utils/karma/karma.ts | 11 +++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/legacy/tools/src/models/package/package.ts b/packages/legacy/tools/src/models/package/package.ts index db1d4747f13..17ce261f3b5 100644 --- a/packages/legacy/tools/src/models/package/package.ts +++ b/packages/legacy/tools/src/models/package/package.ts @@ -167,7 +167,10 @@ class Package { } } } finally { - if (carootsBootstrap) { + // Karma.test resolves on run completion (single-run), so this runs + // after the run. In karma watch/debug mode it resolves early and the + // bootstrap must stay, so skip cleanup there (it is gitignored). + if (carootsBootstrap && !config.karmaDebug) { carootsBootstrap.cleanup(); } } diff --git a/packages/legacy/tools/src/utils/karma/karma.ts b/packages/legacy/tools/src/utils/karma/karma.ts index e703a19d6a7..50c250232ca 100644 --- a/packages/legacy/tools/src/utils/karma/karma.ts +++ b/packages/legacy/tools/src/utils/karma/karma.ts @@ -41,7 +41,9 @@ class Karma { .then((parsedConfig: any) => new Promise((resolve, reject) => { const server = new KarmaRunner.Server(parsedConfig, (code: number) => { if (code !== 0) { - reject(); + reject(new Error(`Karma exited with code ${code}`)); + + return; } resolve(undefined); @@ -60,7 +62,12 @@ class Karma { } server.start(); - resolve(server); + + // In watch/debug mode the run never completes, so resolve immediately to + // avoid blocking; single-run resolves via the completion callback above. + if (debug) { + resolve(server); + } })); } From 0823d5980fae5acc2df909662dbeccbae6c57fa5 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Thu, 24 Sep 2026 13:23:48 +0100 Subject: [PATCH 08/33] test(legacy-tools): mock KMS caroots bootstrap in Package.test tests Package.test now prepends a generated CA-roots bootstrap for integration runs. Stub KmsCaroots.prepareTestBootstrap so the tests do no network/file I/O, and expect the bootstrap prepended to the mocha/karma file lists. This also fixes the test-server specs, which were failing as collateral from the un-mocked generation's real I/O and unhandled rejection. --- .../tools/test/integration/package/package.test.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/legacy/tools/test/integration/package/package.test.js b/packages/legacy/tools/test/integration/package/package.test.js index 17da7735a13..e1a6ea0c4c9 100644 --- a/packages/legacy/tools/test/integration/package/package.test.js +++ b/packages/legacy/tools/test/integration/package/package.test.js @@ -4,6 +4,7 @@ const path = require('path'); const { Jest, Karma, + KmsCaroots, Mocha, Package, PackageFile, @@ -159,6 +160,13 @@ describe('Package', () => { spies.Karma = { test: spyOn(Karma, 'test').and.resolveTo(undefined), }; + + // Integration runs prepend a generated CA-roots bootstrap; stub it out + // so the tests do no network or file I/O. + spies.KmsCaroots = { + prepareTestBootstrap: spyOn(KmsCaroots, 'prepareTestBootstrap') + .and.resolveTo({ file: 'bootstrap.js', cleanup: () => undefined }), + }; }); it('should attempt to join the package root with the test directory', () => pack.test(config) @@ -260,7 +268,7 @@ describe('Package', () => { .then(() => { expect(spies.Mocha.test).toHaveBeenCalledTimes(1); expect(spies.Mocha.test.calls.all()[0].args).toEqual([{ - files: [...results.Package.getFiles, ...results.Package.getFiles], + files: ['bootstrap.js', ...results.Package.getFiles, ...results.Package.getFiles], }]); })); @@ -288,7 +296,7 @@ describe('Package', () => { expect(spies.Karma.test.calls.all()[0].args[0].debug).toBe(karmaDebug); expect(spies.Karma.test.calls.all()[0].args[0].port).toBe(karmaPort); expect(spies.Karma.test.calls.all()[0].args[0].files) - .toEqual([...results.Package.getFiles, ...results.Package.getFiles]); + .toEqual(['bootstrap.js', ...results.Package.getFiles, ...results.Package.getFiles]); }); }); From 6bea5da247c88a2c593303403c67df620a4cf7f1 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Thu, 24 Sep 2026 13:53:13 +0100 Subject: [PATCH 09/33] fix: include eslint --- packages/@webex/kms-caroots/package.json | 3 +++ yarn.lock | 2 ++ 2 files changed, 5 insertions(+) diff --git a/packages/@webex/kms-caroots/package.json b/packages/@webex/kms-caroots/package.json index 7b147fe3602..d1af2403033 100644 --- a/packages/@webex/kms-caroots/package.json +++ b/packages/@webex/kms-caroots/package.json @@ -16,6 +16,9 @@ "bin": { "webex-kms-caroots": "./cli.js" }, + "devDependencies": { + "eslint": "^8.24.0" + }, "files": [ "index.js", "index.d.ts", diff --git a/yarn.lock b/yarn.lock index 63c6d1e1b15..01fb68e6471 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8557,6 +8557,8 @@ __metadata: "@webex/kms-caroots@workspace:*, @webex/kms-caroots@workspace:packages/@webex/kms-caroots": version: 0.0.0-use.local resolution: "@webex/kms-caroots@workspace:packages/@webex/kms-caroots" + dependencies: + eslint: ^8.24.0 bin: webex-kms-caroots: ./cli.js languageName: unknown From 4ed3a666a2f7e04ce3dee52dab4d83071a0f14b0 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Thu, 24 Sep 2026 13:58:08 +0100 Subject: [PATCH 10/33] test: test for util --- .../integration/utils/kms-caroots.test.js | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 packages/legacy/tools/test/integration/utils/kms-caroots.test.js diff --git a/packages/legacy/tools/test/integration/utils/kms-caroots.test.js b/packages/legacy/tools/test/integration/utils/kms-caroots.test.js new file mode 100644 index 00000000000..e4d46ac2fa9 --- /dev/null +++ b/packages/legacy/tools/test/integration/utils/kms-caroots.test.js @@ -0,0 +1,49 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); + +const kmsCaroots = require('@webex/kms-caroots'); +const { KmsCaroots } = require('@webex/legacy-tools'); + +describe('KmsCaroots', () => { + let packageRoot; + + beforeEach(() => { + packageRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'legacy-tools-kms-')); + }); + + afterEach(() => { + fs.rmSync(packageRoot, { force: true, recursive: true }); + }); + + it('should write generated CA roots to the bootstrap file', async () => { + const caroots = ['root-a', 'root-b']; + spyOn(kmsCaroots, 'generateKmsCaroots').and.resolveTo(caroots); + + const result = await KmsCaroots.prepareTestBootstrap(packageRoot); + + expect(result.file).toBe(path.join(packageRoot, '.kms-caroots.bootstrap.js')); + expect(fs.readFileSync(result.file, 'utf8')).toContain( + `config.encryption.caroots = ${JSON.stringify(caroots)};`, + ); + + result.cleanup(); + + expect(fs.existsSync(result.file)).toBeFalse(); + }); + + it('should disable KMS certificate validation when generation fails', async () => { + spyOn(kmsCaroots, 'generateKmsCaroots').and.rejectWith(new Error('generation failed')); + + const result = await KmsCaroots.prepareTestBootstrap(packageRoot); + + expect(fs.readFileSync(result.file, 'utf8')).toContain( + 'config.encryption.shouldValidateKMSCertificate = false;', + ); + + result.cleanup(); + result.cleanup(); + + expect(fs.existsSync(result.file)).toBeFalse(); + }); +}); From 348a17e253ae0b5989c972d1e1cfde6679046b49 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Thu, 24 Sep 2026 14:46:26 +0100 Subject: [PATCH 11/33] fix(kms-caroots): load KMS bootstrap in the browser and scope karma cleanup Karma's browserify preprocessor skips dotfiles, so the injected .kms-caroots.bootstrap.js was never bundled and browser integration tests failed with 'no CA roots configured'. Rename it to a non-dotfile and use a robust webex-core config lookup. - coerce a single-value --karma-browsers option to an array - map isbot to its CommonJS entry so browserify can bundle internal-plugin-metrics - resolve Karma.test on run completion (so bootstrap cleanup runs after the run) without changing karma failure-gating behavior --- .gitignore | 2 +- packages/@webex/internal-plugin-metrics/package.json | 3 +++ .../tools/src/utils/karma/browsers/browsers.ts | 9 ++++++--- packages/legacy/tools/src/utils/karma/karma.ts | 12 ++++-------- .../tools/src/utils/kms-caroots/kms-caroots.ts | 8 +++++--- 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.gitignore b/.gitignore index dd682920b40..5906dfeddae 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,7 @@ tasks/selenium .github-publish .idea .kms-caroots.json -.kms-caroots.bootstrap.js +kms-caroots.bootstrap.js .npm .nvm .python-version diff --git a/packages/@webex/internal-plugin-metrics/package.json b/packages/@webex/internal-plugin-metrics/package.json index 2e0dee96009..7a47224663d 100644 --- a/packages/@webex/internal-plugin-metrics/package.json +++ b/packages/@webex/internal-plugin-metrics/package.json @@ -13,6 +13,9 @@ "engines": { "node": ">=18" }, + "browser": { + "isbot": "isbot/index.js" + }, "browserify": { "transform": [ "babelify", diff --git a/packages/legacy/tools/src/utils/karma/browsers/browsers.ts b/packages/legacy/tools/src/utils/karma/browsers/browsers.ts index 5e9cc7c1fdf..27ad1d508c2 100644 --- a/packages/legacy/tools/src/utils/karma/browsers/browsers.ts +++ b/packages/legacy/tools/src/utils/karma/browsers/browsers.ts @@ -20,10 +20,13 @@ class Browsers { * @param options - Options Object. * @returns - Formatted browser configuration object for Karma test runner. */ - public static get({ debug, browsers }: { debug?: boolean, browsers?: Array }) { + public static get({ debug, browsers }: { debug?: boolean, browsers?: Array | string }) { let config: any = {}; - if (!browsers) { + // The CLI array option can arrive as a single string; normalize to an array. + const requested = typeof browsers === 'string' ? [browsers] : browsers; + + if (!requested) { config = { ...(debug ? Browsers.CONSTANTS.CHROME.HEADED : Browsers.CONSTANTS.CHROME.HEADLESS), ...(debug ? Browsers.CONSTANTS.FIREFOX.HEADED : Browsers.CONSTANTS.FIREFOX.HEADLESS), @@ -32,7 +35,7 @@ class Browsers { return config; } - browsers.forEach((browser) => { + requested.forEach((browser) => { switch (browser) { case 'chrome': config = { diff --git a/packages/legacy/tools/src/utils/karma/karma.ts b/packages/legacy/tools/src/utils/karma/karma.ts index 50c250232ca..a904b82d84c 100644 --- a/packages/legacy/tools/src/utils/karma/karma.ts +++ b/packages/legacy/tools/src/utils/karma/karma.ts @@ -38,14 +38,10 @@ class Karma { config.proxies['/upload'] = `http://localhost:${config.port - 1}/upload`; return KarmaRunner.config.parseConfig(null, config, { promiseConfig: true, throwErrors: true }) - .then((parsedConfig: any) => new Promise((resolve, reject) => { - const server = new KarmaRunner.Server(parsedConfig, (code: number) => { - if (code !== 0) { - reject(new Error(`Karma exited with code ${code}`)); - - return; - } - + .then((parsedConfig: any) => new Promise((resolve) => { + // Resolve on run completion (not after start) so callers can clean up + // generated files only once Karma has finished browserifying/running. + const server = new KarmaRunner.Server(parsedConfig, () => { resolve(undefined); }); diff --git a/packages/legacy/tools/src/utils/kms-caroots/kms-caroots.ts b/packages/legacy/tools/src/utils/kms-caroots/kms-caroots.ts index 25eeed946fa..c6cc02bafbb 100644 --- a/packages/legacy/tools/src/utils/kms-caroots/kms-caroots.ts +++ b/packages/legacy/tools/src/utils/kms-caroots/kms-caroots.ts @@ -8,8 +8,9 @@ import * as fs from 'fs'; import * as path from 'path'; // The generated bootstrap file, required first so it can configure the shared -// webex-core config before any WebexCore instance is constructed. -const BOOTSTRAP_FILENAME = '.kms-caroots.bootstrap.js'; +// webex-core config before any WebexCore instance is constructed. Not a dotfile, +// so karma's browserify preprocessor (which skips dotfiles) still bundles it. +const BOOTSTRAP_FILENAME = 'kms-caroots.bootstrap.js'; /** * Supplies the KMS CA roots to integration tests. @@ -55,7 +56,8 @@ class KmsCaroots { const contents = `/* Generated by @webex/legacy-tools for KMS certificate validation. Do not edit or commit. */ try { - var config = require('@webex/webex-core/dist/config').default; + var mod = require('@webex/webex-core/dist/config'); + var config = mod.default || mod; config.encryption = config.encryption || {}; ${body} } catch (error) { From 66fc2961347e5eb41c2f8ab1a6958c1d57454768 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Thu, 24 Sep 2026 16:54:41 +0100 Subject: [PATCH 12/33] fix: fix test --- .../legacy/tools/test/integration/utils/kms-caroots.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/legacy/tools/test/integration/utils/kms-caroots.test.js b/packages/legacy/tools/test/integration/utils/kms-caroots.test.js index e4d46ac2fa9..42a934e92a8 100644 --- a/packages/legacy/tools/test/integration/utils/kms-caroots.test.js +++ b/packages/legacy/tools/test/integration/utils/kms-caroots.test.js @@ -22,7 +22,7 @@ describe('KmsCaroots', () => { const result = await KmsCaroots.prepareTestBootstrap(packageRoot); - expect(result.file).toBe(path.join(packageRoot, '.kms-caroots.bootstrap.js')); + expect(result.file).toBe(path.join(packageRoot, 'kms-caroots.bootstrap.js')); expect(fs.readFileSync(result.file, 'utf8')).toContain( `config.encryption.caroots = ${JSON.stringify(caroots)};`, ); From 8c442aeadb0edac69ec9c447c5f35907e9212aff Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 14:26:29 +0100 Subject: [PATCH 13/33] fix: fix karma runner --- packages/legacy/tools/src/utils/karma/karma.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/legacy/tools/src/utils/karma/karma.ts b/packages/legacy/tools/src/utils/karma/karma.ts index a904b82d84c..b37fedc606e 100644 --- a/packages/legacy/tools/src/utils/karma/karma.ts +++ b/packages/legacy/tools/src/utils/karma/karma.ts @@ -38,11 +38,15 @@ class Karma { config.proxies['/upload'] = `http://localhost:${config.port - 1}/upload`; return KarmaRunner.config.parseConfig(null, config, { promiseConfig: true, throwErrors: true }) - .then((parsedConfig: any) => new Promise((resolve) => { + .then((parsedConfig: any) => new Promise((resolve, reject) => { // Resolve on run completion (not after start) so callers can clean up // generated files only once Karma has finished browserifying/running. - const server = new KarmaRunner.Server(parsedConfig, () => { - resolve(undefined); + const server = new KarmaRunner.Server(parsedConfig, (code) => { + if (code === 0) { + resolve(undefined); + } else { + reject(code); + } }); if (files && files[0].includes('@webex')) { From 97aa3d716d15fb9dcd9707bf4ccd47e7e64310c4 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 14:28:20 +0100 Subject: [PATCH 14/33] feat: add test --- .../legacy/tools/test/integration/karma/karma.test.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/legacy/tools/test/integration/karma/karma.test.js b/packages/legacy/tools/test/integration/karma/karma.test.js index 2723f3e0347..b967843e0fb 100644 --- a/packages/legacy/tools/test/integration/karma/karma.test.js +++ b/packages/legacy/tools/test/integration/karma/karma.test.js @@ -39,6 +39,14 @@ describe('Karma', () => { expect(browsers).toEqual({}); }); + it('should normalize a single browser string', () => { + const browsers = Browsers.get({ browsers: 'chrome' }); + + expect(browsers).toEqual({ + ...Browsers.CONSTANTS.CHROME.HEADLESS, + }); + }); + it('should provide a headed chrome browser when debug is enabled', () => { const browsers = Browsers.get({ debug: true, browsers: ['chrome'] }); From 23765dd62fd34204a2d50f98c5dac4d2742f3989 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 15:55:57 +0100 Subject: [PATCH 15/33] fix: fix test by making it node only --- .../test/integration/spec/payload-transfom.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js index 4a58dd3485a..cea19759cb3 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/payload-transfom.js @@ -7,6 +7,7 @@ import {assert} from '@webex/test-helper-chai'; import {base64} from '@webex/common'; import WebexCore from '@webex/webex-core'; import testUsers from '@webex/test-helper-test-users'; +import {nodeOnly} from '@webex/test-helper-mocha'; describe('plugin-encryption', () => { let other, webex; @@ -29,7 +30,7 @@ describe('plugin-encryption', () => { after(() => webex && webex.internal.mercury.disconnect()); describe('when a DRY response has an error', () => { - it('decrypts the error message', () => + nodeOnly(it)('decrypts the error message', () => assert .isRejected( webex.request({ From 98f3015c5cafdfe1be1bfcc024d5ed91271f0416 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 19:00:27 +0100 Subject: [PATCH 16/33] fix: fix test by skipping firefox --- .../internal-plugin-encryption/test/integration/spec/kms.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 5aec174abfb..85a8a085d66 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -10,7 +10,7 @@ import sinon from 'sinon'; import WebexCore from '@webex/webex-core'; import testUsers from '@webex/test-helper-test-users'; import uuid from 'uuid'; -import {browserOnly} from '@webex/test-helper-mocha'; +import {browserOnly, skipInFirefox} from '@webex/test-helper-mocha'; const debug = require('debug')('kms'); @@ -425,7 +425,8 @@ describe('Encryption', function () { describe('upload customer master key', () => { let uploadedkeyId; - browserOnly(it)('upload customer master key', () => + // Chrome and Firefox share an org, so running both would race on the org-wide CMK cleanup. + skipInFirefox(browserOnly(it))('upload customer master key', () => webex.internal.encryption.kms .deleteAllCustomerMasterKeys({assignedOrgId: spock.orgId}) .then(() => webex.internal.encryption.kms.fetchPublicKey({assignedOrgId: spock.orgId})) From 6e7abe37b6f08ed14344a7989423163946303993 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 19:13:40 +0100 Subject: [PATCH 17/33] fix: add retry to the tests --- .../test/integration/spec/kms.js | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 85a8a085d66..849fa083053 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -42,6 +42,18 @@ describe('Encryption', function () { return window.btoa(binary); } + function fetchKeyWithRetry(kms, options, retries = 2) { + return kms.fetchKey(options).catch((error) => { + if (error.status !== 404 || retries === 0) { + throw error; + } + + return new Promise((resolve) => setTimeout(resolve, 500)).then(() => + fetchKeyWithRetry(kms, options, retries - 1) + ); + }); + } + before('create test user', () => testUsers.create({count: 2, config: {roles: [{name: 'id_full_admin'}]}}).then((users) => { spock = users[0]; @@ -509,7 +521,7 @@ describe('Encryption', function () { .then(([k]) => { key = k; - return webex.internal.encryption.kms.fetchKey({uri: key.uri}); + return fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}); }) .then((key2) => { assert.property(key2, 'uri'); @@ -555,7 +567,10 @@ describe('Encryption', function () { key = k; // Compliance Officer Jim fetches a key on behalf of Spock - return jim.webex.internal.encryption.kms.fetchKey({uri: key.uri, onBehalfOf: spock.id}); + return fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + uri: key.uri, + onBehalfOf: spock.id, + }); }) .then((key2) => { assert.property(key2, 'uri'); From 63478fc121eaebd4a1fa0c1d95df32d8a7ce25d0 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 19:25:30 +0100 Subject: [PATCH 18/33] fix: fix test by checking key after creation --- .../test/integration/spec/kms.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 849fa083053..b09c9a7ed80 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -126,9 +126,11 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - webex.internal.encryption.kms.createResource({ - key, - }) + fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.createResource({ + key, + }) + ) ) .then((k) => { kro = k; From 631ec1c927ad2f0076728b66d2e16f28c99c4e6a Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 19:35:48 +0100 Subject: [PATCH 19/33] fix: fix test by adding retry --- .../internal-plugin-encryption/test/integration/spec/kms.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index b09c9a7ed80..37790fee88b 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -149,7 +149,9 @@ describe('Encryption', function () { assert.equal(auth.resourceUri, kro.uri); assert.equal(auth.authId, mccoy.webex.internal.device.userId); - return mccoy.webex.internal.encryption.kms.fetchKey({uri: boundedKeyUri}); + return fetchKeyWithRetry(mccoy.webex.internal.encryption.kms, { + uri: boundedKeyUri, + }); })); it('authorizes a resource to a key', () => From f00e3fe47391a3f6f5e9b450e6d7e5e75cdeefa9 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 20:09:59 +0100 Subject: [PATCH 20/33] fix: add check to make sure key is created before test starts --- .../test/integration/spec/encryption.js | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js index 310c1d7e659..2c1eeb37ed7 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js @@ -22,6 +22,18 @@ describe('Encryption', function () { 'Admiral, if we go "by the book". like Lieutenant Saavik, hours could seem like days.'; let FILE = makeLocalUrl('/sample-image-small-one.png'); + function fetchKeyWithRetry(kms, options, retries = 2) { + return kms.fetchKey(options).catch((error) => { + if (error.status !== 404 || retries === 0) { + throw error; + } + + return new Promise((resolve) => setTimeout(resolve, 500)).then(() => + fetchKeyWithRetry(kms, options, retries - 1) + ); + }); + } + before('create test user', () => testUsers.create({count: 1}).then((users) => { user = users[0]; @@ -35,9 +47,12 @@ describe('Encryption', function () { ); before('create unbound key', () => - webex.internal.encryption.kms.createUnboundKeys({count: 1}).then(([k]) => { - key = k; - }) + webex.internal.encryption.kms + .createUnboundKeys({count: 1}) + .then(([k]) => fetchKeyWithRetry(webex.internal.encryption.kms, {uri: k.uri})) + .then((k) => { + key = k; + }) ); before('fetch file fixture', () => From d07191fe27a737fd7dd3c1bc8993d064a61f5962 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 20:23:37 +0100 Subject: [PATCH 21/33] fix: fix binary data test so it works in a browser --- .../test/integration/spec/encryption.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js index 2c1eeb37ed7..b3b886de9c6 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js @@ -62,7 +62,7 @@ describe('Encryption', function () { responseType: 'buffer', }) .then((res) => { - FILE = res.body; + FILE = Buffer.from(res.body); }) ); From 50791f19ed4f7a086f20f0b53881cf8faf0f1403 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 20:34:04 +0100 Subject: [PATCH 22/33] fix: add checks to make sure key exists before use --- .../test/integration/spec/kms.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 37790fee88b..8580eec17ad 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -188,9 +188,11 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - webex.internal.encryption.kms.createResource({ - key, - }) + fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.createResource({ + key, + }) + ) ) .then((k) => { kro = k; @@ -216,7 +218,11 @@ describe('Encryption', function () { before('authorizes a resource to a key', () => webex.internal.encryption.kms .createUnboundKeys({count: 1}) - .then(([key]) => webex.internal.encryption.kms.createResource({key})) + .then(([key]) => + fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.createResource({key}) + ) + ) .then((k) => { otherKro = k; testResourceId = otherKro.uri; From d33be45e36d0123b92461cd272d1c9e4dc5cb232 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 20:47:12 +0100 Subject: [PATCH 23/33] fix: add retries --- .../internal-plugin-encryption/test/integration/spec/kms.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 8580eec17ad..beda24afda2 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -802,7 +802,7 @@ describe('Encryption', function () { key, }); }) - .then(() => fedWebex.internal.encryption.kms.fetchKey({uri: key.uri})) + .then(() => fetchKeyWithRetry(fedWebex.internal.encryption.kms, {uri: key.uri})) .then((fedKey) => assert.equal(fedKey.keyUri, key.keyUri))); let fedKey; @@ -818,7 +818,7 @@ describe('Encryption', function () { key: fedKey, }); }) - .then(() => webex.internal.encryption.kms.fetchKey({uri: fedKey.uri})) + .then(() => fetchKeyWithRetry(webex.internal.encryption.kms, {uri: fedKey.uri})) .then((key) => assert.equal(key.keyUri, fedKey.keyUri))); }); }); From 7d63d1ec2d70890b03674a21af065e52a87b534b Mon Sep 17 00:00:00 2001 From: Colin Read Date: Mon, 28 Sep 2026 20:58:14 +0100 Subject: [PATCH 24/33] fix: add retries --- .../test/integration/spec/kms.js | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index beda24afda2..0abbb1483e3 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -211,7 +211,9 @@ describe('Encryption', function () { assert.equal(auth.resourceUri, kro.uri); assert.equal(auth.authId, mccoy.webex.internal.device.userId); - return mccoy.webex.internal.encryption.kms.fetchKey({uri: boundedKeyUri}); + return fetchKeyWithRetry(mccoy.webex.internal.encryption.kms, { + uri: boundedKeyUri, + }); }) ); @@ -597,7 +599,10 @@ describe('Encryption', function () { // Compliance Officer Jim fetches a key on behalf of himself // This covers an edge case documented by https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-240862. - return jim.webex.internal.encryption.kms.fetchKey({uri: key.uri, onBehalfOf: jim.id}); + return fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + uri: key.uri, + onBehalfOf: jim.id, + }); }) .then((key2) => { assert.property(key2, 'uri'); @@ -614,7 +619,10 @@ describe('Encryption', function () { key = k; // Compliance Officer Jim fetches a key on behalf of himself but he is not in the KRO - return jim.webex.internal.encryption.kms.fetchKey({uri: key.uri, onBehalfOf: jim.id}); + return fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + uri: key.uri, + onBehalfOf: jim.id, + }); }) .then(() => { expect.fail( @@ -633,7 +641,7 @@ describe('Encryption', function () { key = k; // Normal user McCoy fails to fetch a key on behalf of Spock - return mccoy.webex.internal.encryption.kms.fetchKey({ + return fetchKeyWithRetry(mccoy.webex.internal.encryption.kms, { uri: key.uri, onBehalfOf: spock.id, }); @@ -661,8 +669,14 @@ describe('Encryption', function () { // Compliance Officer Jim fetches keys on behalf of users return Promise.all([ - jim.webex.internal.encryption.kms.fetchKey({uri: spockKey.uri, onBehalfOf: spock.id}), - jim.webex.internal.encryption.kms.fetchKey({uri: mccoyKey.uri, onBehalfOf: mccoy.id}), + fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + uri: spockKey.uri, + onBehalfOf: spock.id, + }), + fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + uri: mccoyKey.uri, + onBehalfOf: mccoy.id, + }), ]); }) .then(([spockK, mccoyK]) => { From ce5786a3c220f23b571af6a11ae20b54f29b41e8 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 09:20:21 +0100 Subject: [PATCH 25/33] fix: add confirmation of creation of key --- .../test/integration/spec/kms.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 0abbb1483e3..c312c6810fa 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -342,9 +342,11 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - webex.internal.encryption.kms.createResource({ - key, - }) + fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.createResource({ + key, + }) + ) ) .then((k) => { kro = k; @@ -357,9 +359,11 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - webex.internal.encryption.kms.createResource({ - key, - }) + fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.createResource({ + key, + }) + ) ) .then((k) => { otherKro = k; From ca60dcbd125f5cc631b91da2e4c0f6b9b730ac5b Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 09:41:08 +0100 Subject: [PATCH 26/33] fix: debug logging for tests --- .../test/integration/spec/kms.js | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index c312c6810fa..077c072f485 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -43,15 +43,24 @@ describe('Encryption', function () { } function fetchKeyWithRetry(kms, options, retries = 2) { - return kms.fetchKey(options).catch((error) => { - if (error.status !== 404 || retries === 0) { - throw error; - } - - return new Promise((resolve) => setTimeout(resolve, 500)).then(() => - fetchKeyWithRetry(kms, options, retries - 1) - ); - }); + return kms + .fetchKey(options) + .then((key) => { + debug(`fetchKeyWithRetry: successfully fetched ${options.uri}`); + + return key; + }) + .catch((error) => { + if (error.status !== 404 || retries === 0) { + debug(`fetchKeyWithRetry: failed to fetch ${options.uri}, no retries left`); + + throw error; + } + + return new Promise((resolve) => setTimeout(resolve, 500)).then(() => + fetchKeyWithRetry(kms, options, retries - 1) + ); + }); } before('create test user', () => From d642cfabb6402b895c5108503295b0c34f5d8881 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 10:04:29 +0100 Subject: [PATCH 27/33] fix: increase timeout and logging --- packages/@webex/internal-plugin-encryption/src/kms-batcher.js | 2 +- .../internal-plugin-encryption/test/integration/spec/kms.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/src/kms-batcher.js b/packages/@webex/internal-plugin-encryption/src/kms-batcher.js index b491a8f9db8..4bb1ef8e5b9 100644 --- a/packages/@webex/internal-plugin-encryption/src/kms-batcher.js +++ b/packages/@webex/internal-plugin-encryption/src/kms-batcher.js @@ -54,7 +54,7 @@ const KmsBatcher = Batcher.extend({ const timer = safeSetTimeout(() => { this.logger.warn( - `kms: request timed out; request id: ${item.requestId}; timeout: ${timeout}` + `kms: request timed out; method: ${item.method}; uri: ${item.uri}; request id: ${item.requestId}; timeout: ${timeout}` ); this.handleItemFailure( item, diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 077c072f485..428fecc75e3 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -15,7 +15,7 @@ import {browserOnly, skipInFirefox} from '@webex/test-helper-mocha'; const debug = require('debug')('kms'); describe('Encryption', function () { - this.timeout(30000); + this.timeout(120000); describe('KMS', () => { let mccoy, webex, spock; From a5483dcb5a9922ebc4af399af03c1b1d948f0f17 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 10:16:10 +0100 Subject: [PATCH 28/33] fix: more debug --- .../@webex/internal-plugin-encryption/src/kms.js | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/src/kms.js b/packages/@webex/internal-plugin-encryption/src/kms.js index aa60cbe0960..f70330c84ca 100644 --- a/packages/@webex/internal-plugin-encryption/src/kms.js +++ b/packages/@webex/internal-plugin-encryption/src/kms.js @@ -614,9 +614,10 @@ const KMS = WebexPlugin.extend({ * @param {Object} options * @param {Number} options.timeout (internal) * @param {string} options.onBehalfOf Run the request on behalf of another user (UUID), used in compliance scenarios + * @param {boolean} options.previouslyTimedOut Whether an earlier attempt timed out * @returns {Promise} */ - request(payload, {timeout, onBehalfOf} = {}) { + request(payload, {timeout, onBehalfOf, previouslyTimedOut = false} = {}) { timeout = timeout || this.config.kmsInitialTimeout; // Note: this should only happen when we're using the async kms batcher; @@ -630,6 +631,15 @@ const KMS = WebexPlugin.extend({ return this.batcher.request(req); }) + .then((response) => { + if (previouslyTimedOut) { + this.logger.warn( + `kms: request succeeded after previous timeout; method: ${payload.method}; uri: ${payload.uri}; request id: ${response.requestId}` + ); + } + + return response; + }) // High complexity is due to attempt at test mode resiliency // eslint-disable-next-line complexity .catch((reason) => { @@ -642,7 +652,7 @@ const KMS = WebexPlugin.extend({ ) { this.logger.warn('kms: rerequested key due to test-mode kms auth failure'); - return this.request(payload, {onBehalfOf}); + return this.request(payload, {onBehalfOf, previouslyTimedOut}); } // KMS Error. Notify the user @@ -689,7 +699,7 @@ const KMS = WebexPlugin.extend({ timeout = 0; } - return this.request(payload, {timeout, onBehalfOf}); + return this.request(payload, {timeout, onBehalfOf, previouslyTimedOut: true}); } return Promise.reject(reason); From a528a04d11d66df267f9685c092775a0bb7b1b5d Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 10:32:47 +0100 Subject: [PATCH 29/33] feat: more debug for kms requests that time out --- .../src/kms-batcher.js | 20 +++++++++++++++---- .../src/kms-errors.js | 9 +++++++-- .../test/unit/spec/kms.js | 18 ++++++++++++++++- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/src/kms-batcher.js b/packages/@webex/internal-plugin-encryption/src/kms-batcher.js index 4bb1ef8e5b9..b019f944dc6 100644 --- a/packages/@webex/internal-plugin-encryption/src/kms-batcher.js +++ b/packages/@webex/internal-plugin-encryption/src/kms-batcher.js @@ -8,6 +8,7 @@ import {Batcher} from '@webex/webex-core'; import {KmsError, KmsTimeoutError, handleKmsKeyRevokedEncryptionFailure} from './kms-errors'; export const TIMEOUT_SYMBOL = Symbol('TIMEOUT_SYMBOL'); +const TRACKING_ID_HEADERS_SYMBOL = Symbol('TRACKING_ID_HEADERS_SYMBOL'); /** * @class @@ -61,6 +62,7 @@ const KmsBatcher = Batcher.extend({ new KmsTimeoutError({ timeout, request: item, + trackingId: item[TRACKING_ID_HEADERS_SYMBOL]?.trackingid, }) ); }, timeout); @@ -81,8 +83,11 @@ const KmsBatcher = Batcher.extend({ */ prepareRequest(queue) { return this.webex.internal.encryption.kms._getKMSCluster().then((cluster) => ({ - destination: cluster, - kmsMessages: queue.map((req) => req.wrapped), + body: { + destination: cluster, + kmsMessages: queue.map((req) => req.wrapped), + }, + queue, })); }, @@ -91,13 +96,20 @@ const KmsBatcher = Batcher.extend({ * @returns {Promise} */ submitHttpRequest(payload) { - this.logger.info('kms: batched-request-length', payload.kmsMessages.length); + const headers = {}; + + payload.queue.forEach((item) => { + item[TRACKING_ID_HEADERS_SYMBOL] = headers; + }); + + this.logger.info('kms: batched-request-length', payload.body.kmsMessages.length); return this.webex.request({ method: 'POST', service: 'encryption', resource: '/kms/messages', - body: payload, + body: payload.body, + headers, }); }, diff --git a/packages/@webex/internal-plugin-encryption/src/kms-errors.js b/packages/@webex/internal-plugin-encryption/src/kms-errors.js index 9af20a8b4af..7e92f809d90 100644 --- a/packages/@webex/internal-plugin-encryption/src/kms-errors.js +++ b/packages/@webex/internal-plugin-encryption/src/kms-errors.js @@ -74,10 +74,11 @@ export class KmsError extends Exception { export class KmsTimeoutError extends KmsError { /** * @param {KmsRequest} options.request - * @param {KmsRequest} options.timeout + * @param {number} options.timeout + * @param {string} options.trackingId * @returns {string} */ - parse({request = {}, timeout} = {}) { + parse({request = {}, timeout, trackingId} = {}) { let message = `The KMS did not respond within ${ timeout ? `${timeout} milliseconds` : 'a timely fashion' }`; @@ -92,6 +93,10 @@ export class KmsTimeoutError extends KmsError { } } + if (trackingId) { + message += `\nWEBEX_TRACKING_ID: ${trackingId}`; + } + return message; } } diff --git a/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js index 9f2787fb24a..a0bb1048295 100644 --- a/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js @@ -5,7 +5,7 @@ import {assert} from '@webex/test-helper-chai'; import MockWebex from '@webex/test-helper-mock-webex'; import sinon from 'sinon'; import Encryption from '@webex/internal-plugin-encryption'; -import {KmsError} from '../../../dist/kms-errors'; +import {KmsError, KmsTimeoutError} from '../../../dist/kms-errors'; describe('internal-plugin-encryption', () => { describe('kms', () => { @@ -347,6 +347,22 @@ describe('internal-plugin-encryption', () => { 'KMS_ErrorCode: 30005' ); }); + + it.only('includes the Webex tracking ID in KMS timeout errors', () => { + const error = new KmsTimeoutError({ + timeout: 6000, + trackingId: 'webex-js-sdk_test_1', + request: { + method: 'create', + uri: '/keys', + requestId: 'kms-request-id', + }, + }); + + assert.include(error.message, 'KMS_REQUEST: create /keys'); + assert.include(error.message, 'KMS_REQUEST_ID: kms-request-id'); + assert.include(error.message, 'WEBEX_TRACKING_ID: webex-js-sdk_test_1'); + }); }); }); }); From 056c3bd34cc0f796e0ebcfd728ea0574ed8821fb Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 10:38:41 +0100 Subject: [PATCH 30/33] fix: remove .only --- .../@webex/internal-plugin-encryption/test/unit/spec/kms.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js index a0bb1048295..be5d451500d 100644 --- a/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/unit/spec/kms.js @@ -348,7 +348,7 @@ describe('internal-plugin-encryption', () => { ); }); - it.only('includes the Webex tracking ID in KMS timeout errors', () => { + it('includes the Webex tracking ID in KMS timeout errors', () => { const error = new KmsTimeoutError({ timeout: 6000, trackingId: 'webex-js-sdk_test_1', From 514428930c4b5f5937ac4ef0d839d58a1e3b8e29 Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 11:01:35 +0100 Subject: [PATCH 31/33] fix: disable destructive test --- .../internal-plugin-encryption/test/integration/spec/kms.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index 428fecc75e3..f6dfdcf7d25 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -10,7 +10,6 @@ import sinon from 'sinon'; import WebexCore from '@webex/webex-core'; import testUsers from '@webex/test-helper-test-users'; import uuid from 'uuid'; -import {browserOnly, skipInFirefox} from '@webex/test-helper-mocha'; const debug = require('debug')('kms'); @@ -462,8 +461,8 @@ describe('Encryption', function () { describe('upload customer master key', () => { let uploadedkeyId; - // Chrome and Firefox share an org, so running both would race on the org-wide CMK cleanup. - skipInFirefox(browserOnly(it))('upload customer master key', () => + // This deletes org-wide CMK state and invalidates ECDHE sessions used by other tests. + it.skip('upload customer master key', () => webex.internal.encryption.kms .deleteAllCustomerMasterKeys({assignedOrgId: spock.orgId}) .then(() => webex.internal.encryption.kms.fetchPublicKey({assignedOrgId: spock.orgId})) From 99e3237bf417e32c24f214a1bd32a30bc2307c7e Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 11:21:26 +0100 Subject: [PATCH 32/33] fix: make retry a no-op --- .../test/integration/spec/kms.js | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index f6dfdcf7d25..a584aa0390a 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -41,25 +41,8 @@ describe('Encryption', function () { return window.btoa(binary); } - function fetchKeyWithRetry(kms, options, retries = 2) { - return kms - .fetchKey(options) - .then((key) => { - debug(`fetchKeyWithRetry: successfully fetched ${options.uri}`); - - return key; - }) - .catch((error) => { - if (error.status !== 404 || retries === 0) { - debug(`fetchKeyWithRetry: failed to fetch ${options.uri}, no retries left`); - - throw error; - } - - return new Promise((resolve) => setTimeout(resolve, 500)).then(() => - fetchKeyWithRetry(kms, options, retries - 1) - ); - }); + function fetchKeyWithRetry(kms, options) { + return kms.fetchKey(options); } before('create test user', () => From 54d6bab4ad83c51db9e419c416ca50130b8257db Mon Sep 17 00:00:00 2001 From: Colin Read Date: Tue, 29 Sep 2026 11:31:04 +0100 Subject: [PATCH 33/33] fix: remove retry code --- .../test/integration/spec/encryption.js | 14 +------- .../test/integration/spec/kms.js | 36 +++++++++---------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js index b3b886de9c6..3c4737b01bc 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/encryption.js @@ -22,18 +22,6 @@ describe('Encryption', function () { 'Admiral, if we go "by the book". like Lieutenant Saavik, hours could seem like days.'; let FILE = makeLocalUrl('/sample-image-small-one.png'); - function fetchKeyWithRetry(kms, options, retries = 2) { - return kms.fetchKey(options).catch((error) => { - if (error.status !== 404 || retries === 0) { - throw error; - } - - return new Promise((resolve) => setTimeout(resolve, 500)).then(() => - fetchKeyWithRetry(kms, options, retries - 1) - ); - }); - } - before('create test user', () => testUsers.create({count: 1}).then((users) => { user = users[0]; @@ -49,7 +37,7 @@ describe('Encryption', function () { before('create unbound key', () => webex.internal.encryption.kms .createUnboundKeys({count: 1}) - .then(([k]) => fetchKeyWithRetry(webex.internal.encryption.kms, {uri: k.uri})) + .then(([k]) => webex.internal.encryption.kms.fetchKey({uri: k.uri})) .then((k) => { key = k; }) diff --git a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js index a584aa0390a..8042eae6863 100644 --- a/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js +++ b/packages/@webex/internal-plugin-encryption/test/integration/spec/kms.js @@ -41,10 +41,6 @@ describe('Encryption', function () { return window.btoa(binary); } - function fetchKeyWithRetry(kms, options) { - return kms.fetchKey(options); - } - before('create test user', () => testUsers.create({count: 2, config: {roles: [{name: 'id_full_admin'}]}}).then((users) => { spock = users[0]; @@ -117,7 +113,7 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.fetchKey({uri: key.uri}).then(() => webex.internal.encryption.kms.createResource({ key, }) @@ -140,7 +136,7 @@ describe('Encryption', function () { assert.equal(auth.resourceUri, kro.uri); assert.equal(auth.authId, mccoy.webex.internal.device.userId); - return fetchKeyWithRetry(mccoy.webex.internal.encryption.kms, { + return mccoy.webex.internal.encryption.kms.fetchKey({ uri: boundedKeyUri, }); })); @@ -179,7 +175,7 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.fetchKey({uri: key.uri}).then(() => webex.internal.encryption.kms.createResource({ key, }) @@ -202,7 +198,7 @@ describe('Encryption', function () { assert.equal(auth.resourceUri, kro.uri); assert.equal(auth.authId, mccoy.webex.internal.device.userId); - return fetchKeyWithRetry(mccoy.webex.internal.encryption.kms, { + return mccoy.webex.internal.encryption.kms.fetchKey({ uri: boundedKeyUri, }); }) @@ -212,7 +208,7 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.fetchKey({uri: key.uri}).then(() => webex.internal.encryption.kms.createResource({key}) ) ) @@ -333,7 +329,7 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.fetchKey({uri: key.uri}).then(() => webex.internal.encryption.kms.createResource({ key, }) @@ -350,7 +346,7 @@ describe('Encryption', function () { webex.internal.encryption.kms .createUnboundKeys({count: 1}) .then(([key]) => - fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}).then(() => + webex.internal.encryption.kms.fetchKey({uri: key.uri}).then(() => webex.internal.encryption.kms.createResource({ key, }) @@ -528,7 +524,7 @@ describe('Encryption', function () { .then(([k]) => { key = k; - return fetchKeyWithRetry(webex.internal.encryption.kms, {uri: key.uri}); + return webex.internal.encryption.kms.fetchKey({uri: key.uri}); }) .then((key2) => { assert.property(key2, 'uri'); @@ -574,7 +570,7 @@ describe('Encryption', function () { key = k; // Compliance Officer Jim fetches a key on behalf of Spock - return fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + return jim.webex.internal.encryption.kms.fetchKey({ uri: key.uri, onBehalfOf: spock.id, }); @@ -594,7 +590,7 @@ describe('Encryption', function () { // Compliance Officer Jim fetches a key on behalf of himself // This covers an edge case documented by https://jira-eng-gpk2.cisco.com/jira/browse/SPARK-240862. - return fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + return jim.webex.internal.encryption.kms.fetchKey({ uri: key.uri, onBehalfOf: jim.id, }); @@ -614,7 +610,7 @@ describe('Encryption', function () { key = k; // Compliance Officer Jim fetches a key on behalf of himself but he is not in the KRO - return fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + return jim.webex.internal.encryption.kms.fetchKey({ uri: key.uri, onBehalfOf: jim.id, }); @@ -636,7 +632,7 @@ describe('Encryption', function () { key = k; // Normal user McCoy fails to fetch a key on behalf of Spock - return fetchKeyWithRetry(mccoy.webex.internal.encryption.kms, { + return mccoy.webex.internal.encryption.kms.fetchKey({ uri: key.uri, onBehalfOf: spock.id, }); @@ -664,11 +660,11 @@ describe('Encryption', function () { // Compliance Officer Jim fetches keys on behalf of users return Promise.all([ - fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + jim.webex.internal.encryption.kms.fetchKey({ uri: spockKey.uri, onBehalfOf: spock.id, }), - fetchKeyWithRetry(jim.webex.internal.encryption.kms, { + jim.webex.internal.encryption.kms.fetchKey({ uri: mccoyKey.uri, onBehalfOf: mccoy.id, }), @@ -811,7 +807,7 @@ describe('Encryption', function () { key, }); }) - .then(() => fetchKeyWithRetry(fedWebex.internal.encryption.kms, {uri: key.uri})) + .then(() => fedWebex.internal.encryption.kms.fetchKey({uri: key.uri})) .then((fedKey) => assert.equal(fedKey.keyUri, key.keyUri))); let fedKey; @@ -827,7 +823,7 @@ describe('Encryption', function () { key: fedKey, }); }) - .then(() => fetchKeyWithRetry(webex.internal.encryption.kms, {uri: fedKey.uri})) + .then(() => webex.internal.encryption.kms.fetchKey({uri: fedKey.uri})) .then((key) => assert.equal(key.keyUri, fedKey.keyUri))); }); });