Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CRL should (probably) not include the CRL distribution point URL #64

Closed
bruot opened this issue Aug 20, 2020 · 22 comments
Closed

CRL should (probably) not include the CRL distribution point URL #64

bruot opened this issue Aug 20, 2020 · 22 comments

Comments

@bruot
Copy link

bruot commented Aug 20, 2020

When creating a CA with a CRL URL, I find that OpenSSL fails to validate certificates signed by the CA. The reason seems to be that both certificates and the CRL include the CRL URL. If the CRL does not include that distribution point URL, validation succeeds.

Steps to reproduce:

  1. Create a "RootCA" with CRL: manage.py init_ca --crl-url=http://pki.domain/crl/RootCA.crl RootCA /CN=RootCA.
  2. In the admin site, download the CA as PEM format to "rootca.pem".
  3. In the admin site, create a certificate "Cert1" with some CSR and common name. Download the certificate as "Cert1.pem".
  4. Run manage.py dump_crl -f PEM --ca [ROOT_CA_SERIAL] RootCA_crl.pem.

Now, using all the files gathered before, let's try to validate the certificate:

  1. Run openssl verify -no-CAfile -no-CApath -crl_check_all -CRLfile RootCA_crl.pem -CAfile rootca.pem Cert1.pem

Result: Step 5. fails with error:

[RootCA subject]
error 44 at 1 depth lookup: Different CRL scope
error Cert1.pem: verification failed

I managed to make the validation work with the following:

  1. In the admin site, edit the CA to remove the CRL URL.
  2. Run manage.py dump_crl -f PEM --ca [ROOT_CA_SERIAL] RootCA_crl_wo_distribution_points.pem.
  3. Run openssl verify -no-CAfile -no-CApath -crl_check_all -CRLfile RootCA_crl_wo_distribution_points.pem -CAfile rootca.pem Cert1.pem

This gives:

Cert1.pem: OK

The certificates and CRL files can be examined with the following commands:

openssl x509 -noout -text -in Cert1.pem

Certificate:
    Data:
        Version: 3 (0x2)
        Serial Number:
            19:20:70:95:23:ae:72:e0:74:df:e2:66:d4:3b:3d:46:d4:b9:4a:e7
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: CN = RootCA
        Validity
            Not Before: Aug 21 20:08:00 2020 GMT
            Not After : Aug 21 00:00:00 2022 GMT
        Subject: CN = Cert1
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (4096 bit)
                Modulus:
                    00:96:ca:64:94:ed:87:d0:91:22:9f:e6:8c:86:4f:
                    1d:c4:2f:2a:e0:30:6e:c8:80:08:23:0a:ab:4e:7f:
                    [...]
                    26:0b:5f
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment, Key Agreement
            X509v3 Extended Key Usage: 
                TLS Web Server Authentication
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Authority Key Identifier: 
                keyid:D7:C5:7F:D8:74:40:96:40:2C:E8:4C:BF:D4:C5:EF:44:64:1F:B9:BD

            X509v3 CRL Distribution Points: 

                Full Name:
                  URI:http://pki.domain/crl/RootCA_crl.pem

            X509v3 Subject Alternative Name: 
                DNS:Cert1
            X509v3 Subject Key Identifier: 
                B7:3A:C8:4D:3A:53:3B:9E:2C:90:20:FA:AC:10:F6:D5:7E:BC:E2:01
    Signature Algorithm: sha512WithRSAEncryption
         03:06:50:69:df:55:50:ff:03:7b:5b:d0:ed:d4:04:0d:91:6b:
         08:a7:39:22:c2:4e:62:9d:d1:a6:f4:a0:f7:9a:0d:c2:fc:be:
         [...]
         e2:e0:2f:e6:dd:bf:e3:79

openssl crl -text -in RootCA_crl.pem -noout

Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: CN = RootCA
        Last Update: Aug 21 20:10:21 2020 GMT
        Next Update: Aug 22 20:10:21 2020 GMT
        CRL extensions:
            X509v3 Issuing Distribution Point: critical
                Full Name:
                  URI:http://pki.domain/crl/RootCA_crl.pem

            X509v3 Authority Key Identifier: 
                keyid:D7:C5:7F:D8:74:40:96:40:2C:E8:4C:BF:D4:C5:EF:44:64:1F:B9:BD

            X509v3 CRL Number: 
                1
