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

Feature Request: send entire certificate chain (including root ca) #6776

Open
hanjo opened this issue Jan 8, 2025 · 8 comments
Open

Feature Request: send entire certificate chain (including root ca) #6776

hanjo opened this issue Jan 8, 2025 · 8 comments
Labels
discussion 💬 The right solution needs to be found

Comments

@hanjo
Copy link

hanjo commented Jan 8, 2025

Hi,

I noticed that caddy will present the intermediate ca certificate and the host certificate when accessing it. This can be checked with openssl, for example:

echo "" | openssl s_client -showcerts -servername example.com -connect example.com:443

Here is an example with an internal pki:

[...]
---
Certificate chain
 0 s:
   i:CN = Caddy - ECC Intermediate
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Jan  8 06:37:14 2025 GMT; NotAfter: Jan  8 18:37:14 2025 GMT
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
 1 s:CN = Caddy - ECC Intermediate
   i:CN = Caddy - 2025 ECC Root
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Jan  6 22:07:41 2025 GMT; NotAfter: Jan 13 22:07:41 2025 GMT
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
---
[...]

In this example "1" is the intermediate ca certificate and "0" is the host certificate.

This is usually working fine, as long as the client (i.e. browser) knows and trusts the root ca certificate. However, if the certificate chain is more complex (i.e. longer), this can cause trouble, because the browser wouldn't be able to fill any gaps in the certificate chain presented by caddy.

To fix this, caddy would need to present the entire certificate chain - including the root ca certificate - to the client. I checked the documentation, but I could not find an option to enable this.

So far, I've been under the impression that it is good practice to include the entire certificate chain, which is also done by many (though not all) popular public websites, for example GitHub:

$ echo "" | openssl s_client -showcerts -servername github.com -connect github.com:443
CONNECTED(00000003)
depth=2 C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust ECC Certification Authority
verify return:1
depth=1 C = GB, ST = Greater Manchester, L = Salford, O = Sectigo Limited, CN = Sectigo ECC Domain Validation Secure Server CA
verify return:1
depth=0 CN = github.com
verify return:1
---
Certificate chain
 0 s:CN = github.com
   i:C = GB, ST = Greater Manchester, L = Salford, O = Sectigo Limited, CN = Sectigo ECC Domain Validation Secure Server CA
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA256
   v:NotBefore: Mar  7 00:00:00 2024 GMT; NotAfter: Mar  7 23:59:59 2025 GMT
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
 1 s:C = GB, ST = Greater Manchester, L = Salford, O = Sectigo Limited, CN = Sectigo ECC Domain Validation Secure Server CA
   i:C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust ECC Certification Authority
   a:PKEY: id-ecPublicKey, 256 (bit); sigalg: ecdsa-with-SHA384
   v:NotBefore: Nov  2 00:00:00 2018 GMT; NotAfter: Dec 31 23:59:59 2030 GMT
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
 2 s:C = US, ST = New Jersey, L = Jersey City, O = The USERTRUST Network, CN = USERTrust ECC Certification Authority
   i:C = GB, ST = Greater Manchester, L = Salford, O = Comodo CA Limited, CN = AAA Certificate Services
   a:PKEY: id-ecPublicKey, 384 (bit); sigalg: RSA-SHA384
   v:NotBefore: Mar 12 00:00:00 2019 GMT; NotAfter: Dec 31 23:59:59 2028 GMT
-----BEGIN CERTIFICATE-----
[...]
-----END CERTIFICATE-----
---
[...]

As you can see, "2" is the root ca certificate, "1" is the intermediate ca certificate and "0" is the host certificate.

Therefore, I'd like to request a configuration switch to enable sending the root ca certificate in the certificate chain.

Let me know what you think.
Thanks!

@hslatman
Copy link
Contributor

hslatman commented Jan 8, 2025

Hey @hanjo,

It generally is good practice to send the full chain, excluding the roots.

Roots are to be installed/enabled on relying parties (e.g. client systems such as a browser), so that there's control over which roots are trusted. If a relying party were to blindly trust a root presented by a server (without additional verification) it would be equivalent to trusting any root, and thus similar guarantees as no TLS at all. So, as a web server, it's not logical to return a root certificate as part of the "full chain".

The github.com certificate is a bit of a special case in which there are two different trust paths leading to a trusted root. The first chains to USERTrust ECC Certification Authority. The second chains to AAA Certificate Services. The second includes the first chain in full: an example of cross-signing. This is documented in Sectigo's knowledge base: https://support.sectigo.com/articles/Knowledge/Sectigo-Chain-Hierarchy-and-Intermediate-Roots. This is done for compatibility reasons.

@hanjo
Copy link
Author

hanjo commented Jan 8, 2025

Hi @hslatman,

that's an interesting perspective and I can understand the logic behind, although I don't really see the risk, as no browser/client is blindly trusting a root certificate which is not in their truststore and adding it to the truststore is a manual process which requires verification by the user, regardless if the new root ca was shared by caddy or manually imported.

I'd like to elaborate my requirements some more, maybe that clarifies things:
Assume I have an existing root ca which is not directly available to caddy - say it resides on an air-gapped server for security reasons. This root CA is trusted by all the clients in my networking. I will now create an intermediate ca, have it signed by the root ca and deploy it into the caddy configuration as root ca. Caddy will now use this intermediate ca, create another short-lived intermediate ca underneath and then the host certificates.
When a client visits such a host, there will be a certificate warning, because the client is not presented with the first intermediate ca (only the second one) and thus cannot create the trust link to the original trusted root ca. In this scenario, if caddy would be sharing the entire certificate chain (from caddy's point of view that is), it would work.

Therefore, I still believe such a configuration option would be beneficial, albeit I agree that by default this should probably be disabled, like it is now.

@bt90
Copy link
Contributor

bt90 commented Jan 9, 2025

I will now create an intermediate ca, have it signed by the root ca and deploy it into the caddy configuration as root ca.

Seems like an elaborate way to shoot yourself in the foot? It's an intermediate certificate, just treat it like that.

@hanjo
Copy link
Author

hanjo commented Jan 9, 2025

It might be worth considering that there are specific scenarios where having the option to send the entire certificate chain could add value, like I tried to point out. This feature request aims to address those use cases, providing flexibility for users who need it.

@mholt
Copy link
Member

mholt commented Jan 9, 2025

I'm not sure I'm convinced.

This root CA is trusted by all the clients in my networking. I will now create an intermediate ca, have it signed by the root ca and deploy it into the caddy configuration as root ca. Caddy will now use this intermediate ca, create another short-lived intermediate ca underneath and then the host certificates.

So you have:

Root --> Intermediate 1 --> Intermediate 2 --> Leaf

and Root is already trusted.

I think if a client is presented with Intermediate 2 and Leaf, it should be trusted by Root, unless the chain was formed incorrectly, no? I could be wrong though.

@mholt mholt added the discussion 💬 The right solution needs to be found label Jan 9, 2025
@hanjo
Copy link
Author

hanjo commented Jan 9, 2025

Thanks @mholt for entertaining this idea.

So you have:

Root --> Intermediate 1 --> Intermediate 2 --> Leaf

and Root is already trusted.

This is exactly the scenario. The client trusts "Root", so in turn any certificate where "Root" is the issuer is also trusted. When the client requests the site through caddy, caddy will present the two certificates "Leaf" and "Intermediate 2". "Leaf" is issued by "Intermediate 2", so not trusted (yet). "Intermediate 2" is issued by "Intermediate 1", so not trusted (yet). "Intermediate 1" is unknown to the client (because it is neither in the trust store, nor has it been sent by caddy) and thus the client does not know by whom it's been issued. Therefore, also "Intermediate 2" and "Leaf" remain untrusted.

So there are two solutions to this issue: either I put "Intermediate 1" in the client's truststore, or the webserver (caddy in this case) needs to send also the public part of "Intermediate 1" along with the other two.

@mohammed90
Copy link
Member

So there are two solutions to this issue: either I put "Intermediate 1" in the client's truststore, or the webserver (caddy in this case) needs to send also the public part of "Intermediate 1" along with the other two.

Why not just give Caddy the intermediate? It's already established that Caddy sends leaf and intermediate, and your issue is because you've created a disjoint in the chain by creating an additional intermediate yet serves no purpose.

https://caddyserver.com/docs/caddyfile/options#intermediate

@hslatman
Copy link
Contributor

hslatman commented Jan 10, 2025

Why not just give Caddy the intermediate? It's already established that Caddy sends leaf and intermediate, and your issue is because you've created a disjoint in the chain by creating an additional intermediate yet serves no purpose.

It does serve a purpose, namely being compatible with an existing PKI. Not all certificate chains contain just a single intermediate; two or more is a perfectly valid configuration, albeit more complex (in terms of management). In general web servers do support such configurations, and it's indeed as @hanjo describes by also sending additional intermediates required to form a chain to one (or more) trusted root(s). The longer chain still shouldn't include roots, generally, though.

@hanjo: now that you describe it as an intermediate it indeed does make sense. It's an important distinction (vs. a root), hence my initial comment.

https://caddyserver.com/docs/caddyfile/options#intermediate

I haven't tested/tried it, but I suppose it can be made to work by letting the intermediate PEM file contain multiple intermediate certificates (this would require some plumbing of the slice of x509.Certificate). If the root can be just a cert (with the key being managed outside of Caddy, which seems to be possible), then the configuration isn't that much different from what it is today. A slight variation could be to introduce an additional setting for additional intermediates to be loaded and returned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
discussion 💬 The right solution needs to be found
Projects
None yet
Development

No branches or pull requests

5 participants