Skip to content

checkTlsFeatures rejects every status_request must-staple leaf over TLS 1.3 #2458

Description

@Arpan0995

TlsUtils.checkTlsFeatures (TlsUtils.java:4838) requires a feature named in the leaf's id-pe-tlsfeature extension, and offered by the client itself, to be a key of serverExtensions too (:4874), throwing certificate_unknown otherwise (:4876-4877). processServerCertificate (:4925) calls it at :4951, ahead of the isTLSv13 branch at :4956. On the TLS 1.3 path serverExtensions is the EncryptedExtensions table (TlsClientProtocol.java:1543), which status_request can never be a key of: RFC 8446 sec. 4.2 lists that extension for ClientHello, CertificateRequest and Certificate only, and sec. 4.4.2.1 puts the answer in an extension of the CertificateEntry instead (paraphrasing both). BC enforces the same table (TlsUtils.java:6172-6184, TlsProtocol.java:2119-2123).

Only the status_request arm is affected; a feature that is permitted in EncryptedExtensions, server_name or ALPN say, is still answerable there (:6154-6171). But a TLS 1.3 client that offered status_request refuses every leaf whose feature list names it, correctly stapled or not, and BC's own 1.3 server cannot satisfy it: getServerExtensions adds no echo on that path and says why (AbstractTlsServer.java:561-564).

Lines are origin/main ab16374d37; these classes are byte identical at r1rv86 and in the 1.87-SNAPSHOT beta sources. It is the stock path: AbstractTlsClient.getCertificateStatusRequest (:125) supplies a request unconditionally and getClientExtensions adds it (:290-293). Through the provider too: a BCJSSE client on defaults fails the same way over TLS 1.3, and with jdk.tls.client.enableStatusRequestExtension=false (ProvTlsClient.java:63-64) it completes with no staple.

Full harness
import java.io.*; import java.math.BigInteger; import java.util.Date;
import java.security.KeyPair; import java.security.KeyPairGenerator;
import org.bouncycastle.asn1.*; import org.bouncycastle.asn1.ocsp.*;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.crypto.params.*; import org.bouncycastle.crypto.util.*;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.tls.*; import org.bouncycastle.tls.crypto.TlsCryptoParameters;
import org.bouncycastle.tls.crypto.impl.bc.*;

public class MustStaple13 {
  static AsymmetricKeyParameter k; static byte[] leaf;
  public static void main(String[] a) throws Exception {
    KeyPair kp = KeyPairGenerator.getInstance("RSA").generateKeyPair();
    k = PrivateKeyFactory.createKey(kp.getPrivate().getEncoded());
    X500Name n = new X500Name("CN=t"); long t = System.currentTimeMillis();
    JcaX509v3CertificateBuilder b = new JcaX509v3CertificateBuilder(n, BigInteger.ONE,
      new Date(t - 60000L), new Date(t + 86400000L), n, kp.getPublic());
    // id-pe-tlsfeature = SEQUENCE { INTEGER 5 }; drop the next line for the control run
    b.addExtension(new ASN1ObjectIdentifier("1.3.6.1.5.5.7.1.24"), false, new DERSequence(new ASN1Integer(5)));
    leaf = b.build(new JcaContentSignerBuilder("SHA256withRSA").build(kp.getPrivate())).getEncoded();
    PipedInputStream cr = new PipedInputStream(16384), sr = new PipedInputStream(16384);
    TlsClientProtocol cp = new TlsClientProtocol(cr, new PipedOutputStream(sr));
    final TlsServerProtocol sp = new TlsServerProtocol(sr, new PipedOutputStream(cr));
    Thread s = new Thread() { public void run() { try { sp.accept(new Srv()); }
      catch (Throwable e) { System.out.println("server: " + e); } } };
    s.setDaemon(true); s.start();
    try { cp.connect(new Cli()); }
    catch (Throwable x) { System.out.println(x.getClass().getName() + ": " + x.getMessage());
      StackTraceElement[] f = x.getStackTrace();
      for (int i = 0; i < Math.min(4, f.length); ++i) System.out.println("   at " + f[i]); }
  }
  static class Cli extends DefaultTlsClient { Cli() { super(new BcTlsCrypto()); }
    public TlsAuthentication getAuthentication() { return new TlsAuthentication() {
      public void notifyServerCertificate(TlsServerCertificate s) {
        System.out.println("handshake OK; staple: " + (s.getCertificateStatusAt(0) != null)); }
      public TlsCredentials getClientCredentials(CertificateRequest r) { return null; } }; }
  }
  static class Srv extends DefaultTlsServer { Srv() { super(new BcTlsCrypto()); }
    public TlsCredentials getCredentials() throws IOException {
      BcTlsCrypto c = (BcTlsCrypto)context.getCrypto();
      return new BcDefaultTlsCredentialedSigner(new TlsCryptoParameters(context), c, k,
        new Certificate(new byte[0], new CertificateEntry[]{
          new CertificateEntry(c.createCertificate(leaf), null)}),
        SignatureAndHashAlgorithm.rsa_pss_rsae_sha256); }
    // return null below for the run where the server declines to staple
    public CertificateStatus getCertificateStatus() { return new CertificateStatus(
      CertificateStatusType.ocsp, new OCSPResponse(new OCSPResponseStatus(0), null)); }
  }
}

