Skip to content

Fix jarsigner PKIX error for non-exportable AKV certificates via AIA chain completion - #47977

Open
rujche with Copilot wants to merge 60 commits into
mainfrom
copilot/fix-jarsigner-certificate-chain-issue
Open

Fix jarsigner PKIX error for non-exportable AKV certificates via AIA chain completion#47977
rujche with Copilot wants to merge 60 commits into
mainfrom
copilot/fix-jarsigner-certificate-chain-issue

Conversation

Copilot AI commented Feb 11, 2026

Copy link
Copy Markdown
Contributor

Problem

When a non-exportable DigiCert certificate in Azure Key Vault is used with jarsigner, the signed JAR fails verification with:

Warning:
This jar contains entries whose certificate chain is invalid. Reason:
PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException:
  unable to find valid certification path to requested target

Fixes #44267 (regression confirmed unfixed in 2.11.0-beta.1 per this comment).

Root Cause

The Azure Key Vault /secrets/{alias} endpoint returns only the leaf certificate for non-exportable keys (no private key, and no intermediate CA certificates when the caller only provided the leaf cert during MergeCertificate). The getCertificateChain() method therefore returns a one-element array. An earlier fix (PR #41303) added intermediate cert support and PR #47977 added ordering — neither helps when the intermediate is genuinely absent.

Without the intermediate CA in the chain:

  • jarsigner embeds only the leaf cert in the JAR signature block
  • jarsigner -verify calls Java's PKIX path builder to trace leaf -> intermediate -> root CA
  • The intermediate is not in the JAR and not in cacerts -> path building fails

Solution

After loading certificates from the AKV secret bundle, walk the chain upward. If the top certificate is not self-signed (i.e. the chain is incomplete), parse its AIA (Authority Information Access) extension (OID 1.3.6.1.5.5.7.1.1) to find the caIssuers URL, download the missing intermediate CA certificate (DER or PEM), and append it. Repeat until the chain reaches a self-signed root CA or no AIA URL is found.

This is the standard mechanism used by browsers, AzureSignTool, and major signing tools to complete certificate chains at runtime.

Example (AIA in certificate context):

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            12:34:56:78:9a:bc
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C=US, O=Example PKI, CN=Example Intermediate CA
        Validity
            Not Before: Jul  1 00:00:00 2026 GMT
            Not After : Jul  1 23:59:59 2028 GMT
        Subject: C=US, O=Example Corp, CN=example-signer

        X509v3 extensions:
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Key Usage: critical
                Digital Signature
            Authority Information Access:
                OCSP - URI:http://ocsp.example.com
                CA Issuers - URI:http://cacerts.example.com/ExampleIntermediateCA.crt
            X509v3 Subject Key Identifier:
                4A:7F:01:2B:...

In this flow, chain completion follows CA Issuers level by level (leaf -> intermediate -> root) until the chain is complete or no issuer can be resolved.

Implementation

Core Algorithm

  • Chain Completion: Standard AIA-based chain completion per RFC 4158.
  • Leaf Selection: Prioritizes certificates with existing issuers in chain; falls back to first non-issuer (order-independent). Self-signed roots are only used as fallback in incomplete chains.
  • Chain End Detection: Uses findValidChainEnd() to identify true chain end (prevents interfering with extra/unplaced certs).
  • Issuer Selection from AIA Payloads: When AIA responds with multiple certs (bundle), selects the cert that matches expected issuer DN and can verify the current cert signature.
  • Duplicate/Existing Issuer Handling: Detects valid issuers already present in chain and repositions to contiguous order when needed.
  • Safety Limits: Max 10 issuer-resolution attempts per chain to prevent infinite loops.

Security Hardening

  • SSRF Protection: System property azure.keyvault.jca.disable-aia-download allows disabling AIA in locked-down environments.
  • Self-Signed Verification: Validates both subject==issuer and signature verification.
  • Issuer Validation:
    • Signature must verify with issuer public key.
    • Issuer must be a CA (basicConstraints or self-signed root).
    • If KeyUsage is present, keyCertSign must be set (RFC 5280 alignment).
  • Robust Fallback Parsing: Supports DER and PEM AIA responses and validates issuer before chain insertion.

Lint and Review Follow-ups

  • Narrowed exception type in isValidIssuer to satisfy SpotBugs (REC_CATCH_EXCEPTION).
  • Updated long log messages to satisfy Checkstyle line-length limits.
  • Aligned chain diagnostic logging (Self-Signed) with signature-based self-signed verification.
  • Corrected findValidChainEnd JavaDoc wording to match actual chain-walking direction.

Changes

File Change
CertificateUtil.java Add completeChainViaAia(), downloadIssuerCertificateFromAia(), findValidChainEnd(), isSelfSignedCertificate(), isValidIssuer(); improve orderCertificateChain(); enforce KeyUsage keyCertSign; select matching issuer from AIA bundles; improve diagnostics and lint compliance
HttpUtil.java Add getBytes(String url) for binary downloads (10s timeout) with exception logging for diagnostics
AiaCertificateChainTest.java Expanded AIA coverage to 13 tests including PEM bundle issuer selection and KeyUsage edge case
CertificateOrderTest.java 5 tests verifying certificate chain ordering (leaf -> intermediate -> root) for PEM and PKCS12 formats, including regression test for incomplete [root, leaf] chains and concrete single-cert edge case
CHANGELOG.md Document bug fix, system property, and security advisory
README.md Add azure.keyvault.jca.disable-aia-download to Exposed Options
checkstyle-suppressions.xml Keep module-level GoodLoggingCheck suppression for CertificateUtil.java/HttpUtil.java (module continues to use java.util.logging)

Test Coverage

PKIX Path Building:

Certificate Ordering (PEM/PKCS12):

  • testPemCertificateChainOrder ✓ Verifies correct leaf -> intermediate -> root ordering
  • testPkcs12CertificateChainOrder ✓ Validates PKCS12 format chain ordering
  • testOrderCertificateChainIncompleteRootFirst ✓ Regression test: [root, leaf] input correctly orders to [leaf, root]

AIA Chain Completion:

  • Leaf-only -> downloads intermediate and root ✓
  • Full chain -> skips unnecessary downloads ✓
  • Cross-signed certificates -> handled correctly ✓
  • Duplicate detection/repositioning -> preserves contiguous valid chain ✓
  • PEM bundle AIA response -> selects matching issuer cert, not first entry ✓
  • KeyUsage CA edge case -> rejects issuer missing keyCertSign when KeyUsage present ✓
  • System property disable -> azure.keyvault.jca.disable-aia-download=true prevents AIA downloads ✓

Results: 97/97 tests pass (0 failures, 29 skipped)

Customer Validation

Confirmed working by the original reporter (@manfrede) in #44267 (comment) using azure-security-keyvault-jca-2.12.0-beta.1.jar. The AIA chain completion path executed correctly as shown in debug logs:

FEIN: Downloading issuer certificate from AIA URL: http://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA3842021CA1.crt
FEIN: Downloaded intermediate CA certificate via AIA: CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1,...
FEIN: Downloading issuer certificate from AIA URL: http://cacerts.digicert.com/DigiCertTrustedRootG4.crt
FEIN: Downloaded intermediate CA certificate via AIA: CN=DigiCert Trusted Root G4,...
FEIN: Certificate chain is complete. Root CA: CN=DigiCert Trusted Root G4,...
FEIN: Certificate chain after AIA completion [3 certs]:
  [0] leaf (CN=Industrie Informatik GmbH)
  [1] CN=DigiCert Trusted G4 Code Signing RSA4096 SHA384 2021 CA1 | Self-Signed: false
  [2] CN=DigiCert Trusted Root G4 | Self-Signed: true

jarsigner produced no warnings and the full chain was embedded correctly.

Backward Compatibility

  • ✓ Existing behavior preserved for complete chains
  • ✓ AIA completion enabled by default
  • ✓ System property opt-out for locked-down environments
  • ✓ No breaking changes to public APIs

Copilot AI and others added 4 commits February 11, 2026 02:26
Co-authored-by: rujche <171773178+rujche@users.noreply.github.com>
Co-authored-by: rujche <171773178+rujche@users.noreply.github.com>
Co-authored-by: rujche <171773178+rujche@users.noreply.github.com>
Co-authored-by: rujche <171773178+rujche@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix jarsigner warning for invalid certificate chain Fix certificate chain ordering for jarsigner compatibility Feb 11, 2026
Copilot AI requested a review from rujche February 11, 2026 02:36
@rujche rujche added azure-spring All azure-spring related issues azure-spring-jca labels Feb 11, 2026
@rujche rujche added this to the 2026-03 milestone Feb 11, 2026
@rujche rujche moved this from Todo to In Progress in Spring Cloud Azure Feb 11, 2026
@rujche rujche moved this from In Progress to Need Feedback in Spring Cloud Azure Feb 27, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Hi @copilot. Thank you for your interest in helping to improve the Azure SDK experience and for your contribution. We've noticed that there hasn't been recent engagement on this pull request. If this is still an active work stream, please let us know by pushing some changes or leaving a comment. Otherwise, we'll close this out in 7 days.

@rujche rujche modified the milestones: 2026-03, Backlog May 8, 2026
rujche added 2 commits July 8, 2026 10:23
…ficates

When a non-exportable Azure Key Vault certificate is used with jarsigner,
the /secrets/ endpoint returns only the leaf certificate (no intermediate
CAs). This causes jarsigner -verify to fail with:

  PKIX path building failed: unable to find valid certification path
  to requested target

Fix: after loading certificates from the AKV secret bundle, walk the
chain upward from the current top. If the top cert is not self-signed
(i.e. the chain is incomplete), parse the AIA (Authority Information
Access) extension (OID 1.3.6.1.5.5.7.1.1) to find the CA Issuers URL
and download the missing intermediate CA certificate. Repeat until the
chain reaches a self-signed root CA.

Changes:
- CertificateUtil: add completeChainViaAia() and
  downloadIssuerCertificateFromAia() methods; call completeChainViaAia()
  from loadCertificatesFromSecretBundleValue() after ordering
- HttpUtil: add getBytes(String url) for binary (DER) certificate downloads
- AiaCertificateChainTest: 10 unit tests including two PKIX path-building
  tests that reproduce the exact reported error and confirm it is resolved

Fixes: #44267
@rujche rujche changed the title Fix certificate chain ordering for jarsigner compatibility Fix jarsigner PKIX error for non-exportable AKV certificates via AIA chain completion Jul 8, 2026
@rujche
rujche requested a review from Copilot July 8, 2026 02:54
@rujche rujche moved this from Need Feedback to In Progress in Spring Cloud Azure Jul 8, 2026
@rujche rujche removed this from the Backlog milestone Jul 8, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

@vcolin7 vcolin7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The changes look good for an initial review, but there are a few places where I think we can make improvements. If you have any questions, I'd be happy to discuss my comments further :)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (1)

sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java:344

  • AIA CA Issuers URLs are treated case-sensitively here. URI schemes are case-insensitive, and some certificates may contain HTTP:// / HTTPS:// or surrounding whitespace, which would cause chain completion to skip an otherwise valid issuer URL.
                String url = location.getName().toString();
                if (!url.startsWith("http://") && !url.startsWith("https://")) {
                    continue; // Only HTTP/HTTPS URLs are supported
                }

@moarychan

Copy link
Copy Markdown
Member

@vcolin7 Thank you so much for your valuable comments. I have analyzed and resolved them one by one. Please review this PR again. Thank you very much!

completeChainViaAia() logged "Downloaded intermediate CA certificate via
AIA" for every issuer it added, but the message was wrong on both counts:
the certificate is served from the response cache whenever the URL was
already fetched, and the last hop of a chain is a root rather than an
intermediate. A production trace showed the message emitted 6 times while
only 2 HTTP requests were made, which overstates AIA outbound traffic
three-fold when diagnosing network issues.

Report the issuer as resolved instead, and assert the download and
resolution counts stay distinct across a cached second run.
LOGGER.entering() renders every parameter it is given, so enabling FINER
logging wrote the client secret in clear text, along with every Key Vault
JSON response body and the private key PEM. A production trace captured at
that level held the client secret, 3 access tokens and 6 PKCS12 key
bundles.

Pass only non-secret parameters. The char[] passwords elsewhere are left
untouched: they render as "[C@hash" and reveal nothing.

@vcolin7 vcolin7 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thank you for addressing all of my comments @moarychan! This is really great work. There are still a few things to consider I left comments for. Additionally, here's a design suggestion I think we should consider:

I think the cache should retain successful and failed resolutions. Successful responses would honor HTTP freshness metadata, with a bounded fallback such as your already implemented 24 hours and never beyond the downloaded certificate’s validity. Failed or empty responses should have a short negative TTL, perhaps one minute, so a missing or temporarily unavailable endpoint is not retried for every load. Concurrent misses for the same URL could share one in-flight request so they do not issue duplicate downloads. A somewhat larger bounded result cache, such as 128 entries, with targeted expired/LRU eviction would also avoid the current whole-cache clearing behavior.

Comment thread sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md Outdated
Comment thread sdk/keyvault/azure-security-keyvault-jca/CHANGELOG.md Outdated
moarychan and others added 7 commits August 5, 2026 09:18
Co-authored-by: vcolin7 <vicolina@microsoft.com>
…re/security/keyvault/jca/implementation/utils/AiaCertificateChainUtil.java

Co-authored-by: vcolin7 <vicolina@microsoft.com>
Co-authored-by: vcolin7 <vicolina@microsoft.com>
A contiguous chain may still be missing issuers above its terminal
certificate. Invoke cache-first AIA completion whenever the valid chain
does not end in a self-signed root, so multi-level PKI chains can be
completed without repeating HTTP requests on subsequent loads.
@moarychan

Copy link
Copy Markdown
Member

Implemented the cache design in 9f16c1314cc. Successful responses honor HTTP freshness with a 24-hour and certificate-validity cap. Failed, empty, or unparseable responses use a one-minute negative TTL. Concurrent misses for the same URL share one request, and the cache is bounded to 128 entries with expired-first/LRU eviction. Cached responses are still validated against each certificate on every use.

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

Labels

azure-spring All azure-spring related issues azure-spring-jca

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

[BUG] jarsigner + jca still reports that entries in certificate chain are invalid

5 participants