Skip to content

Select the CRL signed by the path's issuer when CAs share a subject name - #1944

Merged
kevinherron merged 4 commits into
integration/1.2from
fix/same-name-ca-crl-selection
Sep 7, 2026
Merged

Select the CRL signed by the path's issuer when CAs share a subject name#1944
kevinherron merged 4 commits into
integration/1.2from
fix/same-name-ca-crl-selection

Conversation

@kevinherron

Copy link
Copy Markdown
Contributor

A client cannot validate any peer certified by a Global Discovery Server certificate group that offers more than one certificate type.

Such a group issues one CA per certificate type, all under the group's single configured subject name, and publishes a CRL for each. The OPC Foundation .NET reference GDS does this by design, so it is what real trust lists look like: several same-DN CA certificates and several same-DN CRLs.

PKIX selects a CRL by issuer name alone. Given more than one candidate it does not try each in turn — it takes the indirect-CRL route through DistributionPointFetcher.verifyCRL, fails to build a path to the CRL signer, and reports UNDETERMINED_REVOCATION_STATUS. validateTrustedCertPath turns that into Bad_CertificateRevocationUnknown and the handshake dies, even though the correct CRL was in the collection and verifies cleanly. The outcome does not depend on CRL order, and two same-named CAs are enough.

I hit this against a real GDS: an Ignition Gateway pulling from a group carrying RSA, NIST P-256 and P-384 was rejected by a Milo server on every connection. Each connection came up as soon as all but its own CA was removed from the server's trust list.

What changed

selectApplicableCrls filters the collection before it reaches PKIX. A CRL is dropped when the path holds a same-named certificate and none of them signed it. A CRL whose issuer name matches nothing in the path belongs to some other path and is left alone, so this only removes candidates that were going to be ambiguous.

The fallback now registers the revocation checker it configures. When the custom checker cannot be installed, the fallback built a PKIXRevocationChecker, set NO_FALLBACK, PREFER_CRLS and SOFT_FAIL on it, and never called addCertPathChecker. Only setRevocationEnabled(true) took effect, so the default checker ran with default options and a CRL that could not be located failed the whole path — the opposite of what the warning logged beside it promises.

stack-core's surefire run opens sun.security.provider.certpath. OpcUaCertificateRevocationChecker reaches into that package by reflection and bails in its constructor when access is denied, so without the option every validation silently falls back. No test in the project had ever run the custom checker. The two paths differ on whether ValidationCheck.REVOCATION_LISTS is honored, and this bug is only visible on the custom one — with the fallback's SOFT_FAIL now correctly applied, the symptom is suppressed rather than fixed.

Notes for review

The new tests only discriminate because CrlTestUtil now emits the AuthorityKeyIdentifier and CRL number a real CA writes. With a key-id-only AKI the JDK resolves the right CRL and the failure does not reproduce at all, so a test built on JcaX509ExtensionUtils.createAuthorityKeyIdentifier alone passes with or without the fix. I verified the discrimination by reverting each change in turn.

I also checked the fix against the bytes from the live failure — the issued leaf, the three same-DN CAs and their three CRLs — outside this test harness: unfiltered gives UNDETERMINED_REVOCATION_STATUS, filtered gives a valid path.

stack-core 1244 tests and the sdk-server / integration-tests run (1288) pass. Each commit builds and tests green on its own. Unrelated and pre-existing: BinaryDataTypeDictionaryReaderTest in dtd-reader fails with NoClassDefFoundError: com/google/common/io/ByteStreams on a clean tree as well.

OpcUaCertificateRevocationChecker reaches into sun.security.provider.certpath
by reflection and bails in its constructor when that access is denied, so
without --add-opens every validation falls back to the default JDK checker and
logs a warning. The test JVM had no such option, which meant no test in the
project has ever run the custom checker, and the two paths differ in whether
ValidationCheck.REVOCATION_LISTS is honored.

Open the package for surefire so the checker under test is the one production
uses when the option is present.
When the custom revocation checker cannot be installed, the fallback builds a
PKIXRevocationChecker, sets NO_FALLBACK, PREFER_CRLS and SOFT_FAIL on it, and
then never adds it to the PKIXParameters. Only setRevocationEnabled(true) takes
effect, so the default checker runs with default options and a CRL that cannot
be located fails the whole path rather than being tolerated, contradicting the
warning logged alongside it.
PKIX selects a CRL by issuer name alone. Handed several candidates it does not
try each in turn: it takes the indirect-CRL route, fails to build a path to the
CRL signer, and reports UNDETERMINED_REVOCATION_STATUS, so the whole path fails
even though the right CRL was present.

A Global Discovery Server produces exactly that trust list. A certificate group
that offers more than one certificate type issues one CA per type, all under
the group's single configured subject name, and publishes a CRL for each, so a
client that pulls its trust list from such a group cannot validate any peer the
group certified.

Drop a CRL when the path holds a same-named certificate but none of them signed
it. A CRL whose issuer name matches nothing in the path belongs to some other
path and is left alone.

CrlTestUtil now emits the AuthorityKeyIdentifier and CRL number a real CA
writes. The extension matters: with a key-id-only AKI the JDK resolves the
right CRL and the failure does not reproduce, so a test built on
JcaX509ExtensionUtils.createAuthorityKeyIdentifier alone passes either way.
Use the existing Bouncy Castle routing when verifying Brainpool CRL
signatures so unsupported SunEC curves cannot discard valid revocation
information. Keep default provider selection for other issuer keys.

Cover decoded P256 and P384 CRLs with revoked and unrevoked certificates,
including required and optional CRL checks. Document the validation flow
and provider invariant at package level.
@kevinherron
kevinherron merged commit 3b61954 into integration/1.2 Sep 7, 2026
3 checks passed
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