This is an automated email from the ASF dual-hosted git repository.

jamesnetherton pushed a commit to branch camel-quarkus-main
in repository https://gitbox.apache.org/repos/asf/camel-quarkus-examples.git


The following commit(s) were added to refs/heads/camel-quarkus-main by this 
push:
     new b1301e9d http-pqc-j17: validate certificate chains in the custom trust 
manager
b1301e9d is described below

commit b1301e9d233ce2c31a8e2cdaf587971f87a8eec2
Author: James Netherton <[email protected]>
AuthorDate: Fri Aug 7 15:01:22 2026 +0100

    http-pqc-j17: validate certificate chains in the custom trust manager
    
    * http-pqc-j17: validate certificate chains in the custom trust manager
    
    HybridPqcX509TrustManager ran its own certificate checks instead of 
delegating,
    and registering it replaced the trust manager Quarkus builds from the 
configured
    truststore. Client authentication was therefore effectively 
unauthenticated: any
    self-signed certificate carrying the three Chimera extensions was accepted, 
as
    were expired certificates, and checkServerTrusted had an empty body that 
trusted
    every chain. The ML-DSA-65 signature covered only the subject DN, so it 
could be
    lifted onto another certificate along with that DN.
    
    - HybridPqcTrustManagerCustomizer: retrieve the trust manager Quarkus built 
from
      quarkus.http.ssl.certificate.trust-store-file and pass it to
      HybridPqcX509TrustManager as a delegate instead of discarding it. Fail 
startup
      if no truststore is configured, rather than leaving the PQC check as the 
only
      barrier.
    - HybridPqcX509TrustManager: delegate chain, trust anchor and 
validity-period
      validation, then verify the ML-DSA-65 signature on each certificate in the
      chain. Implement checkServerTrusted, and return the configured anchors 
from
      getAcceptedIssuers.
    - CertificatesUtil: verify the alternative signature with the issuer's 
ML-DSA-65
      public key using X509CertificateHolder.isAlternativeSignatureValid, which
      covers the whole TBSCertificate. Drop the cert.verify(own public key) 
call,
      which established nothing.
    - HybridCertificateGenerator: add a hybrid CA holding an RSA and an 
ML-DSA-65
      keypair, and issue the server and client certificates from it, signing 
each
      with both keys so that forging RSA alone cannot mint an accepted 
certificate.
      The truststores now hold the CA. Also add subject alternative names to the
      server certificate and write the CA out as ca-cert.pem.
    - README.adoc, PQC-EXPLANATION.adoc: correct the claims about what is 
validated
      and what is quantum-safe, use curl --cacert instead of -k, and drop the 
pinned
      BouncyCastle version.
    - Tests: cover an untrusted hybrid certificate, an expired one, one issued 
by a
      different CA, one carrying extensions lifted from another certificate, 
and an
      RSA-only certificate issued by the trusted CA where the missing ML-DSA-65
      signature is the only fault.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    * http-pqc-j17: extend X509ExtendedTrustManager and resolve issuers by key 
identifier
    
    Addresses the two review observations on PR #558.
    
    - HybridPqcX509TrustManager extends X509ExtendedTrustManager rather than 
implementing
      X509TrustManager, and delegates the Socket and SSLEngine overloads. Those 
carry the
      connection context the platform needs for endpoint identification. JSSE 
wraps a plain
      X509TrustManager in one of its own that performs the hostname check 
afterwards, so a
      stack built around SSLContext stays safe, but one that bypasses it -- 
Netty's OpenSSL
      provider, for instance -- applies no such wrapper. Anyone adapting the 
class for
      client-side TLS there would have accepted a certificate valid for another 
host.
    - HybridPqcTrustManagerCustomizer requires the platform trust manager to be 
an
      X509ExtendedTrustManager, failing startup instead of quietly wrapping one 
that cannot
      verify hostnames. Every JDK provider has returned the extended form since 
Java 7.
    - findIssuer resolves in three ordered steps: a candidate whose subject key 
identifier
      matches the certificate's authority key identifier, then the certificate 
itself when it
      is self-issued, then a candidate matching the issuer name alone for 
certificates that
      predate the extensions. Matching on the name alone diverges from the path 
the platform
      actually built: a CA that has rolled its key sits in the truststore twice 
under one DN
      with two different ML-DSA-65 keys, and cross-certified CAs share a DN by 
design. Picking
      the wrong one rejects a certificate the delegate just accepted. The 
self-issued step
      matters because a trust anchor need carry no authority key identifier of 
its own, and
      peers do send their CA -- the keystores this example generates put it in 
the chain.
      Preferring the key identifier also stops a certificate that merely 
carries the right
      issuer name from displacing the one the platform built the path through, 
which was the
      third case raised in review.
    - HybridCertificateGenerator issues certificates carrying subject and 
authority key
      identifiers, which is what the matching above needs and is standard 
practice besides.
    - Tests: a recording delegate asserts each three-argument overload reaches 
the matching
      delegate method rather than being forwarded to the two-argument one, and 
a key-rollover
      case puts two CAs sharing a DN in the truststore and requires 
certificates from both to
      validate, sent both alone and with their CA in the chain. Each fails 
without the
      corresponding fix.
    - README.adoc, PQC-EXPLANATION.adoc: explain why the extended interface and 
the key
      identifier matter.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    
    ---------
    
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
---
 http-pqc-j17/PQC-EXPLANATION.adoc                  |  65 ++--
 http-pqc-j17/README.adoc                           |  95 ++++--
 .../main/java/org/acme/http/pqc/PqcCamelRoute.java |  11 +-
 .../certificates/HybridCertificateGenerator.java   | 326 ++++++++++++---------
 .../pqc/certificates/SecurityConfiguration.java    |   4 +-
 .../pqc/certificates/util/CertificatesUtil.java    | 174 ++++-------
 .../java/org/acme/http/pqc/crypto/ChimeraOids.java |  26 +-
 .../HybridPqcTrustManagerCustomizer.java           |  68 ++++-
 .../trustmanager/HybridPqcX509TrustManager.java    | 274 +++++++++++++++--
 .../src/main/resources/application.properties      |   4 +-
 .../org/acme/http/pqc/CertificatesUtilTest.java    | 121 ++++++++
 .../java/org/acme/http/pqc/ForgedCertificates.java | 169 +++++++++++
 .../test/java/org/acme/http/pqc/HttpPqcTest.java   | 128 +++++---
 .../http/pqc/HybridPqcX509TrustManagerTest.java    | 269 +++++++++++++++++
 14 files changed, 1317 insertions(+), 417 deletions(-)

diff --git a/http-pqc-j17/PQC-EXPLANATION.adoc 
b/http-pqc-j17/PQC-EXPLANATION.adoc
index f29ef7e1..06ed5f83 100644
--- a/http-pqc-j17/PQC-EXPLANATION.adoc
+++ b/http-pqc-j17/PQC-EXPLANATION.adoc
@@ -191,21 +191,26 @@ This is a **workaround approach** when you can't modify 
TLS behavior - demonstra
 │            │  Serial: 1234567890       │                    │
 │            │                           │                    │
 │  ┌─────────▼──────────┐      ┌─────────▼──────────┐         │
-│  │ RSA-2048           │      │ Extension OID      │         │
-│  │ Public Key         │      │ 2.5.29.72:         │         │
-│  │ (Standard field)   │      │ ML-DSA-65 Public   │         │
-│  └────────────────────┘      │ Key                │         │
+│  │ RSA-2048           │      │ CA Extension OID   │         │
+│  │ Public Key         │      │ 2.5.29.72: the     │         │
+│  │ (Standard field)   │      │ CA's ML-DSA-65     │         │
+│  └────────────────────┘      │ Public Key         │         │
 │                              └────────────────────┘         │
 │  ┌────────────────────┐      ┌────────────────────┐         │
 │  │ SHA256withRSA      │      │ Extension OID      │         │
 │  │ Signature          │      │ 2.5.29.74:         │         │
 │  │ [RSA signature     │      │ ML-DSA-65          │         │
-│  │  bytes]            │      │ Signature bytes    │         │
-│  └────────────────────┘      └────────────────────┘         │
+│  │  bytes] by the CA  │      │ Signature bytes    │         │
+│  └────────────────────┘      │ by the CA          │         │
+│                              └────────────────────┘         │
 │                                                             │
 │  Verification: BOTH signatures must be valid!               │
 │  - Old systems: Check RSA only ✓                            │
 │  - PQC systems: Check RSA ✓ AND ML-DSA ✓                    │
+│                                                             │
+│  Both are made by the CA, and the ML-DSA one is verified    │
+│  with the CA's key from the CA certificate. Forging RSA     │
+│  alone is therefore not enough to mint a certificate.       │
 └─────────────────────────────────────────────────────────────┘
 ----
 
@@ -260,13 +265,6 @@ This is a **workaround approach** when you can't modify 
TLS behavior - demonstra
 │ ┌─────────────────────────────┐                     │
 │ │ Extensions:                 │                     │
 │ │ ┌─────────────────────────┐ │                     │
-│ │ │ OID 2.5.29.72:          │ │                     │
-│ │ │ altSubjectPublicKeyInfo │ │                     │
-│ │ │   Algorithm: ML-DSA-65  │ │ ✓ Post-Quantum      │
-│ │ │   Key: [3082071E...]    │ │                     │
-│ │ │   Size: 1976 bytes      │ │                     │
-│ │ └─────────────────────────┘ │                     │
-│ │ ┌─────────────────────────┐ │                     │
 │ │ │ OID 2.5.29.73:          │ │                     │
 │ │ │ altSignatureAlgorithm   │ │                     │
 │ │ │   Algorithm: ML-DSA-65  │ │                     │
@@ -276,8 +274,15 @@ This is a **workaround approach** when you can't modify 
TLS behavior - demonstra
 │ │ │ altSignatureValue       │ │                     │
 │ │ │   Signature: [D1L1...]  │ │ ✓ Post-Quantum      │
 │ │ │   Size: 3309 bytes      │ │                     │
+│ │ │   Made by the CA's      │ │                     │
+│ │ │   ML-DSA-65 key over    │ │                     │
+│ │ │   the whole cert body   │ │                     │
 │ │ └─────────────────────────┘ │                     │
 │ └─────────────────────────────┘                     │
+│                                                     │
+│ The CA certificate carries OID 2.5.29.72            │
+│ (subjectAltPublicKeyInfo) holding the CA's          │
+│ ML-DSA-65 public key, which verifies the above.     │
 │ ┌─────────────────────────────┐                     │
 │ │ Signature (PRIMARY):        │                     │
 │ │   Algorithm: SHA256withRSA  │ ✓ Classical         │
@@ -324,8 +329,8 @@ Endpoint: https://localhost:8443/pqc/secure
                 ┌──────────────┴──────────────┐
                 ▼                             ▼
         ┌───────────────┐            ┌────────────────┐
-        │ Validate RSA  │            │ Validate       │
-        │ Signature ✓   │            │ ML-DSA-65 ✓    │
+        │ Validate chain│            │ Validate       │
+        │ to anchor ✓   │            │ ML-DSA-65 ✓    │
         └───────┬───────┘            └────────┬───────┘
                 │                             │
                 └──────────────┬──────────────┘
@@ -371,11 +376,11 @@ Endpoint: https://localhost:8443/pqc/secure
 
 |**Protection Level**
 |⚠️ Demo only, TLS is vulnerable
-|✅ Real security at TLS layer
+|⚠️ Authentication is hybrid, but the key exchange is still classical
 
 |**When to use**
 |Learning, experimenting
-|Production-ready hybrid solution
+|Migrating certificates ahead of a PQC-capable TLS stack
 
 |**Code complexity**
 |Simple (app endpoints)
@@ -387,12 +392,14 @@ Endpoint: https://localhost:8443/pqc/secure
 * **Approach #2** is like having a regular lock on your door (RSA TLS) but 
demonstrating fancy new locks inside your house (PQC demos)
 * **Approach #3** is like installing a dual-lock system ON YOUR DOOR (Chimera 
certificates validated at TLS handshake)
 
-Approach #3 provides actual security because:
+Approach #3 strengthens authentication because:
 
 . Invalid certificates are **rejected during TLS handshake** before any data 
is exchanged
-. Both RSA and ML-DSA-65 signatures must be valid to establish the connection
+. A certificate must chain to a trust anchor, be within its validity period, 
**and** carry a valid ML-DSA-65 signature to establish the connection
 . Works with the existing TLS infrastructure (backward compatible)
 