No Revoked Certificates.
    Signature Algorithm: sha512WithRSAEncryption
         2f:ae:42:8c:ed:ad:49:5a:5a:f6:32:d4:d5:4e:3c:0a:82:ef:
         08:11:4c:82:13:c4:1e:2a:e4:46:c6:06:1f:99:85:f8:66:94:
         [...]
         9b:24:92:96:bf:a8:24:6a

openssl crl -text -in RootCA_crl_wo_distribution_points.pem -noout

Certificate Revocation List (CRL):
        Version 2 (0x1)
        Signature Algorithm: sha512WithRSAEncryption
        Issuer: CN = RootCA
        Last Update: Aug 21 21:06:04 2020 GMT
        Next Update: Aug 22 21:06:04 2020 GMT
        CRL extensions:
            X509v3 Authority Key Identifier: 
                keyid:D7:C5:7F:D8:74:40:96:40:2C:E8:4C:BF:D4:C5:EF:44:64:1F:B9:BD

            X509v3 CRL Number: 
                2
No Revoked Certificates.
    Signature Algorithm: sha512WithRSAEncryption
         6a:bc:94:29:19:e3:e9:e9:49:59:41:a4:1a:52:03:8e:34:24:
         68:d2:b7:6a:4b:f7:c0:a3:fe:34:c9:47:c9:76:21:6c:f3:53:
         [...]
         c6:c4:21:2c:4e:6b:91:2a

They show that the only difference between the two tests is that, in the first case, the CRL includes the "CRL distribution points" extension with a URL, while it does not in the second case. I don't know the details regarding CRLs, but I guess that the error exists so that the snake does not eats its tail by preventing OpenSSL to check the revocation status of the CRL itself, using... that same CRL.

When you look at CRLs from reputed CA, they don't include a CRL URL in their CRL files:

@bruot
Copy link
Author

bruot commented Aug 21, 2020

I had a look at RFC 5280. According to the spec, it is fine to have in the CRL an Issuing Distribution Point that includes CRL URLs. Moreover, I quickly compared side-by-side the RFC and the django_ca implementation at def get_crl(...) in models.py. I did not see anything wrong, except for a little error unrelated to this, which I will post as another issue later. So, the conclusion is that I have no idea what the problem is...

@mathiasertl
Copy link
Owner

I was able to reproduce the error message you described.

However I was able to verify the CRL like this - this CRL includes the Distribution Point:

$ openssl verify -CAfile rootca.pem -CRLfile RootCA_crl.pem -crl_check Cert1.pem 
Cert1.pem: OK

Not that this solves the issue, but at least it's not entirely wrong ;-).

@mathiasertl
Copy link
Owner

mathiasertl commented Aug 25, 2020

Update: Tested this with a second set of CA/CRL/Cert to - seems like -crl_check_all is causing the issue:

$ openssl verify -CAfile RootCA6.pem -CRLfile RootCA6_crl.pem Cert2.pem
Cert2.pem: OK
$ openssl verify -CAfile RootCA6.pem -crl_check_all -CRLfile RootCA6_crl.pem Cert2.pem
CN = RootCA6
error 44 at 1 depth lookup: Different CRL scope
error Cert2.pem: verification failed

In this set of certs I used a localhost CRL to make sure I'd see any connection on the dev server, and there is no connection to it either.

@bruot
Copy link
Author

bruot commented Aug 25, 2020

I think the reason for the problem may be this: https://trac.nginx.org/nginx/ticket/1094#comment:6 (meaning btw that Nginx client auth would also be affected).

The root certificate, "EE Certification Centre Root CA", has a CRL available at ​http://www.sk.ee/repository/crls/eeccrca.crl. This CRL lists Issuing Distrubution Point extension as follows:

        X509v3 Issuing Distrubution Point: critical
            Full Name:
              URI:http://www.sk.ee/repository/crls/eeccrca.crl

But there are no CRL Distribution Points in the certificate itself. As a result, OpenSSL refuses to to use this CRL when it tries to verify more than just a leaf certificate, for example:

$ openssl verify -CAfile EE_Certification_Centre_Root_CA.pem.crt -CRLfile eeccrca.crl.pem -crl_check_all KLASS3-SK_2010_EECCRCA_SHA384.pem.crt
KLASS3-SK_2010_EECCRCA_SHA384.pem.crt: C = EE, O = AS Sertifitseerimiskeskus, CN = EE Certification Centre Root CA, emailAddress = pki@sk.ee
error 44 at 1 depth lookup:Different CRL scope

This probably should be reported to the sk.ee team, likely they want to fix this. Simply removing the IDP extension from the CRL should do the trick.

It looks like the presence of CRL distribution points (DP) in the CRL and in the certificate of the CA that issues the CRL must be consistent. In the example I gave in the bug description, the CA does not show the CRL DP in its own certificate, but the CRL does.

If this is correct, I assume that CRLs of root CAs should not have CRL DP. The alternative would be to have the CRL DP also in the root CA certificate, but this is not common practice.

@mathiasertl
Copy link
Owner

mathiasertl commented Aug 29, 2020

Hmm. I don't think I fully understood this yet. This makes sense:

It looks like the presence of CRL distribution points (DP) in the CRL and in the certificate of the CA that issues the CRL must be consistent. In the example I gave in the bug description, the CA does not show the CRL DP in its own certificate, but the CRL does.

I tested this with the CA/CRL/Cert I generated for this comment, and:

  1. CA does not contain the CRL URL.
  2. CRL and CERT do contain a (consistent) CRL.

But I do not understand what you mean by this:

If this is correct, I assume that CRLs of root CAs should not have CRL DP.

If that is correct, wouldn't this mean that the same is true for intermediate CAs? The intermediate CAs CRL DP provides information on CAs revoked by the root CA (the issuer). So it's not the same URL in the CAs DP is not the same in the certificates signed by the intermediate CA. Basically:

Certificate CRL DP
Root CA -
Intermediate CA http://example.com/certs-signed-by-root-ca.pem
End-user cert signed by Intermediate CA http://example.com/certs-signed-by-intermediate-ca.pem

The two CRLs would then have the following IDPs:

CRL IDP
.../certs-signed-by-root-ca.pem http://example.com/certs-signed-by-root-ca.pem
.../certs-signed-by-intermediate-ca.pem http://example.com/certs-signed-by-intermediate-ca.pem

So that means verification of the end-user cert (only with regards to CRLs) would mean that CRLs are still not consistent. :-(

If the CRL DP is known at the time when the Root CA is generated, it could be added as an extension to the Root CA. CRL and signed certificate would thus all be consistent. - A CRL DP in the CA would provide revocation info for the CA, would could only be signed by the CA itself. So that makes no sense.

The alternative would be to have the CRL DP also in the root CA certificate, but this is not common practice.

In that, in any case, you appear to be correct: https://django-ca.readthedocs.io/en/latest/ca_examples.html#id12 - but I'd guess this is also because public CAs usually never sign a client certificiate.

@mathiasertl
Copy link
Owner

I guess it's not encouraging that that the Estionian CA still has not changed their setup at all.

@mathiasertl
Copy link
Owner

mathiasertl commented Aug 30, 2020

I have now created a more test cases to see how OpenSSL handles CRLs and DP/IDP and if I can make any sense of it related to RFC 5280. It seems the issue comes down to - as the headline suggests - the -crl_check_all flag and the DP/IDP extension in the root CA and CRL for it.

Note that I'm limiting this analysis to validating the Root CA itself. I do not generate any client certs or intermediate CAs.

Current behaviour

First, I created a Root CA and CRL as-is with the current code, meaning no CRLDistributionPoint in Root CA and created a CRL for it. This is basically just reproducing your initial report.

$ ./manage.py init_ca --pathlen=2 Root /CN=Root
$ ./manage.py dump_ca Root > Root.pem
$ ./manage.py dump_crl -f PEM --ca Root Root.crl.pem

Now, let's try some validation:

# first, no CRL checks at all:
$ openssl verify -CAfile Root.pem Root.pem 
Root.pem: OK

# Even just -crl_check fails, not just -crl_check_all:
$ openssl verify -CAfile Root.pem -CRLfile Root.crl.pem -crl_check Root.pem 
CN = Root
error 44 at 0 depth lookup: Different CRL scope
error Root.pem: verification failed
$ openssl verify -CAfile Root.pem -CRLfile Root.crl.pem -crl_check_all Root.pem 
CN = Root
error 44 at 0 depth lookup: Different CRL scope
error Root.pem: verification failed

This matches your intial report, the only thing (a little bit) new is the simplification of just validating the root CA itself, so now just -crl_check also fails.

Remove IDP from CRL

Via a manual source code edit, I ensured that the CRL does not contain an IDP. So CA: No cRLDP, CRL: No IDP.

$ ./manage.py dump_crl -f PEM --ca Root Root-no-idp.crl.pem  # manual source code edit to ensure no IDP in CRL
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem -crl_check Root.pem 
Root.pem: OK
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem -crl_check_all Root.pem 
Root.pem: OK

So it seems that the IDP extension (in its current form) in combination with no cRLDP indeed breaks validation.

CA with cRLDP

A theory above was: "The alternative would be to have the CRL DP also in the root CA certificate, but this is not common practice.", and the nginx issue says that the CRL has an IDP but the Root CA is missing a CRL DP. So lets test this theory.

So I tried to create a CA with - as you and the linked issue for NGINX suggested - a DP in the root CA. Again, I made sure of this via a manual source code edit.

$ ./manage.py init_ca WithCRLDP /CN=WithCRLDP
$ ./manage.py dump_ca WithCRLDP > WithCRLDP.pem
$ openssl x509 -noout -text -in WithCRLDP.pem
...
            X509v3 CRL Distribution Points: 

                Full Name:
                  URI:http://localhost:8000/django_ca/crl/ca/56AD2C5809D194D32CFBC6604D19E78D96126073/
...
$ ./manage.py dump_crl -f PEM --ca WithCRLDP > WithCRLDP.crl.pem
$ openssl crl -in WithCRLDP.crl.pem -noout -text
...
        CRL extensions:
            X509v3 Issuing Distribution Point: critical
                Full Name:
                  URI:http://localhost:8000/django_ca/crl/56AD2C5809D194D32CFBC6604D19E78D96126073/

... So now the Root CA and the CRL have a matching DP. While I consider a CRL DP in a Root CA nonsensical, still: If the theory is correct, validation should work now...

$ openssl verify -CAfile WithCRLDP.pem -CRLfile WithCRLDP.crl.pem -crl_check WithCRLDP.pem 
CN = WithCRLDP
error 44 at 0 depth lookup: Different CRL scope
error WithCRLDP.pem: verification failed

... so simply adding a CRL DP is not a solution, even if it would make sense.

Intermediate CAs & enduser certificates

Next, I wanted to test if removing the IDP from CRLs for the Root CA solves the issue also for intermediate CAs and enduser certificates. (Note that CRLs for child CAs again include an IDP!)

# Create a Child and grandchild CA and a signed certificate
$ ./manage.py init_ca --pathlen=1 --parent=Root Child /CN=Child
$ ./manage.py dump_ca Child > Child.pem
$ ./manage.py init_ca --parent=Child Grandchild /CN=Grandchild
$ ./manage.py dump_ca Grandchild > Grandchild.pem

# Create two enduser certs, one for Child, one for Grandchild
$ ./manage.py sign_cert --subject /CN=example.com --cn-in-san --csr hostname.csr --out child-cert.pem --ca Child
$ ./manage.py sign_cert --subject /CN=example.com --cn-in-san --csr hostname.csr --out grandchild-cert.pem --ca Grandchild

# Create CRLs for Child and Grandchild
$ ./manage.py dump_crl --ca Child > Child.crl.pem
$ ./manage.py dump_crl --ca Grandchild > Grandchild.crl.pem

So, finally, let's try to validate the Child CA and the cert signed by it.

$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem -crl_check_all Child.pem
Child.pem: OK
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem -untrusted Child.pem -CRLfile Child.crl.pem -crl_check_all child-cert.pem 
child-cert.pem: OK

Looks promising, but now let's look at the Grandchild CA:

$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem \
>    -untrusted Child.pem -CRLfile Child.crl.pem \
>    -untrusted Grandchild.pem -CRLfile Grandchild.pem \
>    -crl_check_all Grandchild.pem 
CN = Grandchild
error 44 at 0 depth lookup: Different CRL scope
error Grandchild.pem: verification failed
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem \
>    -untrusted Child.pem -CRLfile Child.crl.pem \
>    -untrusted Grandchild.pem -CRLfile Grandchild.crl.pem \
>    -crl_check_all grandchild-cert.pem 
CN = Grandchild
error 44 at 1 depth lookup: Different CRL scope
error grandchild-cert.pem: verification failed

... sigh, ☹️. So grandchild validation does not work anymore either. For the record, this is how the DP look like in the current CAs and CRLs - I have abbreviated the serials to 8 characters for better readability:

CA/CRL/Cert Serial DP
Root.pem 0F30C839 -
Root.crl.pem http://localhost:8000/django_ca/crl/0F30C839/
Root-no-idp.crl.pem -
Child.pem 36C472B2 http://localhost:8000/django_ca/crl/ca/0F30C839/
Child.crl.pem http://localhost:8000/django_ca/crl/0F30C839/
Grandchild.pem 3A9BA6F1 http://localhost:8000/django_ca/crl/ca/36C472B2/
Grandchild.crl.pem http://localhost:8000/django_ca/crl/3A9BA6F1/
child-cert.pem http://localhost:8000/django_ca/crl/36C472B2/
grandchild-cert.pem http://localhost:8000/django_ca/crl/3A9BA6F1/

Alright, as we can see here, CAs contain a /ca in the DP path, while CRLs and Certs do not. This turns out not to be a problem in the Child case, both for the Child CA and it's cert:

  1. For the Root CA, it uses the Root-no-idp.crl CRL, as before.
  2. For the Child CA, it uses again Root-no-idp.crl, as it was issued by the same CA that issued the Child CA.
  3. For the Child enduser certificate, it uses Child.crl.pem, as it was issued by the same CA that issued the child cert. CRL and Cert have a matching DP.

For the Grandchild case, the first two steps in validation are identical, but after that:

  1. For the Grandchild CA, it uses Child.crl.pem, as it has the same issuer as the Grandchild CA. But the CRL and the CA have no matching DP.
  2. For the Grandchild enduser certificate, validation for the certificate itself passes but fails for the Grandchild CA (see "1 depth" in the output above).

Intermediate CAs & enduser certificates - fixed DP

To test above theory, I again modified the source to generate CAs without the /ca in the path (again: Serials are shortened to 8 chars).

$ ./manage.py init_ca --pathlen=1 --parent=Root ChildFixedDP /CN=ChildFixedDP
$ ./manage.py init_ca --parent=ChildFixedDP GrandchildFixedDP /CN=GrandchildFixedDP
$ ./manage.py dump_ca ChildFixedDP > ChildFixedDP.pem
$ ./manage.py dump_ca GrandchildFixedDP > GrandchildFixedDP.pem

# Notice how the /ca is now gone from the path
$ openssl x509 -noout -text -in ChildFixedDP.pem  | grep django_ca/crl
                  URI:http://localhost:8000/django_ca/crl/0F30C839/
$ openssl x509 -noout -text -in GrandchildFixedDP.pem  | grep django_ca/crl
                  URI:http://localhost:8000/django_ca/crl/1648A015/

Next, we create CRLs and certs again:

$ ./manage.py dump_crl --ca ChildFixedDP > ChildFixedDP.crl.pem
$ ./manage.py dump_crl --ca GrandchildFixedDP > GrandchildFixedDP.crl.pem
$ ./manage.py sign_cert --subject /CN=example.com --cn-in-san --csr hostname.csr --out child-fixed-dp-cert.pem --ca ChildFixedDP
$ ./manage.py sign_cert --subject /CN=example.com --cn-in-san --csr hostname.csr --out grandchild-fixed-dp-cert.pem --ca GrandchildFixedDP

... finally, let's try validation again:

# This was working before, just to be sure
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem -crl_check_all ChildFixedDP.pem
ChildFixedDP.pem: OK
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem -untrusted ChildFixedDP.pem -CRLfile ChildFixedDP.crl.pem -crl_check_all child-fixed-dp-cert.pem 
child-fixed-dp-cert.pem: OK

# *drumroll*
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem \
>    -untrusted ChildFixedDP.pem -CRLfile ChildFixedDP.crl.pem \
>    -crl_check_all GrandchildFixedDP.pem 
GrandchildFixedDP.pem: OK
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem \
>    -untrusted ChildFixedDP.pem -CRLfile ChildFixedDP.crl.pem \
>    -untrusted GrandchildFixedDP.pem -CRLfile GrandchildFixedDP.crl.pem \
>    -crl_check_all grandchild-fixed-dp-cert.pem 
grandchild-fixed-dp-cert.pem: OK

OKAY. Finally :-). Validation bliss! ;-)

Intermediate CAs & enduser certificates - scoped CRLs

As you can perhaps guess, the initial motiviation for separate CRLs for CAs and Certificates was that one is small and rarely updated, while the latter is potentially bigger and frequently updated. But CRLs provide a scope attribute allowing us to distinguish from CRLs for CAs and enduser certificates. So let's try this out, again with a small, manual patch to modify the URL depending on the scope.

We can use the CAs and certificates from before (those without the fixed DP!) but have to dump CRLs again, this time with the existing --scope parameter.

$ ./manage.py dump_crl --ca Child --scope=ca > ChildScopeCA.crl.pem
$ ./manage.py dump_crl --ca Child --scope=user > ChildScopeUser.crl.pem
$ openssl crl -noout -text -in ChildScopeCA.crl.pem
...
                Full Name:
                  URI:http://localhost:8000/django_ca/crl/ca/36C472B2/
                Only CA Certificates
$ openssl crl -noout -text -in ChildScopeUser.crl.pem
...
                Full Name:
                  URI:http://localhost:8000/django_ca/crl/36C472B2/
                Only User Certificates

Notice that the two CRLs now have (1) a different URL and (2) a different scope. The URL in ChildScopeCA.crl.pem now matches the URL in Grandchild.pem. Now let's try to validate again:

$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem \
>    -untrusted Child.pem -CRLfile ChildScopeCA.crl.pem \
>    -crl_check_all Grandchild.pem 
Grandchild.pem: OK
$ openssl verify -CAfile Root.pem -CRLfile Root-no-idp.crl.pem \
>    -untrusted Child.pem -CRLfile ChildScopeCA.crl.pem \
>    -untrusted Grandchild.pem -CRLfile Grandchild.crl.pem \
>    -crl_check_all grandchild-cert.pem 
grandchild-cert.pem: OK

So... this works as well.

Final thoughts

I think we have to think in a bit more detail on how to do this exactly right, but at least we now know what works and what not.

I clearly found a few bugs with intermediate CAs while testing this:

  1. Default URLs (CRL, OCSP, ...) use the serial of the root CA, not the parent CA. Fixed in 55c1302.
  2. I think there is still some issues with zero-padding in URLs - I need to investigate this further. (Zero-padding is consistent after all)
  3. IDP for Root CRL has to be removed, unless we find a different way to validate it.
  4. Obviously: however we solve all of the above.

@mathiasertl
Copy link
Owner

Probably related: openssl/openssl#2873 and openssl/openssl#8439.

@bruot
Copy link
Author

bruot commented Aug 30, 2020

Great job at narrowing down the problem to the root CA only! If I understand your detailed comment correctly, the grandchild CA thing is a separate problem.

About the initial issue: It is still not clear what the origin of the "Different CRL scope" error is. To me, the scope of the root CRL in the initial example is "all the certificates signed by the root CA". That includes the root CA itself, so that the error message when checking that certificate only does not make any sense. Therefore, it is hard to say if removing the IDP from the root CRL is the best fix. Maybe it would be worth at this point to get an opinion from an OpenSSL developer? There could be a bug in their library...

@bruot
Copy link
Author

bruot commented Aug 30, 2020

Question asked to OpenSSL!

@mathiasertl
Copy link
Owner

@bruot after sleeping a few days, I think (but I have no time to try it out today), further insight: When I tried "CA with cRLDP" above, I still had the issue I found in "Intermediate CAs & enduser certificates - fixed DP" later. I even mention the different URLs:

$ openssl x509 -noout -text -in WithCRLDP.pem
                  URI:http://localhost:8000/django_ca/crl/ca/56AD2C5809D194D32CFBC6604D19E78D96126073/
$ openssl crl -in WithCRLDP.crl.pem -noout -text
                  URI:http://localhost:8000/django_ca/crl/56AD2C5809D194D32CFBC6604D19E78D96126073/

So I will try this out again with a fixed DP for the CA to see if it's working.

@mathiasertl
Copy link
Owner

So I've just now verified this behaviour. Again with a patch (see bottom) to make sure that cRLDP in the Root CA and the IDP in the CRL now match. And this also works:

$ ./manage.py init_ca --pathlen=2 FixedRoot /CN=FixedRoot
$ ./manage.py dump_crl -f PEM --scope=ca --ca FixedRoot FixedRoot.crl.pem
$ openssl x509 -noout -text -in FixedRoot.pem  | grep django_ca
                  URI:http://localhost:8000/django_ca/crl/ca/54AB2253D301520F27901CD437C948339842A14/
$ openssl crl -in FixedRoot.crl.pem -noout -text | grep django_ca
                  URI:http://localhost:8000/django_ca/crl/ca/54AB2253D301520F27901CD437C948339842A14/
$ openssl verify -CAfile FixedRoot.pem -CRLfile FixedRoot.crl.pem -crl_check_all FixedRoot.pem 
FixedRoot.pem: OK

This is the diff I used:

$ git diff
diff --git a/ca/django_ca/managers.py b/ca/django_ca/managers.py
index 553998ed..a9c078e6 100644
--- a/ca/django_ca/managers.py
+++ b/ca/django_ca/managers.py
@@ -207,7 +207,7 @@ class CertificateAuthorityManager(CertificateManagerMixin, models.Manager):
             if not crl_url:
                 crl_path = reverse('django_ca:crl', kwargs={'serial': hex_serial})
                 crl_url = ['http://%s%s' % (default_hostname, crl_path)]
-            if parent and not ca_crl_url:  # CRL for CA only makes sense in intermediate CAs
+            if not ca_crl_url:  # CRL for CA only makes sense in intermediate CAs
                 ca_crl_path = reverse('django_ca:ca-crl', kwargs={'serial': root_serial})
                 ca_crl_url = ['http://%s%s' % (default_hostname, ca_crl_path)]
 
diff --git a/ca/django_ca/models.py b/ca/django_ca/models.py
index 4a61650c..770c2e17 100644
--- a/ca/django_ca/models.py
+++ b/ca/django_ca/models.py
@@ -704,6 +704,11 @@ class CertificateAuthority(X509CertMixin):
         if kwargs.get('full_name'):
             full_name = kwargs['full_name']
             full_name = [parse_general_name(n) for n in full_name]
+        elif scope == 'ca':
+            crl_path = reverse('django_ca:ca-crl', kwargs={'serial': self.serial})
+            full_name = [x509.UniformResourceIdentifier(
+                'http://%s%s' % (settings.CA_DEFAULT_HOSTNAME + crl_path)
+            )]
         elif self.crl_url:
             crl_url = [url.strip() for url in self.crl_url.split()]
             full_name = [x509.UniformResourceIdentifier(c) for c in crl_url]

@mathiasertl
Copy link
Owner

mathiasertl commented Sep 5, 2020

So bottom line, this is what is the resulting task list for this issue, some of this is already solved. Please let me know what you think of this:

  • Default URLs (CRL, OCSP, ...) use the serial of the root CA, not the parent CA. Fixed in 55c1302.
  • CRLs for root CAs do not add an IDP by default. An IDP can always be added via command-line parameter already.
  • CRLs for intermediate CAs and with scope=CA use the ca CRL for the cRLDP default.
  • Add end-to-end verification unittests using openssl on the CLI with what we learned above.

I'm unsure if we should allow adding a cRLDP to root CAs. There apparently is a use case, but a rare one at the very least. No public root CA that I could find sets this extension. Currently, trying to add a cRLDP to a root CA is caught by django-ca and throws an error that they cannot be used.

@bruot
Copy link
Author

bruot commented Sep 5, 2020

@mathiasertl I agree with your points 1, 3 and 4. For point 2, I don't understand "An IDP can always be added via command-line parameter already.". Is it in manage.py dump_crl? In any case, it looks good to have a default behaviour that should work in most configurations, but also let the user tune what's in the CRL if he wants to.

@mathiasertl
Copy link
Owner

mathiasertl commented Sep 5, 2020

For point 2, I don't understand "An IDP can always be added via command-line parameter already.". Is it in manage.py dump_crl?

Yes, that's what I meant. Not that it's true, after checking 😆.

I've just finished openssl verify tests here: https://github.com/mathiasertl/django-ca/blob/release/1.16.1/ca/django_ca/tests/tests_verification.py.

I'm quite happy with the state now. The test suite passes, the default creates valid CRLs. Unless it's a feature you required, I would delay manual CRL DP creation for the next release and release the current code as is as 1.16.1.

