Skip to content

fix(internal-plugin-encryption): fail closed on KMS x5c chain validation (FPV-531) - #5154

Open
akulakum wants to merge 4 commits into
webex:nextfrom
akulakum:fix/kms-certificate-chain-validation-FPV-531
Open

fix(internal-plugin-encryption): fail closed on KMS x5c chain validation (FPV-531)#5154
akulakum wants to merge 4 commits into
webex:nextfrom
akulakum:fix/kms-certificate-chain-validation-FPV-531

Conversation

@akulakum

@akulakum akulakum commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

COMPLETES #FPV-531

This pull request addresses

FPV-531 — Webex JS SDK skips KMS certificate chain validation (ASIG / Flashpoint, P2)

Before talking to KMS, the SDK validates the KMS static-key identity from GET /kms/{userId} (rsaPublicKey JWK with x5c chain). The only step that actually authenticates the certificate — validateCertificatesSignature() — was conditional on config.encryption.caroots. That config key was never defined, so caroots was always undefined and chain validation was silently skipped in every deployment.

An attacker who substitutes a self-signed KMS cert can impersonate KMS, harvest the OAuth bearer token embedded in KMS requests, and control all E2E encryption keys. This was demonstrated end-to-end against the integration environment (see Jira for reproduction).

by making the following changes

  1. Fail closed in kms-certificate-validation.js — treat missing or empty caroots as a validation failure instead of skipping validateCertificatesSignature() (per ASIG fix.patch).

  2. Ship default CA roots in config.encryption.caroots — add kms-default-caroots.js with base64 DER trusted roots used by Webex KMS x5c chains (GoDaddy G2 roots, per CE-59676 / kms-1023-client-pkix-validation and the GoDaddy certificate repository).

  3. Fix unit tests — remove the test that accepted self-signed certs when no CA roots were configured; add tests asserting self-signed rejection with empty/default roots and that default roots are shipped.

  4. Samples — remove caroots: null override in docs/samples/calling/app.js so the SDK defaults apply.

  5. Preserve explicit encryption.caroots overridesWebexCore.initialize() / setConfig() use lodash merge, which combines arrays by index. That meant caroots: [] still kept default GoDaddy roots (fail-closed never triggered) and caroots: [customCA] kept a second default root. After merge, replace encryption.caroots when the consumer explicitly sets it so private-KMS / empty-root deployments behave as intended.

Change Type

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update
  • Tooling change
  • Internal code refactor

The following scenarios were tested

  • yarn workspace @webex/internal-plugin-encryption test:unit --targets kms-certificate-validation.js20/20 passed
  • yarn workspace @webex/internal-plugin-encryption test:unit --targets encryption-config.js3/3 passed
  • Verified self-signed x5c is rejected with empty caroots and with default config roots
  • Verified explicit caroots: [] and single custom root replace defaults (no index-merge with GoDaddy roots)
  • Verified existing positive validation tests (explicit test CA roots) still pass

The GAI Coding Policy And Copyright Annotation Best Practices

  • GAI was not used (or, no additional notation is required)
  • Code was generated entirely by GAI
  • GAI was used to create a draft that was subsequently customized or modified
  • Coder created a draft manually that was non-substantively modified by GAI (e.g., refactoring was performed by GAI on manually written code)
  • Tool used for AI assistance (GitHub Copilot / Other - specify)
    • Github Copilot
    • Other - Please Specify
  • This PR is related to
    • Feature
    • Defect fix
    • Tech Debt
    • Automation

I certified that

  • I have read and followed contributing guidelines
  • I discussed changes with code owners prior to submitting this pull request
  • I have not skipped any automated checks
  • All existing and new tests passed
  • I have updated the documentation accordingly

Note for reviewers / ASIG: Default CA roots are GoDaddy public roots aligned with historical Webex KMS chain analysis (CE-59676). Please confirm with ASIG (Phillip McKnight) that current prod/integration KMS 3-cert chains still anchor to these roots; additional roots can be added to kms-default-caroots.js if needed.

Config override behavior: Consumers can still set config.encryption.caroots to [] (fail closed) or a single custom CA for private KMS; those values replace defaults rather than merging with them.

Make sure to have followed the contributing guidelines before submitting.

@akulakum
akulakum requested a review from a team as a code owner August 7, 2026 07:28
@akulakum akulakum added the validated If the pull request is validated for automation. label Aug 7, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e09577aeee

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* 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 👍 / 👎.

@github-actions github-actions Bot removed the validated If the pull request is validated for automation. label Aug 7, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7cdd9752e2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* @type {string[]}
*/
export const DEFAULT_KMS_CAROOTS = [
// Go Daddy Root Certificate Authority - G2 (gdroot-g2.crt)

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 Pin validation to the expected KMS identity

When an attacker can substitute the GET /kms/{userId} response—the scenario this change is intended to stop—these public GoDaddy roots do not authenticate Webex KMS specifically. validateKidHeader() only requires the attacker-supplied kid to use the kms: scheme, and validateCommonName() compares the certificate against that same attacker-supplied value. An attacker with a valid GoDaddy-issued certificate for a controlled domain can therefore provide kid: kms://attacker.example plus matching x5c and key parameters, pass the new chain validation, and retain control of the KMS identity. Bind kid to an independently trusted KMS hostname or use a trust anchor/constraint that identifies only Webex KMS.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d78ada7832

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant