Skip to content

[certs] Full chain support followup #43193

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

Open
HaoK opened this issue Aug 10, 2022 · 32 comments
Open

[certs] Full chain support followup #43193

HaoK opened this issue Aug 10, 2022 · 32 comments
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions bug This issue describes a behavior which is not expected - a bug. feature-kestrel

Comments

@HaoK
Copy link
Member

HaoK commented Aug 10, 2022

See #41944 (comment)

Some followup questions/work:

from @bartonjs

In this if block, you could just make certificate be fullChain[0] (assuming it's non-empty), and then you could remove certificate from the collection; depending on what public API guarantees you're making.

The two halves of that sentence are:

We've already done the work of loading the certificate instance, why go open the file and process the contents a second time?
The perf of a chain build will be marginally faster if you remove unnecessary elements. Since the target certificate is already specified as the target it won't need to be found from the collection. By removing it early you save a "nope, not the one I'm looking for, next!"
If you don't remove it, because you want the "full" chain to be in the HttpsOptions.ServerCertificateChain collection, then you have to decide if you want to have the same instance in the property and the collection. If "yes" to the full chain and "no" to the same instance... then leave the code as-is 😄.

And the reason I put "full" in quotes is that I don't think Let's Encrypt puts their root cert in that file, so the "full" chain is really "the chain except the root". But it's as "full" as that file is, I suppose.

From @davidfowl

I did a bunch of research here a year ago and I can dig up my notes. When you get the certs from certbot for lets encrypt you can get both the fullchain.pem or the chain.pem + cert.pem (https://community.letsencrypt.org/t/generating-cert-pem-chain-pem-and-fullchain-pem-from-order-certificate/78376/6).

We should also support loading the full chai from PFX files if it is there. NGINX supports the full chain as well (https://serverfault.com/questions/987612/nginx-ssl-config-for-cert-pem-and-chain-pem).

I'd like us to support both providing just the intermediates without the leaf cert and the full chain and we can remove the leaf cert (assuming this is easy). That makes us a bit more friendly.

@HaoK HaoK added this to the 7.0-rc2 milestone Aug 10, 2022
@HaoK HaoK self-assigned this Aug 10, 2022
@davidfowl
Copy link
Member

I think you're doing the right things in the PR but we need to test (in this follow up) a PFX file with the full chain.

@Pilchie Pilchie added the area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer label Aug 11, 2022
@davidfowl
Copy link
Member

davidfowl commented Aug 31, 2022

This is what we want to solve, this exact problem https://stackoverflow.com/questions/65926215/kestrel-fails-tls-handshake-after-attempt-to-download-intermediate-certificate-f

I can't figure out why it's attempting to download this certificate, considering the same certificate is embedded in the PKCS#12 PFX bundle that is bound to Kestrel. Am I supposed to load either the root CA or intermediate into the CA trust folder in file system? (Ex. /usr/local/share/ca-certificates/ - I can't load the intermediate there, only the CA?)

@bartonjs Can you help with the exact code we need to load the full chain from a pfx file?

@bartonjs
Copy link
Member

Loading a PFX as a collection is easy:

X509Certificate2Collection coll = new X509Certificate2Collection();
coll.Import(pfx, pwd, flags);

(instead of new X509Certificate2(pfx, pwd, flags)).

But that's just an arbitrary collection. The cert that the single-cert constructor would pick is either the first or last one that has cert.HasPrivateKey:true (there's a stack/reversal somewhere, so I don't remember off the top of my head if it's first or last in the collection ordering). If there aren't any then a) it won't work for your scenarios and b) the ctor would then fall back to the first (or last) cert. (If the collection is empty then the single cert ctor would throw at this point).

So let's go with "Single":

X509Certificate2 target = coll.Single(c => c.HasPrivateKey);
coll.Remove(target);

You can now assume that the remainder of coll is related to the certificate and is part of the chain... or throw the collection into X509ChainPolicy.ExtraStore and build an X509Chain to provably trim it down. Assuming you're just going to pass it into SslStreamCertificateContext, it'll do that for you, so Single + Remove is probably all you need.

@HaoK
Copy link
Member Author

HaoK commented Aug 31, 2022

So just to make sure I understand, we think the code is fine as is, I just need to add a test that loads the PFX into ServerCertificateChain using the above:

X509Certificate2Collection coll = new X509Certificate2Collection();
coll.Import(pfx, pwd, flags);
X509Certificate2 target = coll.Single(c => c.HasPrivateKey);
coll.Remove(target);
// HttpsConnectionAdapterOptions.ServerCertificateChain = coll, and verify that X509ChainPolicy.ExtraStore contains the certs in coll

@HaoK
Copy link
Member Author

HaoK commented Sep 1, 2022

Note, it looks like the test already is testing this format where clientCertificate which has the privateKey is not part of the clientChain. So should I add a test in the other direction where clientChain is included in the clientChain as well and verify thing s?

    public async Task ServerCertificateChainInExtraStore()
    {
        var streams = new List<SslStream>();
        CertHelper.CleanupCertificates(nameof(ServerCertificateChainInExtraStore));
        // clientCertificate hasPrivateKey = true and is not part of clientChain
        (var clientCertificate, var clientChain) = CertHelper.GenerateCertificates(nameof(ServerCertificateChainInExtraStore), longChain: true, serverCertificate: false);

@davidfowl
Copy link
Member

@HaoK it seems like we need to always import the collection when we get a pfx. We don’t do this today do we?

@HaoK
Copy link
Member Author

HaoK commented Sep 1, 2022

I'm not sure I understand since the HttpsConnectionAdapterOptions.ServerCertificateChain property is a X509Certificate2Collection, so if they need to import something from a pfx, they can just do this code themselves right?

Or am I misunderstanding the ask, are we supposed to detect if the cert file is a pfx chain and do stuff automatically to load it into the SslStreamCertificateContext?

@bartonjs
Copy link
Member

bartonjs commented Sep 1, 2022

If no keypath value is specified then you load certinfo.Path/certinfo.Password at

https://github.com/dotnet/aspnetcore/blob/main/src/Servers/Kestrel/Core/src/Internal/Certificates/CertificateConfigLoader.cs#L74

The target scenario there is, presumably, that certinfo.Path is specifying a PFX. My understanding of David's comments are that he would like that PFX to be loaded as a collection, so something similar to

fullChain.Import(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path!), certInfo.Password);
X509Certificate2 target = fullChain.Single(c => c.HasPrivateKey);
fullChain.Remove(target);
return (target, fullChain);

Depending on how you want/allow PFX loads to combine with a chain-PEM file you might want to clear the collection before doing the Import.

@HaoK
Copy link
Member Author

HaoK commented Sep 2, 2022

Ah okay, that makes sense, thanks for the clarification @bartonjs I'll start on the PR now

@HaoK
Copy link
Member Author

HaoK commented Sep 2, 2022

We discussed this in triage, and given that this is no longer a test only change, and is an incremental improvement, we felt like this is a better fit for 8.0 rather than trying to get this in for rc2.

@davidfowl
Copy link
Member

Why? Isn’t it a simple change? I consider it part of the overall fix to support full certificate chains

@HaoK
Copy link
Member Author

HaoK commented Sep 2, 2022

Well, the current proposal for a change from @bartonjs is not what I would consider simple in the PR and we have no test coverage for the code (the tests mock this interface to supply the certs directly to the tests), which means we'd be have to rely on manual testing code or build a reasonable set of tests as part of this PR at the same time. It seems a bit of a stretch to say this meets the rc2 bar was our main concern in triage

@davidfowl
Copy link
Member

What happens when you put the same pfx file as both the leaf cert and the chain?

@bartonjs
Copy link
Member

bartonjs commented Sep 3, 2022

Hm, looking at the code, I'm not sure a PFX will work at all...

else if (certInfo.IsFileCert)
{
var certificatePath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path!);
var fullChain = new X509Certificate2Collection();
fullChain.ImportFromPemFile(certificatePath);
if (certInfo.KeyPath != null)

The first thing that it does with the cert-as-a-file is load it as a multi-pem. If it's a PFX that'll throw.

If certInfo.Path is supposed to be able to be a PFX there's trouble here.

@davidfowl
Copy link
Member

Hmm I think this is a regression then.

@HaoK
Copy link
Member Author

HaoK commented Sep 5, 2022

That's not good, so we might have broken pfx entirely? I guess we just need a variant of this for pfx certs CanReadAndWriteWithHttpsConnectionMiddlewareWithPemCertificate() test:

public async Task CanReadAndWriteWithHttpsConnectionMiddlewareWithPemCertificate()
with a pfx to make sure that works, I'll try that on Tuesday

@davidfowl
Copy link
Member

I was thinking about this and hoping that we have coverage for this at least 😄. Thanks @HaoK !

@HaoK
Copy link
Member Author

HaoK commented Sep 6, 2022

Well, I copied the CanReadAndWriteWithHttpsConnectionMiddlewareWithPemCertificate() test and swapped in a pfx, the good news is that the original code we had works, and it still works with @bartonjs new code too.

The new test is here: https://github.com/dotnet/aspnetcore/pull/43719/files#diff-b50fac2e180288a9c83f5636d8d2e03f9d09b05b96051c056857f95a3417110bR103

So I'm not sure whether that means we should take the change or not for rc2?

@davidfowl
Copy link
Member

Can you also verify it in an running app outside of this repo? If yes then we can skip for RC2 yeah

@HaoK
Copy link
Member Author

HaoK commented Sep 9, 2022

So I verified that I could create a new rc2 app and pointed it at once of our test pfx files and it would startup fine via dotnet run:

    "Certificates": {
      "Default": {
        "Path": "C:\\Github\\aspnetcore\\src\\Servers\\Kestrel\\shared\\test\\TestCertificates\\aspnetdevcert.pfx",
        "Password": "pwd"
      }
    },

Verified that the wrong password would throw, and the right password would start up fine. Is this what you wanted me to double check?

@davidfowl
Copy link
Member

Yep!

@erichiller
Copy link

Just checking, is the plan for this fix to be in .net7-rc2 ? I just spent a long time trying to figure this out before coming across this issue. It is quite confusing that chains work with pem and not with pfx.

@HaoK
Copy link
Member Author

HaoK commented Oct 7, 2022

No, its too late for this to get into 7.0, the plan is for this to go into 8.0. Just to confirm what you are seeing on 7.0, a pfx chain won't work, but you aren't having issues using non chain pfx certs

@erichiller
Copy link

No, its too late for this to get into 7.0, the plan is for this to go into 8.0. Just to confirm what you are seeing on 7.0, a pfx chain won't work, but you aren't having issues using non chain pfx certs

Correct

However, I just found one interesting thing, if I have a chain-containing pem certificate file and load it in DefaultCertificates, then the chaining does not work. Exact same configuration properties copied to an endpoint and it does work.

Chaining does work in this configuration

{
    "Kestrel": {
        "Endpoints": {
            "Https": {
                "Url": "https://*:5007",
                "Certificate": { // here chaining does work 
                    "Path": "./cert.pem",
                    "KeyPath": "./key.pem"
                }
            }
        }
    }
}

Chaining doesn't work in this configuration

{
    "Kestrel": {
        "Endpoints": {
            "Https": {
                "Url": "https://*:5007",
            }
        },
        "Certificates": { 
            "Default": {
                "Path": "./cert.pem",
                "KeyPath": "./key.pem"
            }
        }
    }
}

@HaoK
Copy link
Member Author

HaoK commented Oct 14, 2022

Thanks for that report @erichiller seems like a bug

@HaoK HaoK removed their assignment Nov 16, 2022
@mkArtakMSFT mkArtakMSFT added the bug This issue describes a behavior which is not expected - a bug. label Feb 2, 2023
@ghost
Copy link

ghost commented Feb 2, 2023

Thanks for contacting us.

We're moving this issue to the .NET 8 Planning milestone for future evaluation / consideration. We would like to keep this around to collect more feedback, which can help us with prioritizing this work. We will re-evaluate this issue, during our next planning meeting(s).
If we later determine, that the issue has no community involvement, or it's very rare and low-impact issue, we will close it - so that the team can focus on more important and high impact issues.
To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

@mkArtakMSFT mkArtakMSFT added area-runtime and removed area-auth Includes: Authn, Authz, OAuth, OIDC, Bearer labels Feb 2, 2023
@amcasey amcasey self-assigned this May 30, 2023
@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Jun 2, 2023
@msoler8785
Copy link

The workaround described by @erichiller doesn't seem to be working for me on 8.0.1.

@amcasey
Copy link
Member

amcasey commented Feb 6, 2024

The workaround described by @erichiller doesn't seem to be working for me on 8.0.1.

Did it formerly work for you and stop working in 8.0.1 or are you just providing details about what you tried?

@msoler8785
Copy link

msoler8785 commented Feb 6, 2024

The workaround described by @erichiller doesn't seem to be working for me on 8.0.1.

Did it formerly work for you and stop working in 8.0.1 or are you just providing details about what you tried?

I'm just posting details about what I have tried and 8.0.1 seems to be the latest mainline release. I have tried this on older versions as well and nothing seems to work.

@LiamLamb
Copy link

LiamLamb commented Feb 6, 2024

The workaround described by @erichiller doesn't seem to be working for me on 8.0.1.

Did it formerly work for you and stop working in 8.0.1 or are you just providing details about what you tried?

I'm just posting details about what I have tried and 8.0.1 seems to be the latest mainline release. I have tried this on older versions as well and nothing seems to work.

I've had the same experience unfortunately. I've tried several workarounds, like the one mentioned above on related GitHub issues with no success.

It's a bit unclear to me what the plan is with this and when we can expect a fix. For reference, I am using PKCS12 certificates for both client and server.

I am a bit surprised that this is an issue in a release as mature as .NET 8.0, given it's a pretty important part of TLS as far as I understand 🫤.

I am also surprised this doesn't have more attention from the community in that regard, as surely many people have stumbled on this when setting up their PKI between .NET apps?

@amcasey
Copy link
Member

amcasey commented Feb 8, 2024

It's a bit unclear to me what the plan is with this and when we can expect a fix. For reference, I am using PKCS12 certificates for both client and server.

I apologize for the lack of activity. We've had some team changes reducing our expertise in this area and, obviously, this is the sort of thing you only want to change very carefully and thoroughly. It hasn't fallen through the cracks, but I also can't promise near-term progress.

@amcasey amcasey removed their assignment Oct 25, 2024
@samsp-msft samsp-msft changed the title Full chain support followup [certs] Full chain support followup Feb 9, 2025
@rmmason
Copy link

rmmason commented Apr 15, 2025

I have just wasted a couple of days trying to figure out my issues with this. In the end I came across #52511 which detailed exactly what I was seeing and a workaround.

I'm not sure why fixing these issues are taking so long when this should be considered core basic behaviour that should just work?

Maybe you consider at least reopening #52511 and getting this solved if this is easy compared to the wider changes you are planning?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions bug This issue describes a behavior which is not expected - a bug. feature-kestrel
Projects
None yet
Development

No branches or pull requests

10 participants