+It does **not** make the connection quantum-safe: the key exchange that 
protects the session is still classical, so a recorded session can be decrypted 
by a future quantum computer. Only the certificate authentication gains 
post-quantum protection. Use `http-pqc-j21` for quantum-safe transport.
+
 '''
 
 == Why Chimera is the Best Migration Path
@@ -450,12 +457,16 @@ ADVANTAGE: One certificate works for everyone!
 |✅ Yes
 
 |**HybridPqcX509TrustManager**
-|Validates both RSA and ML-DSA-65 signatures at TLS layer
+|Delegates chain, trust anchor, expiry and endpoint identification checks to 
the platform trust manager, then adds the ML-DSA-65 signature check
 |✅ Yes
 
+|**HybridPqcTrustManagerCustomizer**
+|Registers the custom TrustManager with the Vert.x HTTP server, passing it the 
platform trust manager to delegate to
+|N/A
+
 |**SecurityConfiguration**
-|Configures custom TrustManager and client authentication
-|✅ Yes
+|Registers the BouncyCastle provider and generates the keystores at startup
+|N/A
 
 |**/pqc/secure endpoint**
 |Accessible only with valid hybrid PQC certificates
@@ -465,12 +476,12 @@ ADVANTAGE: One certificate works for everyone!
 **Think of it like this:**
 
 * The **transport tunnel** (HTTPS/TLS) still uses classical encryption (Java 
17 limitation)
-* But the **authentication** (certificate validation) is quantum-safe with 
dual signatures
+* But the **authentication** requires a second, post-quantum signature from 
the CA, so forging RSA is not enough to be let in
 * The **Chimera certificate** is like a dual-lock ID card: RSA works today, 
ML-DSA-65 protects against future quantum attacks
-* Only clients with valid hybrid certificates can access the secure endpoint
+* Only clients with valid hybrid certificates that chain to the trusted CA can 
access the secure endpoint
 
 **When Java 21+ becomes standard:**
 
-* The transport itself can be upgraded to full PQC (e.g., Kyber for key 
exchange)
-* Chimera certificates will work perfectly with full PQC TLS
-* Zero code changes needed - we're already PQC-ready!
+* The transport itself can be upgraded to a post-quantum key exchange (for 
example ML-KEM)
+* Chimera certificates continue to work, since the PQC data rides in X.509 
extensions
+* The custom TrustManager is no longer needed once the TLS stack validates PQC 
signatures itself
diff --git a/http-pqc-j17/README.adoc b/http-pqc-j17/README.adoc
index 9f93ab4d..590606d2 100644
--- a/http-pqc-j17/README.adoc
+++ b/http-pqc-j17/README.adoc
@@ -19,25 +19,35 @@ Since **Java 17 doesn't support PQC algorithms natively in 
TLS**, this example d
 
 * **Chimera hybrid certificates**: Combines RSA-2048 + ML-DSA-65 (NIST FIPS 
204)
 * **X.509 extensions**: Standard extension OIDs (2.5.29.72-74) for alternative 
signatures
-* **Application-level validation**: Custom X509TrustManager validates both RSA 
and ML-DSA-65 signatures during TLS handshake
-* **BouncyCastle 1.83**: Provides ML-DSA-65 post-quantum signature algorithm 
(JCA provider only)
+* **Application-level validation**: A custom X509ExtendedTrustManager adds an 
ML-DSA-65 signature check on top of the standard certificate validation 
performed during the TLS handshake
+* **BouncyCastle**: Provides the ML-DSA-65 post-quantum signature algorithm 
(JCA provider only)
 
-This approach validates PQC signatures at the application level rather than 
relying on Java's TLS stack. For native PQC TLS support (hybrid cipher suites 
like X25519Kyber768), use Java 21+ with BouncyCastle JSSE provider.
+This approach validates PQC signatures at the application level rather than 
relying on Java's TLS stack. For native PQC TLS support (hybrid key exchange 
such as X25519MLKEM768), use Java 21+ with the BouncyCastle JSSE provider.
+
+IMPORTANT: Only the certificate authentication is hybrid here. The connection 
itself is still protected by classical cryptography, because Java 17 cannot 
negotiate a post-quantum key exchange. A recorded session remains vulnerable to 
"harvest now, decrypt later"; see the `http-pqc-j21` example for quantum-safe 
transport.
 
 NOTE: For detailed diagrams and architecture explanation, see 
link:PQC-EXPLANATION.adoc[PQC Visual Guide].
 
 == Certificate Generation
 
-Hybrid certificates are automatically generated on every application startup. 
No manual steps required.
+A hybrid certificate authority and the certificates it issues are generated on 
every application startup. No manual steps required.
+
+The CA holds both an RSA and an ML-DSA-65 keypair, and signs every certificate 
it issues with both. It publishes its ML-DSA-65 public key in its own 
`subjectAltPublicKeyInfo` extension (OID 2.5.29.72), which is what relying 
parties use to verify the alternative signature on the certificates it issued.
 
 Generated files in `target/certs/`:
 
-* `server-hybrid-keystore.p12` - Server certificate with RSA + ML-DSA-65
-* `server-hybrid-truststore.p12` - Truststore for validating clients
+* `server-hybrid-keystore.p12` - Server certificate and key, with the CA 
certificate in the chain
+* `server-hybrid-truststore.p12` - Trust anchor the server validates client 
certificates against, holding the CA certificate
 * `client-hybrid-keystore.p12` - Client certificate with PQC (for success test)
-* `client-hybrid-keystore-cert.pem` - Client certificate in PEM format (for 
curl testing)
-* `client-hybrid-keystore-key.pem` - Client private key in PEM format (for 
curl testing)
-* `client-rsa-only-keystore.p12` - Client certificate without PQC (for failure 
test)
+* `client-hybrid-truststore.p12` - Trust anchor clients validate the server 
against, holding the CA certificate
+* `client-rsa-only-keystore.p12` - Client certificate issued by the same CA 
but without PQC (for failure test)
+* `ca-cert.pem` - CA certificate in PEM format, for clients that cannot read 
PKCS12 truststores (for example `curl --cacert`)
+* `client-hybrid-cert.pem` - Client certificate in PEM format (for curl 
testing)
+* `client-hybrid-key.pem` - Client private key in PEM format (for curl testing)
+
+The server certificate is issued for `CN=localhost` with subject alternative 
names `DNS:localhost` and `IP:127.0.0.1`, so clients can verify the server 
identity rather than having to disable verification.
+
+Because the CA certificate is the trust anchor, an attacker who could forge 
RSA signatures still could not mint a certificate this example accepts: they 
would also need the CA's ML-DSA-65 private key. That is the property the hybrid 
certificate is there to provide.
 
 == Prerequisites
 
@@ -68,23 +78,38 @@ NOTE: With `client-auth=required`, the TLS handshake 
rejects connections without
 
 PEM files are automatically generated during application startup in 
`target/certs/`:
 
-* `client-hybrid-keystore-cert.pem` - Hybrid certificate (RSA + ML-DSA-65)
-* `client-hybrid-keystore-key.pem` - Private key for hybrid certificate
+* `client-hybrid-cert.pem` - Hybrid certificate (RSA + ML-DSA-65)
+* `client-hybrid-key.pem` - Private key for hybrid certificate
+* `ca-cert.pem` - CA certificate, used by curl to verify the server
 
 Test with hybrid certificate (should succeed):
 
 [source,shell]
 ----
-curl --cert target/certs/client-hybrid-keystore-cert.pem \
-       --key target/certs/client-hybrid-keystore-key.pem \
-       -k \
+curl --cert target/certs/client-hybrid-cert.pem \
+       --key target/certs/client-hybrid-key.pem \
+       --cacert target/certs/ca-cert.pem \
        https://localhost:8443/pqc/secure
 
 ✓ Hybrid PQC certificate validated at TLS layer!
-Your connection is quantum-safe.
-Both RSA and ML-DSA-65 signatures were validated during TLS handshake.
+
+Your certificate chained to a configured trust anchor and carried a valid
+ML-DSA-65 alternative signature. Both were checked during the TLS handshake.
+...
 ----
 
+Test with the RSA-only certificate. It is issued by the same CA, so it passes 
chain validation; the handshake fails purely because it carries no ML-DSA-65 
alternative signature:
+
+[source,shell]
+----
+curl --cert-type P12 \
+       --cert target/certs/client-rsa-only-keystore.p12:changeit \
+       --cacert target/certs/ca-cert.pem \
+       https://localhost:8443/pqc/secure
+----
+
+curl reports a TLS alert such as `certificate unknown` or `bad certificate`; 
the exact wording depends on the TLS library it was built against. The 
application log records the reason the certificate was refused.
+
 == Package and Run the Application
 
 Once you are done with developing you may want to package and run the 
application.
@@ -130,7 +155,12 @@ The test suite validates:
 * Hybrid keystores are generated automatically
 * `/pqc/secure` rejects requests without client certificates
 * `/pqc/secure` accepts hybrid certificates (RSA + ML-DSA-65)
-* `/pqc/secure` rejects RSA-only certificates missing PQC extensions
+* `/pqc/secure` rejects an RSA-only certificate issued by the same CA, where 
the missing ML-DSA-65 signature is the only fault
+* `/pqc/secure` rejects a self-signed hybrid certificate that no trust anchor 
vouches for, even though its PQC extensions are valid
+* `/pqc/secure` rejects an expired hybrid certificate
+* The ML-DSA-65 signature check rejects a certificate carrying PQC extensions 
copied from another certificate
+* The ML-DSA-65 signature check rejects a certificate issued by a different 
CA, and one whose issuer publishes no PQC public key
+* The trust manager rejects untrusted server certificates as well as client 
ones, and advertises the configured trust anchors
 
 For native mode testing:
 
@@ -141,7 +171,7 @@ mvn clean verify -Dnative
 
 == How It Works (Java 17 Application-Level Validation)
 
-This example works around Java 17's lack of native PQC support by implementing 
custom certificate validation at the application level, integrated into the TLS 
handshake via a custom `X509TrustManager`.
+This example works around Java 17's lack of native PQC support by implementing 
custom certificate validation at the application level, integrated into the TLS 
handshake via a custom `X509ExtendedTrustManager`.
 
 === Certificate Structure
 
@@ -157,23 +187,32 @@ Both RSA and ML-DSA-65 signatures must validate for 
authentication to succeed.
 
 1. Client connects with TLS client certificate
 2. Java's TLS stack invokes our custom `HybridPqcX509TrustManager` during 
handshake
-3. `CertificatesUtil.validateHybridCertificate()` performs application-level 
checks:
-   - RSA signature (standard X.509 validation via Java crypto)
-   - ML-DSA-65 algorithm OID (2.5.29.73) - manual extraction
-   - ML-DSA-65 public key (2.5.29.72) - manual extraction
-   - ML-DSA-65 signature (2.5.29.74) - manual verification via BouncyCastle
-4. If all checks pass, TLS handshake continues
-5. If any check fails, TLS handshake is rejected
+3. The trust manager delegates to the platform trust manager that Quarkus 
built from the configured truststore, which performs the standard X.509 checks: 
chain building, trust anchor lookup and validity period
+4. Only if those pass, `CertificatesUtil.validateHybridCertificate()` adds the 
post-quantum checks for each certificate in the chain:
+   - ML-DSA-65 algorithm OID (2.5.29.73) is present and names ML-DSA-65
+   - ML-DSA-65 signature (2.5.29.74) is present
+   - The issuer's ML-DSA-65 public key (2.5.29.72) is located, either from the 
next certificate in the chain or from the trust anchors, matching on the 
authority key identifier rather than the issuer name alone
+   - The signature is verified by BouncyCastle over the whole certificate 
body, using that issuer key
+5. If all checks pass, TLS handshake continues
+6. If any check fails, TLS handshake is rejected
 
-NOTE: This validation happens at the application level, not within Java's TLS 
implementation. Java 17's TLS stack only handles the standard RSA certificate; 
our custom code validates the PQC extensions.
+The verification key deliberately comes from the *issuer*, not from the 
certificate being checked. Verifying an alternative signature against a key 
carried by the same certificate authenticates nothing, since whoever produced 
the certificate chose both. Finding that issuer by distinguished name alone is 
not enough either: a CA that has rolled its key appears in the truststore twice 
under one name with two different ML-DSA-65 keys, so the authority key 
identifier is what tells them apart.
+
+IMPORTANT: The ordering in steps 3 and 4 is the point of this example. 
Registering a custom trust manager *replaces* the platform one rather than 
adding to it, because JSSE consults only the trust manager it is given. A 
custom trust manager that performs its own checks without delegating therefore 
accepts any certificate that satisfies those checks — including a self-signed 
one that no trust anchor vouches for. Delegating first, then layering the PQC 
check on top, is what keeps chain, an [...]
+
+NOTE: `HybridPqcX509TrustManager` extends `X509ExtendedTrustManager`, not 
`X509TrustManager`. Only the extended interface receives the `Socket` or 
`SSLEngine` for the connection, and that context is what the platform needs to 
perform endpoint identification — hostname verification, for a client checking 
a server. A wrapper implementing the plain interface loses it. JSSE compensates 
by wrapping such a trust manager in one of its own that performs the check 
afterwards, so a stack built aro [...]
+
+NOTE: The PQC part of the validation happens at the application level, not 
within Java's TLS implementation. Java 17's TLS stack only understands the 
standard RSA certificate; our custom code validates the PQC extensions.
 
 == Important Notes
 
 * **Java 17 Only**: This example is specifically designed for Java 17, which 
lacks native PQC support in its TLS stack. It demonstrates application-level 
validation as a workaround. Do not use this approach if you can upgrade to Java 
21+.
 
-* **Development Only**: Certificates are regenerated on every startup. For 
production, use persistent certificates from a trusted CA.
+* **Development Only**: The CA and the certificates it issues are regenerated 
on every startup, and the CA is self-signed. For production, use persistent 
certificates from a real CA, and keep the CA's ML-DSA-65 private key protected 
as carefully as its RSA one — the post-quantum guarantee rests on it.
+
+* **No revocation checking**: This example performs no CRL or OCSP checks. A 
production deployment needs them.
 
-* **BouncyCastle 1.83**: Uses standardized ML-DSA-65 naming per NIST FIPS 204 
(effective August 2024). This example uses JCA provider only, not JSSE. Note 
that ML-DSA (standardized) and Dilithium (pre-standard) are NOT interoperable.
+* **BouncyCastle**: Uses standardized ML-DSA-65 naming per NIST FIPS 204 
(effective August 2024). This example uses the JCA provider only, not JSSE. 
Note that ML-DSA (standardized) and Dilithium (pre-standard) are NOT 
interoperable.
 
 == Additional Resources
 
diff --git a/http-pqc-j17/src/main/java/org/acme/http/pqc/PqcCamelRoute.java 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/PqcCamelRoute.java
index 2d8f95ac..ae8fd795 100644
--- a/http-pqc-j17/src/main/java/org/acme/http/pqc/PqcCamelRoute.java
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/PqcCamelRoute.java
@@ -33,10 +33,13 @@ public class PqcCamelRoute extends EndpointRouteBuilder {
                 .log("Processing request with validated hybrid PQC 
certificate")
                 .setBody(constant(
                         "✓ Hybrid PQC certificate validated at TLS layer!\n\n" 
+
-                                "Your connection is quantum-safe.\n" +
-                                "Both RSA and ML-DSA-65 signatures were 
validated during TLS handshake.\n\n" +
-                                "This demonstrates TLS-layer validation using 
a custom X509TrustManager.\n" +
-                                "Invalid or RSA-only certificates are rejected 
during the TLS handshake.\n"))
+                                "Your certificate chained to a configured 
trust anchor and carried a valid\n" +
+                                "ML-DSA-65 alternative signature. Both were 
checked during the TLS handshake.\n\n" +
+                                "This demonstrates TLS-layer validation using 
a custom trust manager that\n" +
+                                "adds a post-quantum signature check on top of 
the standard X.509 checks.\n\n" +
+                                "Note: the connection itself is protected by 
classical cryptography. Java 17\n" +
+                                "cannot negotiate a post-quantum key exchange, 
so only the certificate\n" +
+                                "authentication is hybrid here. See 
http-pqc-j21 for quantum-safe transport.\n"))
                 
.to(log("pqc-secure").showExchangePattern(false).showBodyType(false));
     }
 }
diff --git 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/HybridCertificateGenerator.java
 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/HybridCertificateGenerator.java
index e5f7d8db..bc32becc 100644
--- 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/HybridCertificateGenerator.java
+++ 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/HybridCertificateGenerator.java
@@ -26,18 +26,20 @@ import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.KeyStore;
 import java.security.SecureRandom;
-import java.security.Signature;
 import java.security.cert.X509Certificate;
 import java.util.Date;
 
 import org.acme.http.pqc.crypto.ChimeraOids;
-import org.bouncycastle.asn1.DERBitString;
 import org.bouncycastle.asn1.x500.X500Name;
-import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.GeneralName;
+import org.bouncycastle.asn1.x509.GeneralNames;
 import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
 import org.bouncycastle.cert.X509CertificateHolder;
 import org.bouncycastle.cert.X509v3CertificateBuilder;
 import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils;
 import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
 import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
 import org.bouncycastle.operator.ContentSigner;
@@ -46,24 +48,40 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Utility class for generating and persisting Chimera hybrid certificates.
- * These certificates combine classical RSA with post-quantum ML-DSA-65 
signatures
- * using X.509 extensions as specified in the BouncyCastle PQC Almanac.
+ * Generates a small Chimera hybrid certificate hierarchy: a certificate 
authority holding both an RSA
+ * and an ML-DSA-65 keypair, which issues the server and client certificates.
  *
  * <p>
- * Certificates are automatically generated at application startup by
- * {@link SecurityConfiguration} if they don't already exist.
+ * Every certificate the CA issues is signed twice. The RSA signature goes in 
the standard signature
+ * field, and the ML-DSA-65 signature goes in the {@code altSignatureValue} 
extension, produced by
+ * BouncyCastle's {@link X509v3CertificateBuilder#build(ContentSigner, 
boolean, ContentSigner)} which
+ * computes it over the {@code TBSCertificate}. Do not hand-roll that 
signature over a single field
+ * such as the subject DN: a signature that does not cover the body proves 
nothing about the rest of
+ * the certificate.
+ *
+ * <p>
+ * The CA publishes its ML-DSA-65 public key in its own {@code 
subjectAltPublicKeyInfo} extension,
+ * which is what relying parties use to verify the alternative signature on 
the certificates it
+ * issued. Because the CA certificate is the trust anchor, an attacker who can 
forge RSA signatures
+ * still cannot mint a certificate that validates: they would also need the 
CA's ML-DSA-65 private
+ * key. That is the property this example exists to demonstrate.
+ *
+ * <p>
+ * Certificates are generated at application startup by {@link 
SecurityConfiguration}.
  */
 public class HybridCertificateGenerator {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(HybridCertificateGenerator.class);
 
-    // Demo keystore password - DO NOT use in production
+    // WARNING: This password is hardcoded for DEMONSTRATION purposes only.
+    // In production, use environment variables, secrets management, or secure 
configuration.
     private static final String KEYSTORE_PASSWORD = "changeit";
-    private static final String KEYSTORES_DIR = "target/certs";
+    private static final String CERT_DIR = "target/certs";
+    private static final String CA_DN = "CN=PQC Hybrid CA,O=Apache Camel 
Quarkus,C=US";
 
     /**
-     * Certificate data holder for keypairs and certificates.
+     * A certificate together with the keypairs that belong to its subject. 
For the CA, the ML-DSA-65
+     * keypair is the one used to sign the certificates it issues.
      */
     public static class CertificateData {
         public final KeyPair rsaKeyPair;
@@ -78,170 +96,210 @@ public class HybridCertificateGenerator {
     }
 
     /**
-     * Generates a Chimera hybrid certificate combining RSA and ML-DSA-65.
+     * Generates the CA, the certificates it issues, and the keystores and 
truststores holding them.
+     */
+    public static void generateKeystores() throws Exception {
+        CertificateData ca = generateCertificateAuthority();
+        LOG.info("Hybrid CA created: {}", 
ca.certificate.getSubjectX500Principal());
+
+        // The server certificate carries subject alternative names for 
localhost and 127.0.0.1 so that
+        // clients can verify the server identity instead of having to disable 
verification
+        GeneralNames serverAltNames = new GeneralNames(new GeneralName[] {
+                new GeneralName(GeneralName.dNSName, "localhost"),
+                new GeneralName(GeneralName.iPAddress, "127.0.0.1") });
+
+        CertificateData server = issueCertificate(ca, "localhost", true, 
serverAltNames);
+        CertificateData clientHybrid = issueCertificate(ca, "client-hybrid", 
true, null);
+
+        // Issued by the same CA, but without the ML-DSA-65 alternative 
signature. It therefore passes
+        // chain validation and is rejected solely by the post-quantum check.
+        CertificateData clientRsaOnly = issueCertificate(ca, 
"client-rsa-only", false, null);
+
+        saveKeyStore(Paths.get(CERT_DIR, "server-hybrid-keystore.p12"), 
server, ca, "server");
+        saveKeyStore(Paths.get(CERT_DIR, "client-hybrid-keystore.p12"), 
clientHybrid, ca, "client");
+        saveKeyStore(Paths.get(CERT_DIR, "client-rsa-only-keystore.p12"), 
clientRsaOnly, ca, "client");
+
+        // Both sides validate the peer's certificate chain against the CA, 
which is the trust anchor
+        saveTrustStore(Paths.get(CERT_DIR, "server-hybrid-truststore.p12"), 
ca.certificate, "pqc-hybrid-ca");
+        saveTrustStore(Paths.get(CERT_DIR, "client-hybrid-truststore.p12"), 
ca.certificate, "pqc-hybrid-ca");
+
+        // PEM files for tools that cannot read PKCS12, such as curl
+        savePem(Paths.get(CERT_DIR, "ca-cert.pem"), ca.certificate);
+        savePem(Paths.get(CERT_DIR, "client-hybrid-cert.pem"), 
clientHybrid.certificate);
+        savePem(Paths.get(CERT_DIR, "client-hybrid-key.pem"), 
clientHybrid.rsaKeyPair.getPrivate());
+    }
+
+    /**
+     * Generates the self-signed hybrid certificate authority.
      *
-     * @param  commonName          The CN for the certificate subject
-     * @param  includeAltSignature Whether to include the ML-DSA-65 
alternative signature
-     * @return                     CertificateData containing keypairs and 
certificate
+     * <p>
+     * The CA publishes its ML-DSA-65 public key in the {@code 
subjectAltPublicKeyInfo} extension so
+     * that verifiers can check the alternative signature on the certificates 
it issues.
+     *
+     * @return CertificateData whose keypairs are the CA's signing keys
      */
-    public static CertificateData generateChimeraCertificate(String 
commonName, boolean includeAltSignature)
-            throws Exception {
-        LOG.debug("Generating Chimera hybrid certificate for CN={}, 
includeAltSignature={}",
-                commonName, includeAltSignature);
+    public static CertificateData generateCertificateAuthority() throws 
Exception {
+        LOG.debug("Generating hybrid certificate authority");
 
-        // Generate RSA keypair (classical algorithm)
-        KeyPairGenerator rsaKpg = KeyPairGenerator.getInstance("RSA");
-        rsaKpg.initialize(2048, new SecureRandom());
-        KeyPair rsaKeyPair = rsaKpg.generateKeyPair();
+        KeyPair rsaKeyPair = generateRsaKeyPair();
+        KeyPair mlDsaKeyPair = generateMlDsaKeyPair();
+        X500Name caName = new X500Name(CA_DN);
+
+        X509v3CertificateBuilder certBuilder = certificateBuilder(caName, 
caName, rsaKeyPair.getPublic());
+        certBuilder.addExtension(Extension.basicConstraints, true, new 
BasicConstraints(true));
+
+        // The subject key identifier is how relying parties tell two CAs 
apart when they share a
+        // distinguished name, which happens whenever a CA rolls its key
+        certBuilder.addExtension(Extension.subjectKeyIdentifier, false,
+                new 
JcaX509ExtensionUtils().createSubjectKeyIdentifier(rsaKeyPair.getPublic()));
+
+        // Publish the CA's ML-DSA-65 public key: this is the key that 
verifies the alternative
+        // signature on every certificate the CA issues
+        certBuilder.addExtension(ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO, 
false,
+                
SubjectPublicKeyInfo.getInstance(mlDsaKeyPair.getPublic().getEncoded()));
 
-        // Generate ML-DSA-65 keypair (PQC algorithm - NIST FIPS 204)
-        KeyPairGenerator mlDsaKpg = KeyPairGenerator.getInstance("ML-DSA-65", 
"BC");
-        KeyPair mlDsaKeyPair = mlDsaKpg.generateKeyPair();
+        // Self-signed with both of its own keys
+        X509CertificateHolder certHolder = certBuilder.build(
+                rsaSigner(rsaKeyPair), false, mlDsaSigner(mlDsaKeyPair));
+
+        return new CertificateData(rsaKeyPair, mlDsaKeyPair, 
toX509Certificate(certHolder));
+    }
+
+    /**
+     * Issues a certificate signed by the given authority.
+     *
+     * @param  issuer              The CA whose RSA and ML-DSA-65 keys sign 
the certificate
+     * @param  commonName          The CN for the certificate subject
+     * @param  includeAltSignature Whether to add the CA's ML-DSA-65 
alternative signature. Passing
+     *                             {@code false} produces a classical, 
RSA-only certificate.
+     * @param  subjectAltNames     Subject alternative names to add, or {@code 
null} for none
+     * @return                     CertificateData holding the subject's own 
RSA keypair
+     */
+    public static CertificateData issueCertificate(CertificateData issuer, 
String commonName,
+            boolean includeAltSignature, GeneralNames subjectAltNames) throws 
Exception {
+        LOG.debug("Issuing certificate for CN={}, includeAltSignature={}", 
commonName, includeAltSignature);
+
+        KeyPair rsaKeyPair = generateRsaKeyPair();
 
-        // Build certificate
-        X500Name issuer = new X500Name("CN=PQC Hybrid CA,O=Apache Camel 
Quarkus,C=US");
         X500Name subject = new X500Name("CN=" + commonName + ",O=Apache Camel 
Quarkus,C=US");
-        BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
+        X500Name issuerName = 
X500Name.getInstance(issuer.certificate.getSubjectX500Principal().getEncoded());
+
+        X509v3CertificateBuilder certBuilder = certificateBuilder(issuerName, 
subject, rsaKeyPair.getPublic());
+        certBuilder.addExtension(Extension.basicConstraints, true, new 
BasicConstraints(false));
+
+        // The authority key identifier names the key that signed this 
certificate, so a verifier can
+        // pick the right issuer instead of guessing from the distinguished 
name
+        JcaX509ExtensionUtils extensionUtils = new JcaX509ExtensionUtils();
+        certBuilder.addExtension(Extension.authorityKeyIdentifier, false,
+                
extensionUtils.createAuthorityKeyIdentifier(issuer.certificate));
+        certBuilder.addExtension(Extension.subjectKeyIdentifier, false,
+                
extensionUtils.createSubjectKeyIdentifier(rsaKeyPair.getPublic()));
+
+        if (subjectAltNames != null) {
+            certBuilder.addExtension(Extension.subjectAlternativeName, false, 
subjectAltNames);
+        }
+
+        X509CertificateHolder certHolder;
+        if (includeAltSignature) {
+            // BouncyCastle adds the altSignatureAlgorithm and 
altSignatureValue extensions itself,
+            // signing the TBSCertificate with the CA's ML-DSA-65 key so that 
the signature covers the
+            // whole certificate body
+            certHolder = certBuilder.build(
+                    rsaSigner(issuer.rsaKeyPair), false, 
mlDsaSigner(issuer.mlDsaKeyPair));
+        } else {
+            certHolder = certBuilder.build(rsaSigner(issuer.rsaKeyPair));
+        }
+
+        return new CertificateData(rsaKeyPair, null, 
toX509Certificate(certHolder));
+    }
+
+    private static X509v3CertificateBuilder certificateBuilder(X500Name 
issuer, X500Name subject,
+            java.security.PublicKey subjectPublicKey) {
         Date notBefore = new Date();
         Date notAfter = new Date(System.currentTimeMillis() + 365L * 24 * 60 * 
60 * 1000); // 1 year
 
-        X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
+        return new JcaX509v3CertificateBuilder(
                 issuer,
-                serial,
+                new BigInteger(64, new SecureRandom()),
                 notBefore,
                 notAfter,
                 subject,
-                rsaKeyPair.getPublic());
+                subjectPublicKey);
+    }
 
-        if (includeAltSignature) {
-            // Add ML-DSA-65 alternative public key (Chimera extension)
-            SubjectPublicKeyInfo mlDsaPubKeyInfo = SubjectPublicKeyInfo
-                    .getInstance(mlDsaKeyPair.getPublic().getEncoded());
-            certBuilder.addExtension(ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO, 
false, mlDsaPubKeyInfo);
-
-            // Add alternative signature algorithm (Chimera extension)
-            AlgorithmIdentifier mlDsaSigAlg = new 
AlgorithmIdentifier(ChimeraOids.ML_DSA_65);
-            certBuilder.addExtension(ChimeraOids.ALT_SIGNATURE_ALGORITHM, 
false, mlDsaSigAlg);
-
-            // Generate ML-DSA-65 alternative signature (Chimera extension)
-            Signature mlDsaSig = Signature.getInstance("ML-DSA-65", "BC");
-            mlDsaSig.initSign(mlDsaKeyPair.getPrivate());
-            mlDsaSig.update(subject.getEncoded()); // Sign subject DN as per 
Chimera spec
-            byte[] mlDsaSignature = mlDsaSig.sign();
-            certBuilder.addExtension(ChimeraOids.ALT_SIGNATURE_VALUE, false, 
new DERBitString(mlDsaSignature));
-
-            LOG.debug("ML-DSA-65 extensions added to certificate");
-        }
+    private static KeyPair generateRsaKeyPair() throws Exception {
+        KeyPairGenerator rsaKpg = KeyPairGenerator.getInstance("RSA");
+        rsaKpg.initialize(2048, new SecureRandom());
+        return rsaKpg.generateKeyPair();
+    }
 
-        // Sign with RSA (primary signature)
-        ContentSigner rsaSigner = new 
JcaContentSignerBuilder("SHA256withRSA").build(rsaKeyPair.getPrivate());
-        X509CertificateHolder certHolder = certBuilder.build(rsaSigner);
+    private static KeyPair generateMlDsaKeyPair() throws Exception {
+        // ML-DSA-65 - NIST FIPS 204. Java 17 has no built-in implementation, 
so BouncyCastle provides it
+        return KeyPairGenerator.getInstance("ML-DSA-65", 
"BC").generateKeyPair();
+    }
 
-        // Convert to X509Certificate
-        X509Certificate certificate = new JcaX509CertificateConverter()
-                .setProvider("BC")
-                .getCertificate(certHolder);
+    private static ContentSigner rsaSigner(KeyPair keyPair) throws Exception {
+        return new 
JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate());
+    }
 
-        LOG.debug("Chimera certificate generated successfully for CN={}", 
commonName);
+    private static ContentSigner mlDsaSigner(KeyPair keyPair) throws Exception 
{
+        return new 
JcaContentSignerBuilder("ML-DSA-65").setProvider("BC").build(keyPair.getPrivate());
+    }
 
-        return new CertificateData(rsaKeyPair, mlDsaKeyPair, certificate);
+    private static X509Certificate toX509Certificate(X509CertificateHolder 
holder) throws Exception {
+        return new 
JcaX509CertificateConverter().setProvider("BC").getCertificate(holder);
     }
 
     /**
-     * Generates a keystore with the specified certificate type.
-     *
-     * @param commonName          The CN for the certificate
-     * @param includeAltSignature Whether to include ML-DSA-65 extensions
-     * @param keystorePath        Path where keystore will be saved
-     * @param alias               Keystore entry alias
-     * @param includeTruststore   Whether to also create a truststore
-     * @param savePemFiles        Whether to also save certificate and key as 
PEM files
+     * Saves a private key with its certificate chain. The CA certificate is 
included in the chain so
+     * that the peer receives the full path during the handshake.
      */
-    private static void generateKeystore(
-            String commonName,
-            boolean includeAltSignature,
-            String keystorePath,
-            String alias,
-            boolean includeTruststore,
-            boolean savePemFiles) throws Exception {
-
-        CertificateData certData = generateChimeraCertificate(commonName, 
includeAltSignature);
-
+    private static void saveKeyStore(Path path, CertificateData certData, 
CertificateData ca, String alias)
+            throws Exception {
         KeyStore keyStore = KeyStore.getInstance("PKCS12", "BC");
         keyStore.load(null, null);
         keyStore.setKeyEntry(alias,
                 certData.rsaKeyPair.getPrivate(),
                 KEYSTORE_PASSWORD.toCharArray(),
-                new X509Certificate[] { certData.certificate });
-
-        saveKeyStore(keyStore, keystorePath, KEYSTORE_PASSWORD);
-        LOG.info("Keystore created: {}", keystorePath);
-
-        if (includeTruststore) {
-            String trustPath = keystorePath.replace("-keystore.p12", 
"-truststore.p12");
-            KeyStore trustStore = KeyStore.getInstance("PKCS12", "BC");
-            trustStore.load(null, null);
-            trustStore.setCertificateEntry(alias + "-ca", 
certData.certificate);
-            saveKeyStore(trustStore, trustPath, KEYSTORE_PASSWORD);
-            LOG.info("Truststore created: {}", trustPath);
-        }
+                new X509Certificate[] { certData.certificate, ca.certificate 
});
 
-        if (savePemFiles) {
-            // Save certificate as PEM
-            String certPemPath = keystorePath.replace(".p12", "-cert.pem");
-            try (FileWriter certWriter = new FileWriter(certPemPath);
-                    JcaPEMWriter pemWriter = new JcaPEMWriter(certWriter)) {
-                pemWriter.writeObject(certData.certificate);
-            }
-            LOG.info("Certificate PEM file created: {}", certPemPath);
-
-            // Save private key as PEM
-            String keyPemPath = keystorePath.replace(".p12", "-key.pem");
-            try (FileWriter keyWriter = new FileWriter(keyPemPath);
-                    JcaPEMWriter pemWriter = new JcaPEMWriter(keyWriter)) {
-                pemWriter.writeObject(certData.rsaKeyPair.getPrivate());
-            }
-            LOG.info("Private key PEM file created: {}", keyPemPath);
-        }
+        store(keyStore, path);
+        LOG.info("Keystore created: {}", path);
     }
 
-    /**
-     * Generates server hybrid keystore with RSA + ML-DSA-65 certificate.
-     */
-    public static void generateServerKeystore() throws Exception {
-        generateKeystore("localhost", true, KEYSTORES_DIR + 
"/server-hybrid-keystore.p12",
-                "server", true, false);
+    private static void saveTrustStore(Path path, X509Certificate cert, String 
alias) throws Exception {
+        KeyStore trustStore = KeyStore.getInstance("PKCS12", "BC");
+        trustStore.load(null, null);
+        trustStore.setCertificateEntry(alias, cert);
+
+        store(trustStore, path);
+        LOG.info("Truststore created: {}", path);
     }
 
     /**
-     * Generates client hybrid keystore with RSA + ML-DSA-65 certificate.
-     * Also saves PEM files for manual curl testing.
+     * Saves a KeyStore to disk, overwriting any existing file.
      */
-    public static void generateClientHybridKeystore() throws Exception {
-        generateKeystore("client-hybrid", true, KEYSTORES_DIR + 
"/client-hybrid-keystore.p12",
-                "client", false, true);
+    public static void store(KeyStore keyStore, Path path) throws Exception {
+        createParentDirectory(path);
+        try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
+            keyStore.store(fos, KEYSTORE_PASSWORD.toCharArray());
+        }
     }
 
-    /**
-     * Generates client RSA-only keystore (no PQC extensions - for failure 
test).
-     */
-    public static void generateClientRsaOnlyKeystore() throws Exception {
-        generateKeystore("client-rsa-only", false, KEYSTORES_DIR + 
"/client-rsa-only-keystore.p12",
-                "client", false, false);
+    private static void savePem(Path path, Object object) throws Exception {
+        createParentDirectory(path);
+        try (FileWriter writer = new FileWriter(path.toFile());
+                JcaPEMWriter pemWriter = new JcaPEMWriter(writer)) {
+            pemWriter.writeObject(object);
+        }
+        LOG.info("PEM file created: {}", path);
     }
 
-    /**
-     * Saves a KeyStore to disk, overwriting any existing file.
-     */
-    public static void saveKeyStore(KeyStore keyStore, String path, String 
password) throws Exception {
-        Path dirPath = Paths.get(path).getParent();
+    private static void createParentDirectory(Path path) throws Exception {
+        Path dirPath = path.getParent();
         if (!Files.exists(dirPath)) {
             Files.createDirectories(dirPath);
             LOG.info("Created directory: {}", dirPath);
         }
-
-        try (FileOutputStream fos = new FileOutputStream(path)) {
-            keyStore.store(fos, password.toCharArray());
-        }
     }
 }
diff --git 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/SecurityConfiguration.java
 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/SecurityConfiguration.java
index 44309e07..2710681e 100644
--- 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/SecurityConfiguration.java
+++ 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/SecurityConfiguration.java
@@ -42,9 +42,7 @@ public class SecurityConfiguration {
     private void generateKeystores() {
         try {
             LOG.info("Generating fresh hybrid PQC keystores...");
-            HybridCertificateGenerator.generateServerKeystore();
-            HybridCertificateGenerator.generateClientHybridKeystore();
-            HybridCertificateGenerator.generateClientRsaOnlyKeystore();
+            HybridCertificateGenerator.generateKeystores();
             LOG.info("Hybrid PQC keystores generated successfully");
         } catch (Exception e) {
             LOG.error("Failed to generate hybrid PQC keystores", e);
diff --git 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificatesUtil.java
 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificatesUtil.java
index 84c4d8c3..8fe018a3 100644
--- 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificatesUtil.java
+++ 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/certificates/util/CertificatesUtil.java
@@ -16,31 +16,31 @@
  */
 package org.acme.http.pqc.certificates.util;
 
-import java.io.IOException;
-import java.security.InvalidKeyException;
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.PublicKey;
-import java.security.Signature;
-import java.security.SignatureException;
-import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.X509EncodedKeySpec;
 
 import org.acme.http.pqc.crypto.ChimeraOids;
-import org.bouncycastle.asn1.ASN1BitString;
-import org.bouncycastle.asn1.ASN1OctetString;
-import org.bouncycastle.asn1.ASN1Primitive;
 import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
+import org.bouncycastle.asn1.x509.Extension;
 import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.operator.ContentVerifierProvider;
+import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Utility class for validating Chimera hybrid certificates.
- * Validates both RSA and ML-DSA-65 signatures using static methods.
+ * Utility class for validating the post-quantum half of a Chimera hybrid 
certificate.
+ *
+ * <p>
+ * This covers <em>only</em> the ML-DSA-65 alternative signature. Chain 
building, trust anchors and
+ * validity periods are the job of the platform trust manager, which
+ * {@code org.acme.http.pqc.trustmanager.HybridPqcX509TrustManager} runs first.
+ *
+ * <p>
+ * The alternative signature is verified with the <em>issuer's</em> ML-DSA-65 
public key, taken from
+ * the issuer's {@code subjectAltPublicKeyInfo} extension. Verifying it 
against a key carried by the
+ * certificate under test would authenticate nothing, since whoever produced 
the certificate chose
+ * that key.
  */
 public final class CertificatesUtil {
 
@@ -51,34 +51,31 @@ public final class CertificatesUtil {
     }
 
     /**
-     * Validates a hybrid certificate by checking both RSA and ML-DSA-65 
signatures.
+     * Verifies that a certificate carries a valid ML-DSA-65 alternative 
signature made by its issuer.
      *
      * @param  cert                           The certificate to validate
-     * @throws CertificateValidationException if validation fails
+     * @param  issuer                         The certificate of the issuing 
authority, which must
+     *                                        publish its ML-DSA-65 public key 
in
+     *                                        {@code subjectAltPublicKeyInfo}
+     * @throws CertificateValidationException if the PQC extensions are 
missing, name a different
+     *                                        algorithm, or the alternative 
signature does not verify
      */
-    public static void validateHybridCertificate(X509Certificate cert) throws 
CertificateValidationException {
-        LOG.debug("Validating hybrid certificate for subject: {}", 
cert.getSubjectX500Principal());
+    public static void validateHybridCertificate(X509Certificate cert, 
X509Certificate issuer)
+            throws CertificateValidationException {
+        LOG.debug("Validating hybrid certificate for subject: {} issued by: 
{}",
+                cert.getSubjectX500Principal(), 
issuer.getSubjectX500Principal());
 
         try {
-            // Verify RSA signature (standard X.509 verification)
-            if (!verifyRsaSignature(cert)) {
-                throw new CertificateValidationException("RSA signature 
validation failed");
-            }
+            X509CertificateHolder holder = new 
X509CertificateHolder(cert.getEncoded());
 
-            LOG.debug("RSA signature verified");
-
-            // Verify alternative signature algorithm extension exists
-            byte[] altSigAlgExt = 
cert.getExtensionValue(ChimeraOids.ALT_SIGNATURE_ALGORITHM.getId());
-            if (altSigAlgExt == null) {
+            // Verify the alternative signature algorithm extension exists and 
is ML-DSA-65
+            Extension altSigAlg = 
holder.getExtension(ChimeraOids.ALT_SIGNATURE_ALGORITHM);
+            if (altSigAlg == null) {
                 throw new CertificateValidationException(
                         "PQC signature algorithm extension missing (OID 
2.5.29.73)");
             }
 
-            // Validate it's ML-DSA-65
-            ASN1Primitive primitive = 
ASN1Primitive.fromByteArray(altSigAlgExt);
-            byte[] octets = ((ASN1OctetString) primitive).getOctets();
-            AlgorithmIdentifier algId = 
AlgorithmIdentifier.getInstance(octets);
-
+            AlgorithmIdentifier algId = 
AlgorithmIdentifier.getInstance(altSigAlg.getParsedValue());
             if (!ChimeraOids.ML_DSA_65.equals(algId.getAlgorithm())) {
                 throw new CertificateValidationException(
                         "Expected ML-DSA-65 algorithm OID, found: " + 
algId.getAlgorithm());
@@ -86,27 +83,26 @@ public final class CertificatesUtil {
 
             LOG.debug("ML-DSA-65 algorithm OID validated");
 
-            // Extract and verify ML-DSA-65 signature
-            PublicKey mlDsaPublicKey = extractMlDsaPublicKey(cert);
-            if (mlDsaPublicKey == null) {
-                throw new CertificateValidationException(
-                        "PQC public key extension missing (OID 2.5.29.72)");
-            }
-
-            byte[] mlDsaSignature = extractMlDsaSignature(cert);
-            if (mlDsaSignature == null) {
+            if (holder.getExtension(ChimeraOids.ALT_SIGNATURE_VALUE) == null) {
                 throw new CertificateValidationException(
                         "PQC signature extension missing (OID 2.5.29.74)");
             }
 
-            if (!verifyMlDsaSignature(cert, mlDsaPublicKey, mlDsaSignature)) {
+            // The verification key comes from the issuer, not from the 
certificate being checked
+            SubjectPublicKeyInfo mlDsaPublicKey = issuerAltPublicKey(issuer);
+
+            // isAlternativeSignatureValid checks the signature against the 
DER-encoded TBSCertificate
+            // with the altSignatureValue extension removed, so it covers the 
whole certificate body
+            ContentVerifierProvider verifier = new 
JcaContentVerifierProviderBuilder()
+                    .setProvider("BC")
+                    .build(mlDsaPublicKey);
+
+            if (!holder.isAlternativeSignatureValid(verifier)) {
                 throw new CertificateValidationException("ML-DSA-65 signature 
validation failed");
             }
 
             LOG.debug("ML-DSA-65 signature verified - hybrid certificate 
valid");
 
-        } catch (IOException e) {
-            throw new CertificateValidationException("Failed to parse PQC 
extensions", e);
         } catch (CertificateValidationException e) {
             LOG.warn("Certificate validation failed: {}", e.getMessage());
             throw e;
@@ -118,83 +114,19 @@ public final class CertificatesUtil {
     }
 
     /**
-     * Verifies the RSA signature using standard X.509 verification.
+     * Extracts the issuer's ML-DSA-65 public key from its {@code 
subjectAltPublicKeyInfo} extension.
      */
-    private static boolean verifyRsaSignature(X509Certificate cert) {
-        try {
-            // Self-signed certificate - verify with its own public key
-            cert.verify(cert.getPublicKey());
-            return true;
-        } catch (CertificateException | NoSuchAlgorithmException | 
InvalidKeyException | SignatureException
-                | NoSuchProviderException e) {
-            LOG.error("RSA signature verification failed", e);
-            return false;
+    private static SubjectPublicKeyInfo issuerAltPublicKey(X509Certificate 
issuer) throws Exception {
+        Extension altPublicKey = new X509CertificateHolder(issuer.getEncoded())
+                .getExtension(ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO);
+
+        if (altPublicKey == null) {
+            throw new CertificateValidationException(
+                    "Issuer " + issuer.getSubjectX500Principal()
+                            + " publishes no PQC public key (OID 2.5.29.72), 
so the alternative signature "
+                            + "cannot be verified");
         }
-    }
-
-    /**
-     * Extracts the ML-DSA-65 public key from the altSubjectPublicKeyInfo 
extension.
-     */
-    private static PublicKey extractMlDsaPublicKey(X509Certificate cert) {
-        try {
-            byte[] extensionValue = 
cert.getExtensionValue(ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO.getId());
-            if (extensionValue == null) {
-                return null;
-            }
-
-            // Extension value is wrapped in OCTET STRING
-            ASN1Primitive primitive = 
ASN1Primitive.fromByteArray(extensionValue);
-            byte[] octets = ((ASN1OctetString) primitive).getOctets();
-
-            // Parse SubjectPublicKeyInfo
-            SubjectPublicKeyInfo spki = 
SubjectPublicKeyInfo.getInstance(octets);
 
-            // Convert to PublicKey using X509EncodedKeySpec
-            KeyFactory keyFactory = KeyFactory.getInstance("ML-DSA-65", "BC");
-            return keyFactory.generatePublic(new 
X509EncodedKeySpec(spki.getEncoded()));
-
-        } catch (IOException | NoSuchAlgorithmException | 
InvalidKeySpecException | NoSuchProviderException e) {
-            LOG.error("Failed to extract ML-DSA-65 public key", e);
-            return null;
-        }
-    }
-
-    /**
-     * Extracts the ML-DSA-65 signature from the altSignatureValue extension.
-     */
-    private static byte[] extractMlDsaSignature(X509Certificate cert) {
-        try {
-            byte[] extensionValue = 
cert.getExtensionValue(ChimeraOids.ALT_SIGNATURE_VALUE.getId());
-            if (extensionValue == null) {
-                return null;
-            }
-
-            // Extension value is wrapped in OCTET STRING
-            ASN1Primitive primitive = 
ASN1Primitive.fromByteArray(extensionValue);
-            byte[] octets = ((ASN1OctetString) primitive).getOctets();
-
-            // Parse as BIT STRING
-            ASN1BitString bitString = ASN1BitString.getInstance(octets);
-            return bitString.getBytes();
-
-        } catch (IOException e) {
-            LOG.error("Failed to extract ML-DSA-65 signature", e);
-            return null;
-        }
-    }
-
-    /**
-     * Verifies the ML-DSA-65 signature.
-     */
-    private static boolean verifyMlDsaSignature(X509Certificate cert, 
PublicKey pqcKey, byte[] signature) {
-        try {
-            Signature mlDsaVerify = Signature.getInstance("ML-DSA-65", "BC");
-            mlDsaVerify.initVerify(pqcKey);
-            mlDsaVerify.update(cert.getSubjectX500Principal().getEncoded());
-            return mlDsaVerify.verify(signature);
-        } catch (NoSuchAlgorithmException | InvalidKeyException | 
SignatureException | NoSuchProviderException e) {
-            LOG.error("ML-DSA-65 signature verification failed", e);
-            return false;
-        }
+        return SubjectPublicKeyInfo.getInstance(altPublicKey.getParsedValue());
     }
 }
diff --git 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/crypto/ChimeraOids.java 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/crypto/ChimeraOids.java
index d5fc254c..7f058fd0 100644
--- a/http-pqc-j17/src/main/java/org/acme/http/pqc/crypto/ChimeraOids.java
+++ b/http-pqc-j17/src/main/java/org/acme/http/pqc/crypto/ChimeraOids.java
@@ -17,6 +17,8 @@
 package org.acme.http.pqc.crypto;
 
 import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
+import org.bouncycastle.asn1.x509.Extension;
 
 /**
  * X.509 extension OIDs (Object Identifiers) for Chimera hybrid certificate 
format.
@@ -41,9 +43,14 @@ import org.bouncycastle.asn1.ASN1ObjectIdentifier;
  * </pre>
  *
  * <p>
- * <b>Why this matters:</b> Both signatures must be valid for authentication.
- * This provides quantum resistance (via ML-DSA-65) while maintaining backward
- * compatibility with standard TLS (via RSA).
+ * <b>Why this matters:</b> Both signatures must be valid for the certificate 
to be accepted, so a
+ * certificate remains tamper-evident even against an attacker who can forge 
RSA signatures. Standard
+ * TLS stacks that do not understand these extensions still verify the RSA 
signature as usual, which
+ * is what makes the format backward compatible.
+ *
+ * <p>
+ * The constants below simply alias the equivalent BouncyCastle definitions; 
they exist to gather the
+ * relevant references in one place rather than to redefine the OIDs.
  *
  * <h2>OID Hierarchy Explained:</h2>
  * <ul>
@@ -89,7 +96,7 @@ public final class ChimeraOids {
      * RFC 5280 - X.509 Certificate Extensions</a></li>
      * </ul>
      */
-    public static final ASN1ObjectIdentifier SUBJECT_ALT_PUBLIC_KEY_INFO = new 
ASN1ObjectIdentifier("2.5.29.72");
+    public static final ASN1ObjectIdentifier SUBJECT_ALT_PUBLIC_KEY_INFO = 
Extension.subjectAltPublicKeyInfo;
 
     /**
      * OID 2.5.29.73 - Alternative Signature Algorithm extension.
@@ -106,15 +113,16 @@ public final class ChimeraOids {
      * <li><a href="https://oidref.com/2.5.29.73";>OID Repository: 
2.5.29.73</a></li>
      * </ul>
      */
-    public static final ASN1ObjectIdentifier ALT_SIGNATURE_ALGORITHM = new 
ASN1ObjectIdentifier("2.5.29.73");
+    public static final ASN1ObjectIdentifier ALT_SIGNATURE_ALGORITHM = 
Extension.altSignatureAlgorithm;
 
     /**
      * OID 2.5.29.74 - Alternative Signature Value extension.
      * Contains the actual ML-DSA-65 digital signature bytes.
      *
      * <p>
-     * This is the post-quantum signature that proves the certificate is 
authentic
-     * and hasn't been tampered with, computed using the ML-DSA-65 private key.
+     * This is the post-quantum signature over the certificate body (the 
DER-encoded
+     * {@code TBSCertificate} with this extension removed), computed with the 
ML-DSA-65 private key.
+     * Because it covers the whole body, it detects tampering with any field 
of the certificate.
      *
      * <p>
      * <b>Official References:</b>
@@ -124,7 +132,7 @@ public final class ChimeraOids {
      * <li><a href="https://oidref.com/2.5.29.74";>OID Repository: 
2.5.29.74</a></li>
      * </ul>
      */
-    public static final ASN1ObjectIdentifier ALT_SIGNATURE_VALUE = new 
ASN1ObjectIdentifier("2.5.29.74");
+    public static final ASN1ObjectIdentifier ALT_SIGNATURE_VALUE = 
Extension.altSignatureValue;
 
     /**
      * OID 2.16.840.1.101.3.4.3.18 - ML-DSA-65 algorithm identifier (NIST FIPS 
204).
@@ -159,7 +167,7 @@ public final class ChimeraOids {
      * BouncyCastle Source: NIST ML-DSA OID definitions</a></li>
      * </ul>
      */
-    public static final ASN1ObjectIdentifier ML_DSA_65 = new 
ASN1ObjectIdentifier("2.16.840.1.101.3.4.3.18");
+    public static final ASN1ObjectIdentifier ML_DSA_65 = 
NISTObjectIdentifiers.id_ml_dsa_65;
 
     private ChimeraOids() {
         throw new AssertionError("Constants class cannot be instantiated");
diff --git 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcTrustManagerCustomizer.java
 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcTrustManagerCustomizer.java
index c4c48dbf..343b7111 100644
--- 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcTrustManagerCustomizer.java
+++ 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcTrustManagerCustomizer.java
@@ -16,43 +16,83 @@
  */
 package org.acme.http.pqc.trustmanager;
 
-import javax.net.ssl.X509TrustManager;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedTrustManager;
 
 import io.quarkus.vertx.http.HttpServerOptionsCustomizer;
+import io.vertx.core.Vertx;
 import io.vertx.core.http.HttpServerOptions;
 import io.vertx.core.net.TrustOptions;
 import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
  * Quarkus CDI bean that customizes the Vert.x HTTP server to use a custom 
TrustManager.
  *
- * This customizer registers {@link HybridPqcX509TrustManager} with the HTTP 
server,
- * enabling TLS-layer validation of hybrid PQC certificates (RSA + ML-DSA-65) 
during
- * the TLS handshake.
- *
- * Uses Quarkus {@link HttpServerOptionsCustomizer} interface to integrate 
with Vert.x.
+ * <p>
+ * Setting trust options replaces whatever the server would otherwise have 
used, so the trust manager
+ * Quarkus built from {@code quarkus.http.ssl.certificate.trust-store-file} is 
taken out of the
+ * handshake. To keep the standard chain and expiry checks, that trust manager 
is retrieved here and
+ * passed to {@link HybridPqcX509TrustManager} as its delegate rather than 
being discarded.
  */
 @ApplicationScoped
 public class HybridPqcTrustManagerCustomizer implements 
HttpServerOptionsCustomizer {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(HybridPqcTrustManagerCustomizer.class);
 
+    @Inject
+    Vertx vertx;
+
     @Override
     public void customizeHttpsServer(HttpServerOptions options) {
         LOG.info("Registering custom hybrid PQC TrustManager for TLS-layer 
validation...");
 
-        // Create custom TrustManager
-        X509TrustManager customTrustManager = new HybridPqcX509TrustManager();
+        X509ExtendedTrustManager platformTrustManager = 
platformTrustManager(options);
+        X509ExtendedTrustManager customTrustManager = new 
HybridPqcX509TrustManager(platformTrustManager);
 
-        // Wrap the X509TrustManager into Vert.x TrustOptions
-        TrustOptions trustOptions = TrustOptions.wrap(customTrustManager);
-
-        // Register with Vert.x HTTP server using setTrustOptions
-        options.setTrustOptions(trustOptions);
+        // Wrap the trust manager into Vert.x TrustOptions and register it 
with the HTTP server
+        options.setTrustOptions(TrustOptions.wrap(customTrustManager));
 
         LOG.info("Custom hybrid PQC TrustManager registered successfully");
-        LOG.info("  Client certificates will be validated at TLS layer (RSA + 
ML-DSA-65)");
+        LOG.info("  Client certificates must chain to a configured trust 
anchor and carry a valid ML-DSA-65 signature");
+    }
+
+    /**
+     * Returns the trust manager Quarkus built from the configured truststore, 
which performs the
+     * standard chain, trust anchor and validity-period checks.
+     *
+     * <p>
+     * An {@link X509ExtendedTrustManager} is required rather than a plain
+     * {@link javax.net.ssl.X509TrustManager}, because only the extended 
interface can be handed the
+     * {@code Socket} or {@code SSLEngine} that endpoint identification needs. 
Every JDK provider has
+     * returned the extended form since Java 7, so failing here means 
something unusual is in play and
+     * is better than quietly wrapping a trust manager that cannot verify 
hostnames.
+     */
+    private X509ExtendedTrustManager platformTrustManager(HttpServerOptions 
options) {
+        TrustOptions trustOptions = options.getTrustOptions();
+        if (trustOptions == null) {
+            // Without a truststore there is nothing to validate certificate 
chains against. Failing
+            // here is deliberate: continuing would leave the PQC check as the 
only barrier, and that
+            // check cannot establish trust on its own.
+            throw new IllegalStateException(
+                    "No truststore configured. Set 
quarkus.http.ssl.certificate.trust-store-file so that "
+                            + "client certificate chains can be validated 
against a trust anchor.");
+        }
+
+        try {
+            TrustManagerFactory trustManagerFactory = 
trustOptions.getTrustManagerFactory(vertx);
+            for (TrustManager trustManager : 
trustManagerFactory.getTrustManagers()) {
+                if (trustManager instanceof X509ExtendedTrustManager) {
+                    return (X509ExtendedTrustManager) trustManager;
+                }
+            }
+        } catch (Exception e) {
+            throw new IllegalStateException("Could not obtain a trust manager 
from the configured truststore", e);
+        }
+
+        throw new IllegalStateException("The configured truststore yielded no 
X509ExtendedTrustManager");
     }
 }
diff --git 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcX509TrustManager.java
 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcX509TrustManager.java
index 7d85ec9e..64f75a76 100644
--- 
a/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcX509TrustManager.java
+++ 
b/http-pqc-j17/src/main/java/org/acme/http/pqc/trustmanager/HybridPqcX509TrustManager.java
@@ -16,62 +16,276 @@
  */
 package org.acme.http.pqc.trustmanager;
 
+import java.net.Socket;
 import java.security.cert.CertificateException;
 import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Objects;
 
-import javax.net.ssl.X509TrustManager;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.X509ExtendedTrustManager;
+import javax.security.auth.x500.X500Principal;
 
 import org.acme.http.pqc.certificates.util.CertificateValidationException;
 import org.acme.http.pqc.certificates.util.CertificatesUtil;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.asn1.x509.AuthorityKeyIdentifier;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.SubjectKeyIdentifier;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Custom X509TrustManager that validates hybrid PQC certificates at the TLS 
layer.
+ * Custom {@link X509ExtendedTrustManager} that adds hybrid PQC validation on 
top of the standard
+ * checks.
  *
- * This TrustManager validates both RSA and ML-DSA-65 signatures during the 
TLS handshake,
- * rejecting connections with invalid or RSA-only certificates before the 
application layer
- * sees the request.
+ * <p>
+ * The important part of this class is what it does <em>not</em> do itself. 
Chain building, trust
+ * anchor lookup and validity-period checking are delegated to the platform 
trust manager that
+ * Quarkus builds from the configured truststore; only once that has passed is 
the ML-DSA-65
+ * alternative signature verified on top. Getting this ordering wrong is the 
classic way to write a
+ * trust manager that accepts anything: a custom check on its own replaces the 
platform checks rather
+ * than adding to them, because the JSSE handshake asks this class and nothing 
else.
+ *
+ * <p>
+ * The effect is that a client certificate must chain to a trust anchor, be 
inside its validity
+ * period, <em>and</em> carry a valid ML-DSA-65 alternative signature made by 
its issuer. A
+ * self-signed certificate carrying well-formed PQC extensions is rejected, 
because no anchor vouches
+ * for it, and a certificate issued by the trusted CA without an alternative 
signature is rejected
+ * too.
+ *
+ * <p>
+ * {@link X509ExtendedTrustManager} is extended rather than {@link 
javax.net.ssl.X509TrustManager}
+ * implemented, so that the {@code Socket} and {@code SSLEngine} overloads are 
delegated as well.
+ * Those carry the connection context the platform needs for endpoint 
identification, which is how
+ * hostname verification happens on the client side. A plain {@code 
X509TrustManager} only gets the
+ * two-argument methods, so a wrapper that implements it silently drops 
hostname verification for any
+ * TLS stack that does not apply the JSSE wrapper that would otherwise 
compensate.
+ *
+ * <p>
+ * Note that this example has no revocation checking (no CRL or OCSP), which a 
production deployment
+ * would need.
  */
-public class HybridPqcX509TrustManager implements X509TrustManager {
+public class HybridPqcX509TrustManager extends X509ExtendedTrustManager {
 
     private static final Logger LOG = 
LoggerFactory.getLogger(HybridPqcX509TrustManager.class);
 
+    private final X509ExtendedTrustManager delegate;
+
+    /**
+     * @param delegate the platform trust manager to perform chain, anchor, 
expiry and endpoint
+     *                 identification checks
+     */
+    public HybridPqcX509TrustManager(X509ExtendedTrustManager delegate) {
+        this.delegate = Objects.requireNonNull(delegate, "delegate trust 
manager is required");
+    }
+
     @Override
     public void checkClientTrusted(X509Certificate[] chain, String authType) 
throws CertificateException {
-        if (chain == null || chain.length == 0) {
-            throw new CertificateException("Client certificate chain is 
empty");
-        }
+        requireChain(chain, "Client");
 
-        X509Certificate clientCert = chain[0];
-        LOG.debug("Validating client certificate at TLS layer: {}", 
clientCert.getSubjectX500Principal());
+        // Standard X.509 validation first: chain, trust anchor, validity 
period
+        delegate.checkClientTrusted(chain, authType);
 
-        try {
-            // Validate hybrid certificate - throws 
CertificateValidationException on failure
-            CertificatesUtil.validateHybridCertificate(clientCert);
-            LOG.debug("Client certificate validated successfully at TLS layer 
(RSA + ML-DSA-65)");
-        } catch (CertificateValidationException e) {
-            LOG.error("Hybrid PQC certificate validation failed: {}", 
e.getMessage());
-            throw new CertificateException("Validation failed: " + 
e.getMessage(), e);
-        }
+        validateHybridChain(chain, "Client");
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType, 
Socket socket)
+            throws CertificateException {
+        requireChain(chain, "Client");
+        delegate.checkClientTrusted(chain, authType, socket);
+        validateHybridChain(chain, "Client");
+    }
+
+    @Override
+    public void checkClientTrusted(X509Certificate[] chain, String authType, 
SSLEngine engine)
+            throws CertificateException {
+        requireChain(chain, "Client");
+        delegate.checkClientTrusted(chain, authType, engine);
+        validateHybridChain(chain, "Client");
     }
 
     @Override
     public void checkServerTrusted(X509Certificate[] chain, String authType) 
throws CertificateException {
-        // Not implemented - this example only validates client certificates
-        // (server-to-client authentication, not client-to-server)
-        //
-        // In mutual TLS where client validates server's hybrid certificate,
-        // this method would implement similar validation logic.
-        LOG.debug("Server certificate validation not implemented (client-auth 
only)");
+        requireChain(chain, "Server");
+
+        // Standard X.509 validation first: chain, trust anchor, validity 
period
+        delegate.checkServerTrusted(chain, authType);
+
+        validateHybridChain(chain, "Server");
+    }
+
+    /**
+     * The {@code Socket} and {@code SSLEngine} overloads are what let the 
delegate perform endpoint
+     * identification, so they must be passed through rather than being 
redirected to the two-argument
+     * method.
+     */
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType, 
Socket socket)
+            throws CertificateException {
+        requireChain(chain, "Server");
+        delegate.checkServerTrusted(chain, authType, socket);
+        validateHybridChain(chain, "Server");
+    }
+
+    @Override
+    public void checkServerTrusted(X509Certificate[] chain, String authType, 
SSLEngine engine)
+            throws CertificateException {
+        requireChain(chain, "Server");
+        delegate.checkServerTrusted(chain, authType, engine);
+        validateHybridChain(chain, "Server");
     }
 
     @Override
     public X509Certificate[] getAcceptedIssuers() {
-        // Return empty array for self-signed certificates in this demo.
-        //
-        // In production with a CA hierarchy, this would return the
-        // list of trusted CA certificates that can issue client certificates.
-        return new X509Certificate[0];
+        // Delegate, so that the certificate authorities advertised during the 
handshake are the ones
+        // in the configured truststore. Returning an empty array here tells 
peers nothing about which
+        // issuers are acceptable.
+        return delegate.getAcceptedIssuers();
+    }
+
+    private static void requireChain(X509Certificate[] chain, String peer) 
throws CertificateException {
+        if (chain == null || chain.length == 0) {
+            throw new CertificateException(peer + " certificate chain is 
empty");
+        }
+    }
+
+    /**
+     * Verifies the ML-DSA-65 alternative signature on every certificate in 
the chain, each against the
+     * ML-DSA-65 public key published by its issuer.
+     */
+    private void validateHybridChain(X509Certificate[] chain, String peer) 
throws CertificateException {
+        for (X509Certificate cert : chain) {
+            LOG.debug("Validating {} certificate hybrid PQC extensions: {}", 
peer, cert.getSubjectX500Principal());
+
+            X509Certificate issuer = findIssuer(cert, chain);
+            if (issuer == null) {
+                throw new CertificateException("Could not find the issuer of " 
+ cert.getSubjectX500Principal()
+                        + ", so its ML-DSA-65 signature cannot be verified");
+            }
+
+            try {
+                CertificatesUtil.validateHybridCertificate(cert, issuer);
+            } catch (CertificateValidationException e) {
+                LOG.error("Hybrid PQC certificate validation failed: {}", 
e.getMessage());
+                throw new CertificateException("Validation failed: " + 
e.getMessage(), e);
+            }
+        }
+
+        LOG.debug("{} certificate chain validated successfully (RSA chain + 
ML-DSA-65)", peer);
+    }
+
+    /**
+     * Finds the certificate that issued {@code cert}, looking in the chain 
the peer presented and then
+     * among the configured trust anchors. Peers may send only their own 
certificate and leave the anchor
+     * to the relying party, or send the anchor along with it, so both need 
checking.
+     *
+     * <p>
+     * Candidates matching on key identifier are preferred to candidates 
matching on the issuer name
+     * alone, because the name is the weaker claim and anything the peer sends 
can carry one. The chain
+     * has already been validated by the delegate at this point, so a 
certificate found in it is one the
+     * platform trust manager accepted as part of a path to an anchor — but 
not necessarily one it built
+     * the path <em>through</em>, since it ignores any certificate that does 
not belong there.
+     */
+    private X509Certificate findIssuer(X509Certificate cert, X509Certificate[] 
chain) {
+        X509Certificate[] anchors = delegate.getAcceptedIssuers();
+
+        // Key identifiers first, so that a candidate which merely shares the 
issuer name cannot
+        // displace the one the platform actually built the path through
+        X509Certificate issuer = findIssuerAmong(cert, chain, anchors, true);
+        if (issuer != null) {
+            return issuer;
+        }
+
+        // A self-issued certificate is its own issuer. Trust anchors carry no 
authority key
+        // identifier to match on, so without this the name fallback below 
would settle for any
+        // same-DN sibling, and a rolled-over CA has one of those in the 
truststore by definition
+        if (isSelfIssued(cert)) {
+            return cert;
+        }
+
+        return findIssuerAmong(cert, chain, anchors, false);
+    }
+
+    private static X509Certificate findIssuerAmong(X509Certificate cert, 
X509Certificate[] chain,
+            X509Certificate[] anchors, boolean requireKeyIdentifier) {
+        for (X509Certificate candidate : chain) {
+            if (candidate != cert && isIssuerOf(candidate, cert, 
requireKeyIdentifier)) {
+                return candidate;
+            }
+        }
+
+        for (X509Certificate anchor : anchors) {
+            if (isIssuerOf(anchor, cert, requireKeyIdentifier)) {
+                return anchor;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Decides whether {@code candidate} is the issuer of {@code cert}. Both 
the authority key
+     * identifier and the issuer name must match when {@code 
requireKeyIdentifier} is set; otherwise the
+     * name alone is enough.
+     *
+     * <p>
+     * Matching on the name alone is not enough on its own. Distinguished 
names are not unique over time
+     * or across a federation: a CA that has rolled its key is present in the 
truststore twice under one
+     * DN with two different ML-DSA-65 keys, and cross-certified CAs share a 
DN by design. Picking the
+     * wrong one makes this class reject a certificate the platform trust 
manager just accepted. Matching
+     * the authority key identifier against the candidate's subject key 
identifier, as the platform does
+     * when it builds the path, keeps the two in step. The name-only pass 
exists for certificates that
+     * predate the extensions, RFC 5280 requiring neither of them on a 
self-signed anchor.
+     */
+    private static boolean isIssuerOf(X509Certificate candidate, 
X509Certificate cert,
+            boolean requireKeyIdentifier) {
+        X500Principal issuerName = cert.getIssuerX500Principal();
+        if (!candidate.getSubjectX500Principal().equals(issuerName)) {
+            return false;
+        }
+
+        if (!requireKeyIdentifier) {
+            return true;
+        }
+
+        try {
+            byte[] authorityKeyId = authorityKeyIdentifier(cert);
+            byte[] subjectKeyId = subjectKeyIdentifier(candidate);
+
+            return authorityKeyId != null && subjectKeyId != null && 
Arrays.equals(authorityKeyId, subjectKeyId);
+        } catch (IllegalArgumentException e) {
+            // Anything a peer sends can be malformed. A key identifier that 
cannot be read is treated
+            // as one that is not there, leaving the issuer name as the only 
thing left to match on
+            LOG.debug("Ignoring unparseable key identifier extension: {}", 
e.getMessage());
+            return false;
+        }
+    }
+
+    private static boolean isSelfIssued(X509Certificate cert) {
+        return 
cert.getSubjectX500Principal().equals(cert.getIssuerX500Principal());
+    }
+
+    private static byte[] authorityKeyIdentifier(X509Certificate cert) {
+        byte[] encoded = 
cert.getExtensionValue(Extension.authorityKeyIdentifier.getId());
+        return encoded == null
+                ? null
+                : 
AuthorityKeyIdentifier.getInstance(unwrap(encoded)).getKeyIdentifier();
+    }
+
+    private static byte[] subjectKeyIdentifier(X509Certificate cert) {
+        byte[] encoded = 
cert.getExtensionValue(Extension.subjectKeyIdentifier.getId());
+        return encoded == null
+                ? null
+                : 
SubjectKeyIdentifier.getInstance(unwrap(encoded)).getKeyIdentifier();
+    }
+
+    /**
+     * {@link X509Certificate#getExtensionValue(String)} returns the extension 
wrapped in a DER octet
+     * string, which has to be unwrapped before the value inside can be parsed.
+     */
+    private static byte[] unwrap(byte[] encoded) {
+        return ASN1OctetString.getInstance(encoded).getOctets();
     }
 }
diff --git a/http-pqc-j17/src/main/resources/application.properties 
b/http-pqc-j17/src/main/resources/application.properties
index 90b2e0cd..cf7f5efd 100644
--- a/http-pqc-j17/src/main/resources/application.properties
+++ b/http-pqc-j17/src/main/resources/application.properties
@@ -33,7 +33,9 @@ quarkus.http.ssl.certificate.key-store-file-type = PKCS12
 # Certificates must be valid hybrid PQC certificates (RSA + ML-DSA-65)
 quarkus.http.ssl.client-auth = required
 
-# Truststore for client certificate validation (standard TLS layer)
+# Trust anchors for client certificate validation. 
HybridPqcTrustManagerCustomizer passes the trust
+# manager built from this truststore to the custom TrustManager as its 
delegate, so these anchors are
+# what client certificate chains are validated against.
 quarkus.http.ssl.certificate.trust-store-file = 
target/certs/server-hybrid-truststore.p12
 quarkus.http.ssl.certificate.trust-store-password = changeit
 quarkus.http.ssl.certificate.trust-store-file-type = PKCS12
diff --git 
a/http-pqc-j17/src/test/java/org/acme/http/pqc/CertificatesUtilTest.java 
b/http-pqc-j17/src/test/java/org/acme/http/pqc/CertificatesUtilTest.java
new file mode 100644
index 00000000..c617dbe9
--- /dev/null
+++ b/http-pqc-j17/src/test/java/org/acme/http/pqc/CertificatesUtilTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.acme.http.pqc;
+
+import java.security.Security;
+import java.security.cert.X509Certificate;
+
+import org.acme.http.pqc.certificates.HybridCertificateGenerator;
+import 
org.acme.http.pqc.certificates.HybridCertificateGenerator.CertificateData;
+import org.acme.http.pqc.certificates.util.CertificateValidationException;
+import org.acme.http.pqc.certificates.util.CertificatesUtil;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests the ML-DSA-65 half of the validation in isolation.
+ *
+ * <p>
+ * Some of these cases cannot be reached through the TLS handshake: the 
standard chain validation that
+ * {@code HybridPqcX509TrustManager} runs first rejects an untrusted 
certificate before the PQC check
+ * is consulted, so an end-to-end test would pass even if this check were 
broken.
+ */
+public class CertificatesUtilTest {
+
+    private static CertificateData ca;
+    private static X509Certificate leaf;
+
+    @BeforeAll
+    public static void setUpClass() throws Exception {
+        if (Security.getProvider("BC") == null) {
+            Security.insertProviderAt(new BouncyCastleProvider(), 1);
+        }
+
+        ca = HybridCertificateGenerator.generateCertificateAuthority();
+        leaf = HybridCertificateGenerator.issueCertificate(ca, 
"unit-test-leaf", true, null).certificate;
+    }
+
+    @Test
+    public void certificateIssuedByTheCaIsAccepted() {
+        assertDoesNotThrow(() -> 
CertificatesUtil.validateHybridCertificate(leaf, ca.certificate));
+    }
+
+    @Test
+    public void selfSignedCaIsAcceptedAgainstItself() {
+        assertDoesNotThrow(() -> 
CertificatesUtil.validateHybridCertificate(ca.certificate, ca.certificate));
+    }
+
+    @Test
+    public void certificateWithoutAltSignatureIsRejected() throws Exception {
+        X509Certificate rsaOnly = HybridCertificateGenerator
+                .issueCertificate(ca, "unit-test-rsa-only", false, 
null).certificate;
+
+        CertificateValidationException e = 
assertThrows(CertificateValidationException.class,
+                () -> CertificatesUtil.validateHybridCertificate(rsaOnly, 
ca.certificate));
+        assertTrue(e.getMessage().contains("2.5.29.73"), "Unexpected message: 
" + e.getMessage());
+    }
+
+    /**
+     * The signature must be verified with the issuer's key, so a certificate 
issued by one CA must not
+     * validate against another.
+     */
+    @Test
+    public void certificateFromAnotherCaIsRejected() throws Exception {
+        CertificateData otherCa = 
HybridCertificateGenerator.generateCertificateAuthority();
+
+        CertificateValidationException e = 
assertThrows(CertificateValidationException.class,
+                () -> CertificatesUtil.validateHybridCertificate(leaf, 
otherCa.certificate));
+        assertEquals("ML-DSA-65 signature validation failed", e.getMessage());
+    }
+
+    /**
+     * The alternative signature must cover the certificate body, so copying 
the PQC extensions onto a
+     * different certificate must not produce one that validates. A signature 
computed over only the
+     * subject DN would pass this, because the DN is copied along with the 
extensions.
+     */
+    @Test
+    public void certificateWithLiftedExtensionsIsRejected() throws Exception {
+        X509Certificate forged = 
ForgedCertificates.withLiftedExtensions(leaf).certificate;
+
+        assertEquals(leaf.getSubjectX500Principal(), 
forged.getSubjectX500Principal(),
+                "The forgery should reuse the victim's subject DN");
+
+        CertificateValidationException e = 
assertThrows(CertificateValidationException.class,
+                () -> CertificatesUtil.validateHybridCertificate(forged, 
ca.certificate));
+        assertEquals("ML-DSA-65 signature validation failed", e.getMessage());
+    }
+
+    /**
+     * Without the issuer's published ML-DSA-65 key there is nothing to verify 
against, so validation
+     * must fail rather than skip the check.
+     */
+    @Test
+    public void issuerWithoutPublishedAltKeyIsRejected() throws Exception {
+        X509Certificate issuerWithoutAltKey = HybridCertificateGenerator
+                .issueCertificate(ca, "unit-test-no-alt-key", true, 
null).certificate;
+
+        CertificateValidationException e = 
assertThrows(CertificateValidationException.class,
+                () -> CertificatesUtil.validateHybridCertificate(leaf, 
issuerWithoutAltKey));
+        assertTrue(e.getMessage().contains("2.5.29.72"), "Unexpected message: 
" + e.getMessage());
+    }
+}
diff --git 
a/http-pqc-j17/src/test/java/org/acme/http/pqc/ForgedCertificates.java 
b/http-pqc-j17/src/test/java/org/acme/http/pqc/ForgedCertificates.java
new file mode 100644
index 00000000..a74feb43
--- /dev/null
+++ b/http-pqc-j17/src/test/java/org/acme/http/pqc/ForgedCertificates.java
@@ -0,0 +1,169 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.acme.http.pqc;
+
+import java.io.FileOutputStream;
+import java.math.BigInteger;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+
+import org.acme.http.pqc.crypto.ChimeraOids;
+import org.bouncycastle.asn1.ASN1ObjectIdentifier;
+import org.bouncycastle.asn1.ASN1OctetString;
+import org.bouncycastle.asn1.ASN1Primitive;
+import org.bouncycastle.asn1.DEROctetString;
+import org.bouncycastle.asn1.x500.X500Name;
+import org.bouncycastle.asn1.x509.BasicConstraints;
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo;
+import org.bouncycastle.cert.X509CertificateHolder;
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.ContentSigner;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+
+/**
+ * Builds certificates that a reader might expect to be accepted but that must 
be rejected.
+ * Used by {@link HttpPqcTest} and {@link CertificatesUtilTest}.
+ */
+public final class ForgedCertificates {
+
+    private static final String PASSWORD = "changeit";
+    private static final String CERT_DIR = "target/certs";
+
+    private ForgedCertificates() {
+    }
+
+    /**
+     * A certificate and the private key needed to present it during a TLS 
handshake.
+     */
+    public static class Forged {
+        public final KeyPair rsaKeyPair;
+        public final X509Certificate certificate;
+
+        Forged(KeyPair rsaKeyPair, X509Certificate certificate) {
+            this.rsaKeyPair = rsaKeyPair;
+            this.certificate = certificate;
+        }
+    }
+
+    /**
+     * A self-signed hybrid certificate with a correct, freshly generated 
ML-DSA-65 alternative
+     * signature. Everything about it is well formed; it simply is not in the 
server truststore.
+     */
+    public static Forged rogueHybrid() throws Exception {
+        return forge(new X500Name("CN=attacker,O=Evil Corp,C=XX"), 0, null);
+    }
+
+    /**
+     * A hybrid certificate whose validity period ended a year ago.
+     */
+    public static Forged expiredHybrid() throws Exception {
+        return forge(new X500Name("CN=expired,O=Evil Corp,C=XX"), -730, null);
+    }
+
+    /**
+     * A certificate built on a freshly generated RSA key, carrying the PQC 
extensions copied verbatim
+     * off {@code victim} along with its subject DN. This is the forgery that 
succeeds when the
+     * alternative signature covers only the subject DN instead of the 
certificate body: the forger
+     * never holds the ML-DSA-65 private key that produced the copied 
signature.
+     *
+     * <p>
+     * The result is self-signed, so at TLS level it is rejected for lacking a 
trust anchor. Use it
+     * against {@code CertificatesUtil} directly to exercise the signature 
check itself.
+     */
+    public static Forged withLiftedExtensions(X509Certificate victim) throws 
Exception {
+        // Rebuild the DN from its DER encoding so the bytes are identical to 
the victim's
+        X500Name victimDn = 
X500Name.getInstance(victim.getSubjectX500Principal().getEncoded());
+        return forge(victimDn, 0, victim);
+    }
+
+    /**
+     * Writes a certificate and its key to a PKCS12 keystore that a client can 
present.
+     */
+    public static Path keystore(String alias, Forged forged) throws Exception {
+        Path path = Paths.get(CERT_DIR, alias + "-keystore.p12");
+        KeyStore keyStore = KeyStore.getInstance("PKCS12");
+        keyStore.load(null, null);
+        keyStore.setKeyEntry(alias, forged.rsaKeyPair.getPrivate(), 
PASSWORD.toCharArray(),
+                new X509Certificate[] { forged.certificate });
+        try (FileOutputStream fos = new FileOutputStream(path.toFile())) {
+            keyStore.store(fos, PASSWORD.toCharArray());
+        }
+        return path;
+    }
+
+    private static Forged forge(X500Name name, int dayOffset, X509Certificate 
liftFrom) throws Exception {
+        KeyPairGenerator rsaKpg = KeyPairGenerator.getInstance("RSA");
+        rsaKpg.initialize(2048, new SecureRandom());
+        KeyPair rsa = rsaKpg.generateKeyPair();
+
+        long day = 24L * 60 * 60 * 1000;
+        long start = System.currentTimeMillis() + dayOffset * day;
+
+        JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
+                name,
+                new BigInteger(64, new SecureRandom()),
+                new Date(start),
+                new Date(start + 365 * day),
+                name,
+                rsa.getPublic());
+        builder.addExtension(Extension.basicConstraints, true, new 
BasicConstraints(false));
+
+        ContentSigner rsaSigner = new 
JcaContentSignerBuilder("SHA256withRSA").build(rsa.getPrivate());
+        X509CertificateHolder holder;
+
+        if (liftFrom != null) {
+            // A certificate issued by a CA carries no subjectAltPublicKeyInfo 
of its own, so only copy
+            // the extensions that are actually present
+            copyExtensionIfPresent(builder, liftFrom, 
ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO);
+            copyExtensionIfPresent(builder, liftFrom, 
ChimeraOids.ALT_SIGNATURE_ALGORITHM);
+            copyExtensionIfPresent(builder, liftFrom, 
ChimeraOids.ALT_SIGNATURE_VALUE);
+            holder = builder.build(rsaSigner);
+        } else {
+            KeyPair mlDsa = KeyPairGenerator.getInstance("ML-DSA-65", 
"BC").generateKeyPair();
+            builder.addExtension(ChimeraOids.SUBJECT_ALT_PUBLIC_KEY_INFO, 
false,
+                    
SubjectPublicKeyInfo.getInstance(mlDsa.getPublic().getEncoded()));
+            ContentSigner mlDsaSigner = new 
JcaContentSignerBuilder("ML-DSA-65")
+                    .setProvider("BC")
+                    .build(mlDsa.getPrivate());
+            holder = builder.build(rsaSigner, false, mlDsaSigner);
+        }
+
+        X509Certificate certificate = new JcaX509CertificateConverter()
+                .setProvider("BC")
+                .getCertificate(holder);
+
+        return new Forged(rsa, certificate);
+    }
+
+    private static void copyExtensionIfPresent(JcaX509v3CertificateBuilder 
builder, X509Certificate from,
+            ASN1ObjectIdentifier oid) throws Exception {
+        byte[] wrapped = from.getExtensionValue(oid.getId());
+        if (wrapped == null) {
+            return;
+        }
+        byte[] inner = ((ASN1OctetString) 
ASN1Primitive.fromByteArray(wrapped)).getOctets();
+        builder.addExtension(new Extension(oid, false, new 
DEROctetString(inner)));
+    }
+}
diff --git a/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcTest.java 
b/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcTest.java
index 39387ef6..35c60127 100644
--- a/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcTest.java
+++ b/http-pqc-j17/src/test/java/org/acme/http/pqc/HttpPqcTest.java
@@ -16,10 +16,16 @@
  */
 package org.acme.http.pqc;
 
+import java.io.IOException;
+import java.nio.file.Path;
+import java.security.Security;
+
 import io.quarkus.test.junit.QuarkusTest;
 import io.restassured.RestAssured;
 import io.restassured.config.RestAssuredConfig;
 import io.restassured.config.SSLConfig;
+import io.restassured.specification.RequestSpecification;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.eclipse.microprofile.config.ConfigProvider;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
@@ -28,17 +34,22 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import static org.hamcrest.Matchers.containsString;
-import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 @QuarkusTest
 public class HttpPqcTest {
 
     private static final Logger log = 
LoggerFactory.getLogger(HttpPqcTest.class);
+    private static final String PASSWORD = "changeit";
+    private static final String CERT_DIR = "target/certs";
 
     @BeforeAll
     public static void setUpClass() {
-        // Use relaxed HTTPS validation for self-signed certificates in tests
-        RestAssured.useRelaxedHTTPSValidation();
+        // Needed to build the deliberately invalid certificates used by the 
rejection tests
+        if (Security.getProvider("BC") == null) {
+            Security.insertProviderAt(new BouncyCastleProvider(), 1);
+        }
     }
 
     @BeforeEach
@@ -54,62 +65,87 @@ public class HttpPqcTest {
     @Test
     public void testPqcSecureEndpointWithoutClientCert() {
         // With client-auth=required, TLS handshake fails before reaching the 
route
-        try {
-            RestAssured.given()
-                    .when()
-                    .get("/pqc/secure")
-                    .then()
-                    .statusCode(200);
-
-            fail("Expected SSL exception but connection succeeded");
-        } catch (Exception e) {
-            // Expected: SSL handshake failure (certificate_required)
-            log.info("✓ Expected SSL exception without client cert: {}", 
e.getMessage());
-        }
+        assertRejected(withTrustStore(SSLConfig.sslConfig()), "no client 
certificate");
     }
 
     @Test
     public void testPqcSecureEndpointWithValidClientCert() {
-        // Test /pqc/secure WITH valid hybrid client certificate
-        // TLS handshake should succeed, and custom TrustManager validates 
both RSA + ML-DSA-65
-        // Configure RestAssured with client certificate keystore and server 
truststore
-
-        RestAssured.given()
-                .config(RestAssuredConfig.config().sslConfig(
-                        SSLConfig.sslConfig()
-                                
.keyStore("target/certs/client-hybrid-keystore.p12", "changeit")
-                                
.trustStore("target/certs/server-hybrid-truststore.p12", "changeit")
-                                .allowAllHostnames() // Accept localhost with 
self-signed cert
-                ))
+        // Test /pqc/secure WITH valid hybrid client certificate. It chains to 
the CA in the server
+        // truststore and carries the CA's ML-DSA-65 alternative signature, so 
the handshake succeeds.
+        client(CERT_DIR + "/client-hybrid-keystore.p12")
                 .when()
                 .get("/pqc/secure")
                 .then()
                 .statusCode(200)
                 .body(containsString("Hybrid PQC certificate validated"))
-                .body(containsString("quantum-safe"))
-                .body(containsString("TLS layer"));
+                .body(containsString("trust anchor"))
+                .body(containsString("ML-DSA-65"));
     }
 
     @Test
     public void testPqcSecureEndpointWithRsaOnlyCertificate() {
-        // Test /pqc/secure WITH RSA-only client certificate (no PQC 
extensions)
-        // TLS handshake should FAIL because custom TrustManager requires 
hybrid cert
-        try {
-            RestAssured.given()
-                    .config(RestAssuredConfig.config().sslConfig(
-                            SSLConfig.sslConfig()
-                                    
.keyStore("target/certs/client-rsa-only-keystore.p12", "changeit")
-                                    
.trustStore("target/certs/server-hybrid-truststore.p12", "changeit")
-                                    .allowAllHostnames()))
-                    .when()
-                    .get("/pqc/secure")
-                    .then()
-                    .statusCode(200); // Should NOT reach here
-
-            fail("Expected SSL exception for RSA-only certificate, but 
connection succeeded");
-        } catch (Exception e) {
-            // Expected: TLS handshake failure due to missing PQC extensions
-            log.info("✓ Expected SSL exception for RSA-only cert: {}", 
e.getMessage());
+        // Test /pqc/secure WITH RSA-only client certificate (no PQC 
extensions). It is issued by the
+        // same CA, so it passes chain validation and the ML-DSA-65 check is 
the only thing rejecting it.
+        assertRejected(CERT_DIR + "/client-rsa-only-keystore.p12", "RSA-only 
cert issued by the CA");
+    }
+
+    @Test
+    public void testPqcSecureEndpointWithUntrustedHybridCertificate() throws 
Exception {
+        // A self-signed hybrid certificate carrying a well-formed ML-DSA-65 
alternative signature over
+        // its own body, but issued by nobody the server trusts. Rejected 
because the PQC check adds to
+        // the standard chain validation rather than replacing it. This is the 
case that a custom
+        // TrustManager doing its own validation instead of delegating will 
wrongly accept.
+        Path keystore = ForgedCertificates.keystore("rogue", 
ForgedCertificates.rogueHybrid());
+        assertRejected(keystore.toString(), "untrusted self-signed hybrid 
cert");
+    }
+
+    @Test
+    public void testPqcSecureEndpointWithExpiredHybridCertificate() throws 
Exception {
+        // A hybrid certificate whose validity period ended a year ago, which 
the validity check picks
+        // up. It is self-signed too, so it fails for want of a trust anchor 
as well; both checks only
+        // run because validation is delegated to the platform trust manager.
+        Path keystore = ForgedCertificates.keystore("expired", 
ForgedCertificates.expiredHybrid());
+        assertRejected(keystore.toString(), "expired hybrid cert");
+    }
+
+    private void assertRejected(String keyStorePath, String description) {
+        
assertRejected(withTrustStore(SSLConfig.sslConfig()).keyStore(keyStorePath, 
PASSWORD), description);
+    }
+
+    private void assertRejected(SSLConfig sslConfig, String description) {
+        Exception e = assertThrows(Exception.class,
+                () -> 
given(sslConfig).when().get("/pqc/secure").then().statusCode(200),
+                "Expected the TLS handshake to be rejected for " + 
description);
+
+        // TLS-level rejections surface as an IOException subclass, most often 
SSLHandshakeException.
+        // Asserting that keeps the test from passing because of an unrelated 
failure.
+        assertInstanceOf(IOException.class, rootCause(e),
+                "Expected a TLS failure for " + description + " but got: " + 
e);
+        log.info("✓ Rejected as expected ({}): {}", description, 
e.getMessage());
+    }
+
+    /**
+     * Trusts the server via the generated client truststore, and verifies its 
identity against the
+     * subject alternative names in the server certificate. Hostname 
verification is deliberately left
+     * enabled so that the tests fail if those names regress.
+     */
+    private static SSLConfig withTrustStore(SSLConfig sslConfig) {
+        return sslConfig.trustStore(CERT_DIR + 
"/client-hybrid-truststore.p12", PASSWORD);
+    }
+
+    private static RequestSpecification client(String keyStorePath) {
+        return 
given(withTrustStore(SSLConfig.sslConfig()).keyStore(keyStorePath, PASSWORD));
+    }
+
+    private static RequestSpecification given(SSLConfig sslConfig) {
+        return 
RestAssured.given().config(RestAssuredConfig.config().sslConfig(sslConfig));
+    }
+
+    private static Throwable rootCause(Throwable t) {
+        Throwable cause = t;
+        while (cause.getCause() != null && cause.getCause() != cause) {
+            cause = cause.getCause();
         }
+        return cause;
     }
 }
diff --git 
a/http-pqc-j17/src/test/java/org/acme/http/pqc/HybridPqcX509TrustManagerTest.java
 
b/http-pqc-j17/src/test/java/org/acme/http/pqc/HybridPqcX509TrustManagerTest.java
new file mode 100644
index 00000000..251f71f7
--- /dev/null
+++ 
b/http-pqc-j17/src/test/java/org/acme/http/pqc/HybridPqcX509TrustManagerTest.java
@@ -0,0 +1,269 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.acme.http.pqc;
+
+import java.net.Socket;
+import java.security.KeyStore;
+import java.security.Security;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import javax.net.ssl.X509ExtendedTrustManager;
+
+import org.acme.http.pqc.certificates.HybridCertificateGenerator;
+import 
org.acme.http.pqc.certificates.HybridCertificateGenerator.CertificateData;
+import org.acme.http.pqc.trustmanager.HybridPqcX509TrustManager;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Verifies that {@link HybridPqcX509TrustManager} implements the whole
+ * {@link X509ExtendedTrustManager} contract rather than leaving parts of it 
permissive. An empty
+ * {@code checkServerTrusted} or an empty {@code getAcceptedIssuers} compiles 
and passes any
+ * happy-path test, so each is asserted here explicitly.
+ */
+public class HybridPqcX509TrustManagerTest {
+
+    private static CertificateData ca;
+    private static X509Certificate leaf;
+    private static HybridPqcX509TrustManager trustManager;
+
+    @BeforeAll
+    public static void setUpClass() throws Exception {
+        if (Security.getProvider("BC") == null) {
+            Security.insertProviderAt(new BouncyCastleProvider(), 1);
+        }
+
+        ca = HybridCertificateGenerator.generateCertificateAuthority();
+        leaf = HybridCertificateGenerator.issueCertificate(ca, "trusted-leaf", 
true, null).certificate;
+        trustManager = trustManagerAnchoredOn(ca.certificate);
+    }
+
+    /**
+     * Builds the trust manager under test on top of a platform one seeded 
with the given trust anchors,
+     * which is the arrangement {@code HybridPqcTrustManagerCustomizer} sets 
up at runtime.
+     */
+    private static HybridPqcX509TrustManager 
trustManagerAnchoredOn(X509Certificate... anchors) throws Exception {
+        KeyStore trustStore = KeyStore.getInstance("PKCS12");
+        trustStore.load(null, null);
+        for (int i = 0; i < anchors.length; i++) {
+            trustStore.setCertificateEntry("anchor-" + i, anchors[i]);
+        }
+
+        TrustManagerFactory tmf = 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+        tmf.init(trustStore);
+
+        for (TrustManager candidate : tmf.getTrustManagers()) {
+            if (candidate instanceof X509ExtendedTrustManager) {
+                return new 
HybridPqcX509TrustManager((X509ExtendedTrustManager) candidate);
+            }
+        }
+        throw new IllegalStateException("No X509ExtendedTrustManager 
available");
+    }
+
+    /**
+     * A peer that sends only its own certificate, leaving the relying party 
to supply the anchor. The
+     * issuer's ML-DSA-65 key has to be found in the truststore for this to 
pass.
+     */
+    @Test
+    public void leafAloneIsAccepted() {
+        assertDoesNotThrow(() -> trustManager.checkClientTrusted(new 
X509Certificate[] { leaf }, "RSA"));
+    }
+
+    @Test
+    public void leafWithCaInChainIsAccepted() {
+        assertDoesNotThrow(
+                () -> trustManager.checkClientTrusted(new X509Certificate[] { 
leaf, ca.certificate }, "RSA"));
+    }
+
+    @Test
+    public void untrustedHybridClientIsRejected() throws Exception {
+        X509Certificate rogue = ForgedCertificates.rogueHybrid().certificate;
+        assertThrows(CertificateException.class,
+                () -> trustManager.checkClientTrusted(new X509Certificate[] { 
rogue }, "RSA"));
+    }
+
+    /**
+     * Issued by the trusted CA, so it passes chain validation, but carries no 
ML-DSA-65 signature. This
+     * isolates the post-quantum check as the sole reason for rejection.
+     */
+    @Test
+    public void certificateFromTheCaWithoutAltSignatureIsRejected() throws 
Exception {
+        X509Certificate rsaOnly = HybridCertificateGenerator
+                .issueCertificate(ca, "rsa-only-leaf", false, 
null).certificate;
+
+        CertificateException e = assertThrows(CertificateException.class,
+                () -> trustManager.checkClientTrusted(new X509Certificate[] { 
rsaOnly }, "RSA"));
+        assertEquals(true, e.getMessage().contains("2.5.29.73"), "Unexpected 
message: " + e.getMessage());
+    }
+
+    @Test
+    public void untrustedHybridServerIsRejected() throws Exception {
+        X509Certificate rogue = ForgedCertificates.rogueHybrid().certificate;
+        assertThrows(CertificateException.class,
+                () -> trustManager.checkServerTrusted(new X509Certificate[] { 
rogue }, "RSA"),
+                "checkServerTrusted must validate the chain, not accept 
anything");
+    }
+
+    @Test
+    public void emptyChainIsRejected() {
+        assertThrows(CertificateException.class, () -> 
trustManager.checkClientTrusted(null, "RSA"));
+        assertThrows(CertificateException.class,
+                () -> trustManager.checkClientTrusted(new X509Certificate[0], 
"RSA"));
+    }
+
+    /**
+     * The {@code Socket} and {@code SSLEngine} overloads are the ones a TLS 
stack actually calls, so
+     * they have to run the same two checks as the two-argument methods.
+     */
+    @Test
+    public void extendedOverloadsValidateToo() throws Exception {
+        X509Certificate rogue = ForgedCertificates.rogueHybrid().certificate;
+
+        assertDoesNotThrow(() -> trustManager.checkClientTrusted(new 
X509Certificate[] { leaf }, "RSA", (Socket) null));
+        assertDoesNotThrow(
+                () -> trustManager.checkClientTrusted(new X509Certificate[] { 
leaf }, "RSA", (SSLEngine) null));
+
+        assertThrows(CertificateException.class,
+                () -> trustManager.checkClientTrusted(new X509Certificate[] { 
rogue }, "RSA", (Socket) null));
+        assertThrows(CertificateException.class,
+                () -> trustManager.checkServerTrusted(new X509Certificate[] { 
rogue }, "RSA", (SSLEngine) null));
+    }
+
+    /**
+     * The connection context has to reach the delegate, because that is what 
the platform uses for
+     * endpoint identification. Forwarding the three-argument overloads to the 
two-argument method
+     * validates the certificate just as well and would satisfy the test 
above, while dropping the
+     * hostname check on the way through.
+     */
+    @Test
+    public void connectionContextIsHandedToTheDelegate() throws Exception {
+        RecordingTrustManager recorder = new 
RecordingTrustManager(ca.certificate);
+        HybridPqcX509TrustManager wrapper = new 
HybridPqcX509TrustManager(recorder);
+        X509Certificate[] chain = new X509Certificate[] { leaf };
+
+        wrapper.checkServerTrusted(chain, "RSA", (SSLEngine) null);
+        assertEquals("checkServerTrusted/SSLEngine", recorder.lastCall);
+
+        wrapper.checkServerTrusted(chain, "RSA", (Socket) null);
+        assertEquals("checkServerTrusted/Socket", recorder.lastCall);
+
+        wrapper.checkClientTrusted(chain, "RSA", (SSLEngine) null);
+        assertEquals("checkClientTrusted/SSLEngine", recorder.lastCall);
+
+        wrapper.checkClientTrusted(chain, "RSA", (Socket) null);
+        assertEquals("checkClientTrusted/Socket", recorder.lastCall);
+    }
+
+    /**
+     * Two CAs sharing a distinguished name, which is what a key rollover 
looks like to a relying party
+     * holding both. Resolving the issuer by DN alone picks whichever anchor 
happens to come first, so
+     * one of these two certificates would fail its ML-DSA-65 check despite 
the platform trust manager
+     * having just accepted its chain.
+     *
+     * <p>
+     * Both chain shapes are exercised. The CA is the interesting one: it 
carries no authority key
+     * identifier of its own, being self-signed, so matching it needs more 
than the key identifiers the
+     * leaf supplies — and the keystores this example generates do put the CA 
in the chain.
+     */
+    @Test
+    public void certificatesFromTwoCasSharingADnAreBothAccepted() throws 
Exception {
+        CertificateData firstCa = 
HybridCertificateGenerator.generateCertificateAuthority();
+        CertificateData secondCa = 
HybridCertificateGenerator.generateCertificateAuthority();
+        assertEquals(firstCa.certificate.getSubjectX500Principal(), 
secondCa.certificate.getSubjectX500Principal(),
+                "The two CAs are only interesting to this test if they share a 
DN");
+
+        X509Certificate firstLeaf = HybridCertificateGenerator
+                .issueCertificate(firstCa, "rollover-leaf-1", true, 
null).certificate;
+        X509Certificate secondLeaf = HybridCertificateGenerator
+                .issueCertificate(secondCa, "rollover-leaf-2", true, 
null).certificate;
+
+        HybridPqcX509TrustManager bothAnchors = 
trustManagerAnchoredOn(firstCa.certificate, secondCa.certificate);
+
+        assertDoesNotThrow(() -> bothAnchors.checkClientTrusted(new 
X509Certificate[] { firstLeaf }, "RSA"));
+        assertDoesNotThrow(() -> bothAnchors.checkClientTrusted(new 
X509Certificate[] { secondLeaf }, "RSA"));
+
+        assertDoesNotThrow(() -> bothAnchors
+                .checkClientTrusted(new X509Certificate[] { firstLeaf, 
firstCa.certificate }, "RSA"));
+        assertDoesNotThrow(() -> bothAnchors
+                .checkClientTrusted(new X509Certificate[] { secondLeaf, 
secondCa.certificate }, "RSA"));
+    }
+
+    @Test
+    public void acceptedIssuersComeFromTheTruststore() {
+        X509Certificate[] issuers = trustManager.getAcceptedIssuers();
+        assertEquals(1, issuers.length, "Expected the configured trust anchor 
to be advertised");
+        assertEquals(ca.certificate, issuers[0]);
+    }
+
+    /**
+     * A delegate that accepts everything and records which of the six methods 
it was asked, so that the
+     * overload the wrapper chose can be asserted.
+     */
+    private static final class RecordingTrustManager extends 
X509ExtendedTrustManager {
+
+        private final X509Certificate anchor;
+        private String lastCall;
+
+        private RecordingTrustManager(X509Certificate anchor) {
+            this.anchor = anchor;
+        }
+
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String 
authType) {
+            lastCall = "checkClientTrusted/2";
+        }
+
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String 
authType, Socket socket) {
+            lastCall = "checkClientTrusted/Socket";
+        }
+
+        @Override
+        public void checkClientTrusted(X509Certificate[] chain, String 
authType, SSLEngine engine) {
+            lastCall = "checkClientTrusted/SSLEngine";
+        }
+
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String 
authType) {
+            lastCall = "checkServerTrusted/2";
+        }
+
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String 
authType, Socket socket) {
+            lastCall = "checkServerTrusted/Socket";
+        }
+
+        @Override
+        public void checkServerTrusted(X509Certificate[] chain, String 
authType, SSLEngine engine) {
+            lastCall = "checkServerTrusted/SSLEngine";
+        }
+
+        @Override
+        public X509Certificate[] getAcceptedIssuers() {
+            return new X509Certificate[] { anchor };
+        }
+    }
+}

Reply via email to