8

I have two extremely similar self signed certificates, generated via two different methods.

To test them I have:

  1. Added an entry in my hosts file for local.mydomain.com
  2. Set up an nginx server to listen on that domain on port 443 with the certificate under test plus associated private key (I then switch the cert and restart nginx to compare)
  3. Connected to nginx with openssl s_client -connect local.mydomain.com -CAfile /path/to/the/ca/cert.pem

One certificate fails:

CONNECTED(00000003)
depth=0 CN = local.mydomain.com
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = local.mydomain.com
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
 0 s:/CN=local.mydomain.com
   i:/CN=local.mydomain.com
---

One certificate succeeds:

CONNECTED(00000003)
depth=0 CN = local.mydomain.com
verify return:1
---
Certificate chain
 0 s:/CN = local.mydomain.com
   i:/CN = local.mydomain.com
---

I compare the details of the certificates with openssl x509 -in /path/to/the/ca/cert.pem -text -noout

The failing cert:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            47:dc:02:c7:11:fc:8e:96:45:22:aa:6b:23:79:32:ca
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=local.mydomain.com
        Validity
            Not Before: Nov 18 11:55:31 2016 GMT
            Not After : Nov 18 12:15:31 2017 GMT
        Subject: CN=local.mydomain.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    <stuff>
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                TLS Web Client Authentication, TLS Web Server Authentication
            X509v3 Subject Alternative Name:
                DNS:local.mydomain.com
            X509v3 Subject Key Identifier:
                6D:4F:AF:E4:60:23:72:E5:83:27:91:7D:1D:5F:E9:7C:D9:B6:00:2A
    Signature Algorithm: sha256WithRSAEncryption
         <stuff>

The working cert:

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            9b:6b:3d:a3:b9:a3:a4:b4
    Signature Algorithm: sha256WithRSAEncryption
        Issuer: CN=local.mydomain.com
        Validity
            Not Before: Nov 19 13:27:30 2016 GMT
            Not After : Nov 19 13:27:30 2017 GMT
        Subject: CN=local.mydomain.com
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                Public-Key: (2048 bit)
                Modulus:
                    <stuff>
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Subject Key Identifier:
                03:E7:DA:AA:2E:CC:23:ED:C5:07:3D:E1:33:86:F5:22:D4:76:EB:CB
            X509v3 Authority Key Identifier:
                keyid:03:E7:DA:AA:2E:CC:23:ED:C5:07:3D:E1:33:86:F5:22:D4:76:EB:CB

            X509v3 Basic Constraints:
                CA:TRUE
    Signature Algorithm: sha256WithRSAEncryption
         57<stuff>

Looking at this the most obvious difference is that the working cert has CA:TRUE under X509v3 Basic Constraints. However, from reading around the web I was under the impression that self signed certs weren't meant to be CAs, in particular this says they normally won't be:

Basic self-signed certificate questions

The answer there says that being self-signed there is no CA involved. But maybe openssl requires self signed certs to have that set anyway? Or maybe the certs differ in some other pertinent way?

UPDATE:

I spent some time trying to printf debug the internals of openssl, not that I understand any of it. In the file v3_purp.c is the following macro:

#define ku_reject(x, usage) \
        (((x)->ex_flags & EXFLAG_KUSAGE) && !((x)->ex_kusage & (usage)))

It is used in this bit of code that checks for self-signed certificates:

/* Does subject name match issuer ? */

if (X509_check_akid(x, x->akid) == X509_V_OK &&
            !ku_reject(x, KU_KEY_CERT_SIGN))
            x->ex_flags |= EXFLAG_SS;

In the case of the failing certificate (x)->ex_flags & EXFLAG_KUSAGE is equal to 2

EXFLAG_KUSAGE gets set for the failing certificate here, earlier on in the same file:

static void x509v3_cache_extensions(X509 *x)
{
    ......
    if ((usage = X509_get_ext_d2i(x, NID_key_usage, NULL, NULL))) {
        if (usage->length > 0) {
            x->ex_kusage = usage->data[0];
            if (usage->length > 1)
                x->ex_kusage |= usage->data[1] << 8;
        } else
            x->ex_kusage = 0;
        x->ex_flags |= EXFLAG_KUSAGE;
        ASN1_BIT_STRING_free(usage);
    }
    ....

So it looks like the issue is the failing cert has the X509v3 Key Usage extension, but doesn't specify KU_KEY_CERT_SIGN in that extension?

UPDATE 2:

According to https://www.rfc-editor.org/rfc/rfc5280#section-4.2.1.3:

"The keyCertSign bit is asserted when the subject public key is used for verifying signatures on public key certificates. If the keyCertSign bit is asserted, then the cA bit in the basic constraints extension (Section 4.2.1.9) MUST also be asserted."

So the CA bit in basic constraints needn't be present, but if you include a X509v3 Key Usage section in the cert then according to the openssl codebase you must specify keyCertSign, and according to the RFC if you do specify keyCertSign then you must also include the CA bit basic constraints?

junichiro
  • 183
  • 1
  • 1
  • 5
  • x-post: https://stackoverflow.com/q/40696726/94687 – imz -- Ivan Zakharyaschev Sep 27 '17 at 10:17
  • 1
    OpenSSL requires certSign, as you saw; it does not require BC, and will accept 'issuer' cert with _no_ BC, though not with BC containing CA=false. 5280 requires BC (with CA=true), and so does CABforum Baseline Reqts, but those are only controlling (supposedly) for the Internet and the WWW, and it is possible to use X.509 certs without either. – dave_thompson_085 Jul 21 '18 at 16:27

2 Answers2

7

The issue described in the question can be reproduced with a simpler setup. I've created two self signed certificates for testing which only differ in that one has a CA flag (ss-ca.pem) while the other does not (ss-noca.pem). With openssl verify one can check if the certificate can be verified against a specific CA path.

The self signed certificate with CA:true gets successfully verified against itself ('OK') although it stumbles over X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT (error 18) while verifying the chain:

$ openssl verify -CAfile ss-ca.pem ss-ca.pem 
ss-ca.pem: CN = test CA
error 18 at 0 depth lookup:self signed certificate
OK

With the self signed certificate with CA:false the verification does not succeed (no 'OK') and it shows the error X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY (error 20):

$ openssl verify -CAfile ss-noca.pem ss-noca.pem 
ss-noca.pem: CN = test no CA
error 20 at 0 depth lookup:unable to get local issuer certificate

My theory is that OpenSSL tries to build the trust chain to a certificate given with -CAfile. To build the trust chain the issuer certificate subject must match the issuer of the certificate, the signature must be valid (i.e. validated using the issuers public key) and the issuer certificate must be allowed to sign certificates, i.e. CA:true. While the first two checks returns no problems with both certificates the check for CA:true is only valid for ss-ca.pem.

I think the main difference from OpenSSL -CAfile to other concepts of a trust store is that -CAfile only does what it name implies: it contain a list of trusted CA which are used to validate the trust chain. Contrary to this other trust store implementations might also contain any kind of certificates where the validation routine simply checks if the certificate send by the server is directly trusted because it is contained in the trust store, no matter what kind of flags the certificate has or even if it is expired.

This difference between a general purpose trust store and OpenSSL -CAfile can also be seen when putting a non-CA end entity certificate as trusted into the store. In the example I use a CA certificate ca.pem which has issued a end entity certificate server.pem which of course does not have CA:true.

Validating the end entity certificate against the CA certificate works as expected:

$ openssl verify -CAfile ca.pem server.pem
server.pem: OK

But trying to trusting the end entity certificate directly by putting it into the CA store does not work because the CA store is not a general purpose trust store but limited to CA certificates:

$ openssl verify -CAfile server.pem server.pem
error 20 at 0 depth lookup:unable to get local issuer certificate

With a general purpose trust store the last verification should have succeeded too because the end entity certificate was explicitly declared as trusted.

Steffen Ullrich
  • 190,458
  • 29
  • 381
  • 434
  • This seems contrary to the linked answer on "Basic self-signed certificate questions". It says "So a self-signed but not CA certificate, when used as a trust anchor, will be accepted as valid as an end-entity certificate (i.e. in a chain reduced to that certificate exactly) but not otherwise. This is the normal case". OpenSSL seemingly doesn't allow trust anchors that are not also CAs, even in a chain of 1. – junichiro Nov 20 '16 at 12:05
  • @junichiro: It might contradict the other answer or not. As written in the other answer a self-signed certificate is in effect direct trust. But it looks like that `-CAfile` does not specify a general purpose trust store but as the name implies only the store for trusted **CA**. – Steffen Ullrich Nov 20 '16 at 14:07
  • @junichiro: I've edited the answer to show that `-CAfile` does not implement a general purpose trust store but is limited to CA certificates. – Steffen Ullrich Nov 20 '16 at 14:15
  • OK, I guess that kind of makes sense. This means that anything ultimately based on openssl (in my case, the Python requests library) can't be made to trust self-signed certs without the CA bit set, which seems awkward, but at least I kind of understand what's going on now... – junichiro Nov 20 '16 at 18:54
  • 2
    For OpenSSL 1.0.2 (Jan. 2015) up, if you specify `-partial_chain` to commandline, or the equivalent `VERIFY_PARAMS` flag bit in the API, it accepts a non-self-signed (i.e. non-root) anchor in the truststore. – dave_thompson_085 Jul 21 '18 at 16:18
2

This issue has been fixed today in OpenSSL (see https://github.com/openssl/openssl/issues/1418); should be available soon.

dvo
  • 121
  • 2