diff --git a/docs/samples/calling/app.js b/docs/samples/calling/app.js index 68a7a0e97e6..0caa1f4ca99 100644 --- a/docs/samples/calling/app.js +++ b/docs/samples/calling/app.js @@ -283,7 +283,6 @@ async function initCalling(e) { kmsInitialTimeout: 8000, kmsMaxTimeout: 40000, batcherMaxCalls: 30, - caroots: null, }, dss: {}, }, diff --git a/packages/@webex/internal-plugin-encryption/src/config.js b/packages/@webex/internal-plugin-encryption/src/config.js index 2f10c8ce70d..a34a8d767cf 100644 --- a/packages/@webex/internal-plugin-encryption/src/config.js +++ b/packages/@webex/internal-plugin-encryption/src/config.js @@ -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; + } +} + 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} + */ + caroots: DEFAULT_KMS_CAROOTS, + joseOptions: { compact: true, contentAlg: 'A256GCM', diff --git a/packages/@webex/internal-plugin-encryption/src/index.js b/packages/@webex/internal-plugin-encryption/src/index.js index 72ba3dccb0d..f6bb58f4ec0 100644 --- a/packages/@webex/internal-plugin-encryption/src/index.js +++ b/packages/@webex/internal-plugin-encryption/src/index.js @@ -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'; @@ -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: [ 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..0ab4d4b3265 100644 --- a/packages/@webex/internal-plugin-encryption/src/kms-certificate-validation.js +++ b/packages/@webex/internal-plugin-encryption/src/kms-certificate-validation.js @@ -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; diff --git a/packages/@webex/internal-plugin-encryption/src/kms-default-caroots.js b/packages/@webex/internal-plugin-encryption/src/kms-default-caroots.js new file mode 100644 index 00000000000..850d4f59db2 --- /dev/null +++ b/packages/@webex/internal-plugin-encryption/src/kms-default-caroots.js @@ -0,0 +1,23 @@ +/*! + * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file. + */ + +/** + * Default trusted CA roots for Webex KMS static-key x5c chain validation. + * + * Webex KMS certificates chain to GoDaddy-issued roots (see CE-59676 / + * kms-1023-client-pkix-validation). Values are base64-encoded DER, matching + * the format expected by kms-certificate-validation.decodeCert(). + * + * Source: https://certs.godaddy.com/repository/ (public root certificates) + * + * @type {string[]} + */ +export const DEFAULT_KMS_CAROOTS = [ + // Go Daddy Root Certificate Authority - G2 (gdroot-g2.crt) + 'MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPOLPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1', + // Go Daddy Class 2 Certification Authority (gd-class2-root.crt) + 'MIIEADCCAuigAwIBAgIBADANBgkqhkiG9w0BAQUFADBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA0MDYyOTE3MDYyMFoXDTM0MDYyOTE3MDYyMFowYzELMAkGA1UEBhMCVVMxITAfBgNVBAoTGFRoZSBHbyBEYWRkeSBHcm91cCwgSW5jLjExMC8GA1UECxMoR28gRGFkZHkgQ2xhc3MgMiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAN6d1+pXGEmhW+vXX0iG6r7d/+TvZxz0ZWizV3GgXne77ZtJ6XCAPVYYYwhv2vLM0D9/AlQiVBDYsoHUwHU9S3/Hd8M+eKsaA7Ugay9qK7HFiH7Eux6wwdhFJ2+qN1j3hybX2C32qRe3H3I2TqYXP2WYktsqbl2i/ojgC95/5Y0V4evLOtXiEqITLdiOr18SPaAIBQi2XKVlOARFmR6jYGB0xUGlcmIbYsUfb18aQr4CUWWoriMYavx4A6lNf4DD+qta/KFApMoZFv6yyO9ecw3ud72a9nmYvLEHZ6IVDd2gWMZEewo+YihfukEHU1jPEX44dMX4/7VpkI+EdOqXG68CAQOjgcAwgb0wHQYDVR0OBBYEFNLEsNKR1EwRcbNhyz2h/t2oatTjMIGNBgNVHSMEgYUwgYKAFNLEsNKR1EwRcbNhyz2h/t2oatTjoWekZTBjMQswCQYDVQQGEwJVUzEhMB8GA1UEChMYVGhlIEdvIERhZGR5IEdyb3VwLCBJbmMuMTEwLwYDVQQLEyhHbyBEYWRkeSBDbGFzcyAyIENlcnRpZmljYXRpb24gQXV0aG9yaXR5ggEAMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBADJL87LKPpH8EsahB4yOd6AzBhRckB4Y9wimPQoZ+YeAEW5p5JYXMP80kWNyOO7MHAGjHZQopDH2esRU1/blMVgDoszOYtuURXO1v0XJJLXVggKtI3lpjbi2Tc7PTMozI+gciKqdi0FuFskg5YmezTvacPd+mSYgFFQlq25zheabIZ0KbIIOqPjCDPoQHmyW74cNxA9hi63ugyuV+I6ShHI56yDqg+2DzZduCLzrTia2cyvk0/ZM/iZx4mERdEr/VxqHD3VILs9RaRegAhJhldXRQLIQTO7ErBBDpqWeCtWVYpoNz4iCxTIM5CufReYNnyicsbkqWletNw+vHX/bvZ8=', +]; + +export default DEFAULT_KMS_CAROOTS; diff --git a/packages/@webex/internal-plugin-encryption/test/unit/spec/encryption-config.js b/packages/@webex/internal-plugin-encryption/test/unit/spec/encryption-config.js new file mode 100644 index 00000000000..2f90779f420 --- /dev/null +++ b/packages/@webex/internal-plugin-encryption/test/unit/spec/encryption-config.js @@ -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); + }); + }); +}); 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..841cb9df68b 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 @@ -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 = [ @@ -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); }); }); });