Skip to content

[OID4VCI] Self-Signed x5c Bypasses Key Attestation Validation #48035

@ahus1

Description

@ahus1

Summary

AttestationValidatorUtil.verifierFromX5CChain() skips PKIX certificate chain validation when x5c[0] is self-signed (subject == issuer). An attacker generates a software key, creates a self-signed cert, and forges a key attestation JWT claiming hardware-level security (VAN.5). Keycloak accepts it and issues a Verifiable Credential.

Affected: Keycloak <= 26.5.6 (latest stable), all versions with oid4vc-vci feature
CWE: CWE-295 (Improper Certificate Validation)

Acknowledgement: The Keycloak Security team acknowledges Trung Nguyen from CyStack Security for reporting this.
As OID4VCI is in experimental mode, this is handled as a regular bug in upstream, and not a CVE.
The automated reproducer is not attached to the public issue.

Vulnerable Code

services/.../oid4vc/issuance/keybinding/AttestationValidatorUtil.java:332-347

// Check if this is a self-signed certificate (for test environments)
X509Certificate firstCert = certChain.get(0);
boolean isSelfSigned = firstCert.getSubjectX500Principal()
    .equals(firstCert.getIssuerX500Principal());

// Only validate the certificate chain if it's not a self-signed certificate in a test environment
if (!isSelfSigned) {
    CertPathValidator validator = CertPathValidator.getInstance("PKIX");
    PKIXParameters params = new PKIXParameters(getTrustAnchors());
    params.setRevocationEnabled(false);
    validator.validate(certPath, params);
}

PublicKey publicKey = certChain.get(0).getPublicKey();  // attacker's key trusted

Comment says "for test environments" but there is no runtime check. Bypass is always active.

Execution Path

POST /protocol/oid4vc/credential
  AttestationProofValidator.validateProof()
    AttestationValidatorUtil.validateAttestationJwt()
      validateJwsHeader()          -- alg/typ check passes (ES256)
      verifierFromX5CChain()       -- PKIX SKIPPED for self-signed cert
      verify JWT signature         -- passes (attacker signed with own key)
      validateAttestationPayload() -- nonce/resistance checks pass
    return attested_keys           -- attacker's software JWK
  credential issued

No secondary defense: no SPI intercept, no realm-level trust store, no event listener.

Confirmed

Test x5c cert HTTP Result
Self-signed (subject == issuer) CN=Fake WSCD / CN=Fake WSCD 200 Credential issued
Non-self-signed (subject != issuer) CN=Leaf / CN=Fake CA 400 PKIX rejects

Tested on Keycloak 26.5.6 (latest stable) and nightly (2026-03-30). Both jwt_vc_json and dc+sd-jwt formats exploitable.

Impact

Key attestation proves a credential holder's private key resides in secure hardware. This bypass allows:

  • Forge hardware attestation -- Software key claims iso_18045_high (VAN.5). Server cannot distinguish from genuine hardware.
  • Clone credentials -- Software key is exportable. Hardware-backed keys are not.
  • Identity fraud -- In eIDAS 2.0 / EU Digital Identity Wallet, government-issued credentials (national ID, driving licence) can be obtained with forged hardware guarantees, then cloned and shared.
  • LoA bypass -- Relying parties requiring "high" assurance accept forged credentials.

Fix

if (isSelfSigned) {
    throw new VCIssuerException(ErrorType.INVALID_PROOF,
        "Self-signed certificates are not accepted for key attestation");
}
CertPathValidator validator = CertPathValidator.getInstance("PKIX");
PKIXParameters params = new PKIXParameters(getTrustAnchors());
params.setRevocationEnabled(false);
validator.validate(certPath, params);

This issue was originally tracked in the private repository. Migrated by @ahus1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions