Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
35fbb6e
feat: update docs on KMS certificate verification
Coread Sep 21, 2026
a2d47cf
feat(internal-plugin-encryption): validate KMS cert safe default and …
Coread Sep 21, 2026
8a4f4d3
feat(internal-plugin-encryption): add KMS CA roots tooling and env-ba…
Coread Sep 22, 2026
5f6c3a6
fix(internal-plugin-encryption): deliver KMS CA roots via test fixtur…
Coread Sep 22, 2026
a53c931
fix(internal-plugin-encryption): generate KMS CA roots in test script…
Coread Sep 22, 2026
b760e37
Merge branch 'next' of github.com:webex/webex-js-sdk into validate_km…
Coread Sep 24, 2026
8a2d7ca
feat(kms-caroots): add @webex/kms-caroots package and auto-generate C…
Coread Sep 24, 2026
72a2c7f
fix(legacy-tools): await Karma completion before cleaning up KMS boot…
Coread Sep 24, 2026
0823d59
test(legacy-tools): mock KMS caroots bootstrap in Package.test tests
Coread Sep 24, 2026
6bea5da
fix: include eslint
Coread Sep 24, 2026
4ed3a66
test: test for util
Coread Sep 24, 2026
348a17e
fix(kms-caroots): load KMS bootstrap in the browser and scope karma c…
Coread Sep 24, 2026
66fc296
fix: fix test
Coread Sep 24, 2026
8c442ae
fix: fix karma runner
Coread Sep 28, 2026
97aa3d7
feat: add test
Coread Sep 28, 2026
23765dd
fix: fix test by making it node only
Coread Sep 28, 2026
e7af66e
Merge branch 'next' of github.com:webex/webex-js-sdk into validate_km…
Coread Sep 28, 2026
98f3015
fix: fix test by skipping firefox
Coread Sep 28, 2026
6e7abe3
fix: add retry to the tests
Coread Sep 28, 2026
63478fc
fix: fix test by checking key after creation
Coread Sep 28, 2026
631ec1c
fix: fix test by adding retry
Coread Sep 28, 2026
f00e3fe
fix: add check to make sure key is created before test starts
Coread Sep 28, 2026
d07191f
fix: fix binary data test so it works in a browser
Coread Sep 28, 2026
50791f1
fix: add checks to make sure key exists before use
Coread Sep 28, 2026
d33be45
fix: add retries
Coread Sep 28, 2026
7d63d1e
fix: add retries
Coread Sep 28, 2026
ce5786a
fix: add confirmation of creation of key
Coread Sep 29, 2026
ca60dcb
fix: debug logging for tests
Coread Sep 29, 2026
d642cfa
fix: increase timeout and logging
Coread Sep 29, 2026
a5483dc
fix: more debug
Coread Sep 29, 2026
a528a04
feat: more debug for kms requests that time out
Coread Sep 29, 2026
056c3bd
fix: remove .only
Coread Sep 29, 2026
5144289
fix: disable destructive test
Coread Sep 29, 2026
99e3237
fix: make retry a no-op
Coread Sep 29, 2026
54d6bab
fix: remove retry code
Coread Sep 29, 2026
fac9faf
Merge branch 'next' of github.com:webex/webex-js-sdk into validate_km…
Coread Sep 29, 2026
4f4a715
fix(kms-caroots): harden downloads and document generator usage
Coread Sep 29, 2026
d98806c
fix(kms-caroots): exclude default OpenSSL trust locations
Coread Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ tasks/selenium
.grunt
.github-publish
.idea
.kms-caroots.json
.kms-caroots.bootstrap.js
.npm
.nvm
.python-version
Expand Down
4 changes: 4 additions & 0 deletions docs/samples/browser-plugin-meetings/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
};
Expand Down
5 changes: 4 additions & 1 deletion docs/samples/browser-read-status/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 4 additions & 1 deletion docs/samples/browser-socket/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/samples/calling/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ async function initCalling(e) {
kmsMaxTimeout: 40000,
batcherMaxCalls: 30,
caroots: null,
shouldValidateKMSCertificate: false,
},
dss: {},
},
Expand Down
4 changes: 4 additions & 0 deletions docs/samples/contact-center/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
}
Expand Down
4 changes: 4 additions & 0 deletions docs/samples/plugin-encryption/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "webex-kms-caroots",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare the generator CLI in the root workspace

On a normal root install, yarn caroots:generate runs with only the root workspace's dependency binaries on PATH. @webex/kms-caroots is declared only by packages/legacy/tools, so its webex-kms-caroots bin is not available to this script; the new root command exits command not found unless the CLI was installed globally. Add the package as a root dependency or invoke it through its workspace.

Useful? React with 👍 / 👎.

"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",
Expand Down
81 changes: 81 additions & 0 deletions packages/@webex/internal-plugin-encryption/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,87 @@ const webex = new WebexCore();
webex.internal.encryption.WHATEVER;
```

## KMS certificate validation

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:

- `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, 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:
<https://www.cisco.com/security/pki/trs/readme.html>

### Generating the CA roots

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
# Print the JSON array to stdout
npx webex-kms-caroots

# Or write it to a file
npx webex-kms-caroots --out ./caroots.json
```

```js
const {generateKmsCaroots} = require('@webex/kms-caroots');

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
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';

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/).
Expand Down
22 changes: 20 additions & 2 deletions packages/@webex/internal-plugin-encryption/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,26 @@ 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.
* 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,
Comment thread
Coread marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid unhandled rejections for missing CA roots

When a Node consumer upgrades without encryption.caroots, this new default makes _prepareContext() reject as documented; however, _getContext() also attaches a fulfillment-only promise.then(...) at kms.js:720. Even if the caller handles the KMS request rejection, that derived promise remains unhandled, and Node 22 treats it as fatal, so the first KMS operation can terminate the process instead of returning the expected KMSError. Attach a rejection handler to the bookkeeping chain.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Configure CA roots for the top-level integration runner

When users invoke the root yarn test:integration command, it routes through tooling/lib/test/index.js's Karma runner rather than @webex/legacy-tools; that runner only loads the integration specs and never calls KmsCaroots.prepareTestBootstrap (the only bootstrap setup is in packages/legacy/tools/src/models/package/package.ts). The encryption integration suite calls createUnboundKeys(), so its WebexCore instances retain caroots: undefined and this new default makes KMS setup reject, causing the documented top-level integration suite to fail. Inject the roots into the tooling Karma configuration as well, or explicitly opt that runner out.

Useful? React with 👍 / 👎.


/**
* 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). Required when
* `shouldValidateKMSCertificate` is true.
*
* 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: undefined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
20 changes: 12 additions & 8 deletions packages/@webex/internal-plugin-encryption/src/kms.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<Object>} 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) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear the cached context when validation rejects

When the new default validation rejects (for example, an upgraded client initially has no CA roots, or its bundled roots are stale), _getContext() has already stored that rejected _prepareContext() promise in contexts and only schedules deletion on fulfillment (kms.js:724-734). Every later KMS operation therefore reuses the rejected promise, so updating encryption.caroots or disabling validation with the public setConfig() API cannot recover the existing Webex instance; remove the cached context on rejection before propagating the error.

Useful? React with 👍 / 👎.

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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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)
);
});
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand All @@ -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);

Expand Down
17 changes: 17 additions & 0 deletions packages/@webex/kms-caroots/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -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',
},
};
Loading
Loading