bc-java icon indicating copy to clipboard operation
bc-java copied to clipboard

X.509 Certificate with critical extendedKeyUsage (EKU) extension behaves differently between JDK and BC

Open ghbz2024 opened this issue 1 year ago • 8 comments

Loading an X.509 certificate through the JCE CertificateFactory using either the JDK or the BC provider results in an instance of the JCE X509Certificate that gives different answers from its hasUnsupportedCriticalExtension() method.

While the JDK apparently treats the EKU extension as supported when being marked as critical, the instance from the BC provider considers such an EKU as unsupported.

I'd assume that the method X509CertificateImpl.hasUnsupportedCriticalExtension() ([1]) should be extended to also skip over the Extension.extendedKeyUsage OID to treat it as supported.

However, I am uncertain what implications would arise from such a change. But I still assume that this extension can be considered as fully supported in BC, especially as RFC 5280 ([2]) explicitly states it being allowed to be marked as critical.

[1] https://github.com/bcgit/bc-java/blob/7324d9ad6bd8443c6ef0b08fd9eb854de79039a1/prov/src/main/java/org/bouncycastle/jcajce/provider/asymmetric/x509/X509CertificateImpl.java#L404

[2] https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.12

ghbz2024 avatar Aug 29 '24 12:08 ghbz2024

PS: in case sample code or a sample certificate would help, I'd be happy to provide the same.

ghbz2024 avatar Aug 29 '24 12:08 ghbz2024

You can mark any extension as Critical. The impact:

  • If a certificate processing entity cannot recognize/parse/process an extension, and it is not marked Critical - this extension is ignored and the processing continues;
  • If a processing entity can't recognize an extension, and it is marked Critical - the processing aborts.

mouse07410 avatar Aug 29 '24 15:08 mouse07410

Thanks for that clarification.

The question I would like to clarify in this issue is, whether inclusion of the EKU as being supported in BC will have any unintended side effects.

If code that makes use of the JCE method hasUnsupportedCriticalExtension() relies on a stable behavior, regardless of whether a certificate had been parsed using the JDK or the BC CertificateFactory, that code will currently see a different behavior.

Client code sometimes uses the outcome of that method to determine whether to accept a certificate or not - which might make such code fail when the BC provider is added with higher priority than the standard JDK provider.

ghbz2024 avatar Aug 29 '24 15:08 ghbz2024

Here's a small sample that illustrates the different behavior:

import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

public final class CriticalEKU {

private static final String CERT_PEM =
  "-----BEGIN CERTIFICATE-----\n" +
  "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\n" +
  "-----END CERTIFICATE-----\n";

public static void main( final String[] pArgs ) throws Exception
{
  Security.addProvider( new BouncyCastleProvider() );
  evaluateCertFromProvider( null );
  evaluateCertFromProvider( "BC" );
} // main

private static void evaluateCertFromProvider( final String pProviderName ) throws Exception
{
  System.out.println( "----------------------------------------------" );
  System.out.println( "provider: " + ( pProviderName == null ? "default" : pProviderName ) );
  final CertificateFactory lFactory = pProviderName == null ?
    CertificateFactory.getInstance( "X509" ) :
    CertificateFactory.getInstance( "X509", pProviderName );
  final X509Certificate lCert =
    ( X509Certificate )lFactory.generateCertificate(
      new ByteArrayInputStream( CERT_PEM.getBytes( StandardCharsets.UTF_8 ) ) );
  System.out.println( "extension OIDs:");
  System.out.println( "- non-critical: " + lCert.getNonCriticalExtensionOIDs() );
  System.out.println( "- critical:     " + lCert.getCriticalExtensionOIDs() );
  System.out.println( "=> has unsupported critical extension: " +
                      lCert.hasUnsupportedCriticalExtension() );
} // evaluateCertFromProvider

}

which will result in the following output when run on Java 17 with BouncyCastle 1.78.1:

----------------------------------------------
provider: default
extension OIDs:
- non-critical: [1.3.6.1.5.5.7.1.1, 2.5.29.14, 2.5.29.31, 2.5.29.35]
- critical:     [2.5.29.15, 2.5.29.19, 2.5.29.37]
=> has unsupported critical extension: false
----------------------------------------------
provider: BC
extension OIDs:
- non-critical: [1.3.6.1.5.5.7.1.1, 2.5.29.31, 2.5.29.35, 2.5.29.14]
- critical:     [2.5.29.19, 2.5.29.15, 2.5.29.37]
=> has unsupported critical extension: true

ghbz2024 avatar Aug 29 '24 15:08 ghbz2024

the code of that method in BC currently looks as follows:

    public boolean hasUnsupportedCriticalExtension()
    {
        if (this.getVersion() == 3)
        {
            Extensions  extensions = c.getTBSCertificate().getExtensions();

            if (extensions != null)
            {
                Enumeration     e = extensions.oids();

                while (e.hasMoreElements())
                {
                    ASN1ObjectIdentifier oid = (ASN1ObjectIdentifier)e.nextElement();

                    if (oid.equals(Extension.keyUsage)
                     || oid.equals(Extension.certificatePolicies)
                     || oid.equals(Extension.policyMappings)
                     || oid.equals(Extension.inhibitAnyPolicy)
                     || oid.equals(Extension.cRLDistributionPoints)
                     || oid.equals(Extension.issuingDistributionPoint)
                     || oid.equals(Extension.deltaCRLIndicator)
                     || oid.equals(Extension.policyConstraints)
                     || oid.equals(Extension.basicConstraints)
                     || oid.equals(Extension.subjectAlternativeName)
                     || oid.equals(Extension.nameConstraints))
                    {
                        continue;
                    }

                    Extension       ext = extensions.getExtension(oid);

                    if (ext.isCritical())
                    {
                        return true;
                    }
                }
            }
        }

        return false;
    }

=> would it be safe to add one more extension OID to the combined ifstatement to also treat the Extension.extendedKeyUsage as being supported when marked critical?

ghbz2024 avatar Aug 29 '24 16:08 ghbz2024

this issue might be of interest, indicating a related discrepancy between the Java and C# versions of BC:

https://github.com/bcgit/bc-csharp/issues/395

ghbz2024 avatar Aug 29 '24 16:08 ghbz2024

The question I would like to clarify in this issue is, whether inclusion of the EKU as being supported in BC will have any unintended side effects

Yes. Any entity (including BC) that does not know how to process this EKU will simply ignore it - usually it's no big deal. If you marked it Critical - your authentication (or whatever else you're going to use that certificate) will fail, which can be a big deal.

mouse07410 avatar Aug 29 '24 23:08 mouse07410

should it be considered a BC bug that BC treats the certificate differently from the JDK?

=> code that runs correctly with the JDK provider will fail on the BC provider ... (although the certificate is perfectly valid as issued by a CA)

ghbz2024 avatar Aug 29 '24 23:08 ghbz2024