On the 1.86 and 1.87-SNAPSHOT jars, JDK 25, with the same output on both:

org.bouncycastle.tls.TlsFatalAlert: certificate_unknown(46); Server extensions missing TLS Feature 5
   at org.bouncycastle.tls.TlsUtils.checkTlsFeatures(Unknown Source)
   at org.bouncycastle.tls.TlsUtils.processServerCertificate(Unknown Source)
   at org.bouncycastle.tls.TlsClientProtocol.handleServerCertificate(Unknown Source)
   at org.bouncycastle.tls.TlsClientProtocol.receive13ServerCertificate(Unknown Source)

Dropping the extension line completes the handshake with a staple; the server declining to staple gives the same alert, so the check never reaches the staple.

It is fail closed, with no stapling bypass. The cost is reachability: a default BC client cannot complete TLS 1.3 with a must-staple server, and clearing the property avoids the alert only by giving up stapling. TLS 1.2, run separately, is unaffected where the server echoes status_request; where the server has allowMultiCertStatus() on, which is not the default, and the client offered both extensions, the status_request_v2 echo takes precedence (AbstractTlsServer.java:599-616) and the check fails there too, which looks like a separate question about which extension answers the feature.

Proposed change

Two shapes, and the choice is yours. The smaller one moves nothing: checkTlsFeatures already receives serverCertificate, getCertificateEntryAt(0).getExtensions() (Certificate.java:135) is where RFC 8446 sec. 4.4.2.1 puts the answer, and Certificate.parse leaves entry extensions null below 1.3 (:330-338), so counting a status_request key found there as satisfying the feature is a no-op for 1.2 and leaves the signature, the alert order and CheckTlsFeaturesExtensionTest alone. It tests key presence only: an entry whose status_request body does not decode passes it, and the handshake still ends a few lines on, when read13CertificateStatuses (called at TlsUtils.java:4962) hands the body to TlsExtensionsUtils.readStatusRequestExtension13 and that raises decode_error, so nothing undecodable reaches the application. The stricter one hoists the 1.3 read at :4962 above the check at :4951 and counts the feature as answered only when the decoded CertificateStatus for the leaf is non-null. As far as I can see the two accept and reject the same handshakes, a present key either decoding or ending the handshake (TlsUtils.java:5752-5756), so the difference is whether the check reads the decoded status, which costs it a new parameter or an overload beside the one that the unit test calls.

Tls13CertificateStatusTest drives 1.3 stapling handshakes end to end already, but builds no leaf carrying id-pe-tlsfeature, so nothing there reaches this check. Happy to put a patch together for whichever you prefer, with a must-staple case there asserting the handshake completes and getCertificateStatusAt(0) is non-null.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions