Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
1 change: 0 additions & 1 deletion docs/samples/calling/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,6 @@ async function initCalling(e) {
kmsInitialTimeout: 8000,
kmsMaxTimeout: 40000,
batcherMaxCalls: 30,
caroots: null,
},
dss: {},
},
Expand Down
30 changes: 30 additions & 0 deletions packages/@webex/internal-plugin-encryption/src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,38 @@
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
*/

import {has} from 'lodash';

import DEFAULT_KMS_CAROOTS from './kms-default-caroots';

/**
* lodash merge combines arrays by index, so an explicit encryption.caroots
* override (including []) would otherwise retain default root entries.
*
* @param {Object} webexConfig merged webex config object
* @param {Object} [overrideConfig] config passed to initialize/setConfig
* @returns {void}
*/
export function applyEncryptionConfigOverrides(webexConfig, overrideConfig = {}) {
if (!webexConfig?.encryption) {
return;
}

if (has(overrideConfig, 'encryption.caroots')) {
webexConfig.encryption.caroots = overrideConfig.encryption.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 Clone the replacement CA-root array

When a consumer retains and later mutates the array passed in config.encryption.caroots, this direct assignment mutates the SDK's active trust store as well. This differs from the normal merge({}, …) configuration path, which creates a separate array, and can unexpectedly add or remove trusted roots after initialization without calling setConfig(). Clone the explicit replacement array while preserving replacement rather than index-merge semantics.

Useful? React with 👍 / 👎.

}
}

export default {
encryption: {
/**
* PEM (base64 DER) encoded CA certificates trusted to sign the KMS
* static-key certificate chain. KMS validation fails closed when this
* list is empty; deployments MUST provide the Webex KMS issuing roots.
* @type {Array<string>}
*/
caroots: DEFAULT_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.

P1 Badge Preserve explicit CA-root overrides

When a consumer supplies config.encryption.caroots: [], WebexCore.initialize() combines it with these defaults using lodash merge, which retains both default array entries; a one-element custom root similarly retains the second GoDaddy root. Consequently, the new empty-root guard never fails closed for an explicit empty array, and private-KMS deployments cannot restrict trust to a single custom CA. Ensure this array is replaced rather than index-merged during configuration normalization.

Useful? React with 👍 / 👎.


joseOptions: {
compact: true,
contentAlg: 'A256GCM',
Expand Down
28 changes: 26 additions & 2 deletions packages/@webex/internal-plugin-encryption/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import '@webex/internal-plugin-device';

import '@webex/internal-plugin-mercury';

import {registerInternalPlugin} from '@webex/webex-core';
import WebexCore, {registerInternalPlugin} from '@webex/webex-core';
import {has, isObject, isString} from 'lodash';

import Encryption from './encryption';
import config from './config';
import config, {applyEncryptionConfigOverrides} from './config';
import {DryError} from './kms-errors';

import KmsDryErrorInterceptor from './kms-dry-error-interceptor';
Expand All @@ -29,6 +29,30 @@ if (process.env.NODE_ENV === 'test') {
};
}

let encryptionConfigNormalizationInstalled = false;

function installEncryptionConfigNormalization() {
if (encryptionConfigNormalizationInstalled) {
return;
}

encryptionConfigNormalizationInstalled = true;

const {initialize, setConfig} = WebexCore.prototype;

WebexCore.prototype.initialize = function initializeWithEncryptionConfig(attrs = {}) {
initialize.call(this, attrs);
applyEncryptionConfigOverrides(this.config, attrs.config);
};

WebexCore.prototype.setConfig = function setConfigWithEncryptionConfig(newConfig = {}) {
setConfig.call(this, newConfig);
applyEncryptionConfigOverrides(this.config, newConfig);
};
}

installEncryptionConfigNormalization();

registerInternalPlugin('encryption', Encryption, {
payloadTransformer: {
predicates: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,12 +221,14 @@ 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();
// Fail closed: without trusted CA roots the x5c chain cannot be
// authenticated, and an attacker-supplied self-signed certificate
// would satisfy every other check above.
if (!isArray(caroots) || caroots.length === 0) {
throwError('no trusted CA roots configured; cannot validate KMS certificate chain');
}

return promise.then(() => jwt);
return validateCertificatesSignature(certificates, caroots).then(() => jwt);
});

export default validateKMS;

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {assert} from '@webex/test-helper-chai';
import {cloneDeep, merge} from 'lodash';

import defaultConfig, {applyEncryptionConfigOverrides} from '../../../src/config';

describe('internal-plugin-encryption', () => {
describe('encryption config', () => {
it('replaces caroots when consumer supplies an empty array', () => {
const webexConfig = merge({}, defaultConfig, {encryption: {caroots: []}});

assert.isAbove(webexConfig.encryption.caroots.length, 0);

applyEncryptionConfigOverrides(webexConfig, {encryption: {caroots: []}});

assert.deepEqual(webexConfig.encryption.caroots, []);
});

it('replaces caroots when consumer supplies a single custom root', () => {
const customRoot = 'CUSTOM_CA_ROOT';
const webexConfig = merge({}, defaultConfig, {encryption: {caroots: [customRoot]}});

assert.isAbove(webexConfig.encryption.caroots.length, 1);
assert.include(webexConfig.encryption.caroots, customRoot);

applyEncryptionConfigOverrides(webexConfig, {encryption: {caroots: [customRoot]}});

assert.deepEqual(webexConfig.encryption.caroots, [customRoot]);
});

it('leaves default caroots when consumer does not override caroots', () => {
const webexConfig = merge({}, defaultConfig, {encryption: {kmsInitialTimeout: 1000}});
const expectedCaroots = cloneDeep(webexConfig.encryption.caroots);

applyEncryptionConfigOverrides(webexConfig, {encryption: {kmsInitialTimeout: 1000}});

assert.deepEqual(webexConfig.encryption.caroots, expectedCaroots);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {assert} from '@webex/test-helper-chai';

import config from '../../../src/config';
import validateCert, {KMSError, validateCommonName, X509_SUBJECT_ALT_NAME_KEY} from '../../../src/kms-certificate-validation';

const caroots = [
Expand Down Expand Up @@ -152,14 +153,29 @@ describe('internal-plugin-encryption', () => {
return assert.isRejected(validate(jwt), KMSError);
});

it('accepts self signed certificate if no CA roots.', () => {
it('rejects self-signed certificate when no CA roots are configured', () => {
const jwt = {
...VALID_JWT,
x5c: x5cSelfSigned,
n: x5cSelfSignedModulus,
};

return validateCert()(jwt).then((results) => assert.equal(results, jwt));
return assert.isRejected(validateCert([])(jwt), KMSError);
});

it('rejects self-signed certificate with default config caroots', () => {
const jwt = {
...VALID_JWT,
x5c: x5cSelfSigned,
n: x5cSelfSignedModulus,
};

return assert.isRejected(validateCert(config.encryption.caroots)(jwt), KMSError);
});

it('ships default trusted CA roots in encryption config', () => {
assert.isArray(config.encryption.caroots);
assert.isAbove(config.encryption.caroots.length, 0);
});
});
});
Expand Down