If possible, please test the release/1.16.1 branch if everything works for you as expected. I will release once you let me know (or next week or so if you don't respond 😄 ).

@bruot
Copy link
Author

bruot commented Sep 5, 2020

Thanks for the changes. It looks all fine with 9f34420, except one thing:

When creating a root CA and then an intermediate CA, the init_ca command throws me an exception (although the CA seems to be created and well formed):

python manage.py init_ca --pathlen=1 --expires=20 RootCA_4 "/CN=RootCA_4"

python manage.py init_ca --parent 2C:CC:6C:3B:6E:54:75:B0:20:C3:3C:32:49:5A:48:62:61:89:5A:80 --expires=20 IntermediateCA_4 "/CN=IntermediateCA_4" &> /tmp/error_log

gives:

Traceback (most recent call last):
  File "manage.py", line 22, in <module>
    main()
  File "manage.py", line 18, in main
    execute_from_command_line(sys.argv)
  File "/var/local/cache/djca_test/python/lib/python3.7/site-packages/django/core/management/__init__.py", line 401, in execute_from_command_line
    utility.execute()
  File "/var/local/cache/djca_test/python/lib/python3.7/site-packages/django/core/management/__init__.py", line 395, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/var/local/cache/djca_test/python/lib/python3.7/site-packages/django/core/management/base.py", line 330, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/var/local/djca_test/djca_test/django_ca/management/base.py", line 281, in execute
    super(BaseCommand, self).execute(*args, **options)
  File "/var/local/cache/djca_test/python/lib/python3.7/site-packages/django/core/management/base.py", line 371, in execute
    output = self.handle(*args, **options)
  File "/var/local/djca_test/djca_test/django_ca/management/commands/init_ca.py", line 195, in handle
    run_task(cache_crl, serial=ca.serial, password=options['password'])
  File "/var/local/djca_test/djca_test/django_ca/tasks.py", line 35, in run_task
    return task(*args, **kwargs)
  File "/var/local/djca_test/djca_test/django_ca/tasks.py", line 41, in cache_crl
    ca.cache_crls(**kwargs)
  File "/var/local/djca_test/djca_test/django_ca/models.py", line 560, in cache_crls
    scope=scope, full_name=full_name, relative_name=relative_name)
  File "/var/local/djca_test/djca_test/django_ca/models.py", line 723, in get_crl
    'http://%s%s' % (settings.CA_DEFAULT_HOSTNAME, crl_path)
  File "/var/local/cache/djca_test/python/lib/python3.7/site-packages/django/conf/__init__.py", line 84, in __getattr__
    val = getattr(self._wrapped, name)
AttributeError: 'Settings' object has no attribute 'CA_DEFAULT_HOSTNAME'

Line 723 in "django_ca/models.py" appears to be something you have just modified. If I add CA_DEFAULT_HOSTNAME = None to my settings.py, it works, but the doc suggests the parameter is optional.

@mathiasertl
Copy link
Owner

yes that certainly is an issue I'll fix!

@mathiasertl
Copy link
Owner

tested and fixed your issue. it should access (and check for) ca_settings.CA_DEFAULT_HOSTNAME instead.

@mathiasertl
Copy link
Owner

Fixed in 1.16.1. I'm leaving this one open for a while in case you still have problems.

@bruot
Copy link
Author

bruot commented Sep 7, 2020

@mathiasertl It looks all good to me. Thanks for this and the other fixes!

@kevin-olbrich
Copy link

kevin-olbrich commented Jun 27, 2022

How can I remove the IDP from CertificateRevocationListView? The docs state I need to set full_name to None but this is already the default.

I've now monkey patched add_idp to be False and the verfication succeeds.

add_idp = (

Is it possible to get a global setting to disable IDP completely or as an alternative, as on option to CertificateRevocationListView like "expires"?

Thank you very much!

@mathiasertl
Copy link
Owner

Hi @kevin-olbrich,

Thank you for your comment and question!

However, in the future it's probably better to just create a new ticket, rather then commenting on a two year old closed ticket. If you have further questions, please just open a new ticket.

The docs seem to be a little out of date in that particular function: An IDP is always added, just the full name will not be set for root CAs with a ca scope or no scope.

Is it possible to get a global setting to disable IDP completely or as an alternative, as on option to CertificateRevocationListView like "expires"?

YES absolutely! BUT for that please file a new issue and please make sure that you document the use case. Where/how are you using your CAs/CRLs so that the current code does not work for you?

kr, Mat

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants