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

RemoteCertificateValidationCallback can not detect some incomplete certificate chains #59944

Closed
stephenweaver opened this issue Oct 1, 2021 · 22 comments
Labels
area-System.Net.Security enhancement Product code improvement that does NOT require public API changes/additions
Milestone

Comments

@stephenweaver
Copy link

stephenweaver commented Oct 1, 2021

Description

It appears that RemoteCertificateValidationCallback (tested with SslStream and HttpWebRequest ) incorrectly validates some certificates with chain issues (ex. https://incomplete-chain.badssl.com). The code I'm attempting to write validates certificates, similar to what this tool is doing https://www.sslshopper.com/ssl-checker.html.

There does not appear to be a way to detect these issues, which is why this feels like a bug to me.

Here is my attempt at overriding the default behavior, but by the time my code it hit, all 3 certificates look like they exist. (https://dotnetfiddle.net/2aybuI)

using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

public class Program
{
	public static void Main(string[] args)
	{
		var request = (HttpWebRequest)WebRequest.Create("https://incomplete-chain.badssl.com");
		request.AllowAutoRedirect = false;
		request.ServerCertificateValidationCallback = ServerCertificateValidationCallback;
		var response = (HttpWebResponse)request.GetResponse();
		response.Close();
	}

	private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
	{
		if (sslPolicyErrors != SslPolicyErrors.None)
		{
			return false;
		}

		var newChain = new X509Chain();
		newChain.ChainPolicy.DisableCertificateDownloads = true;
		
		// Skip the leaf cert and stop short of the root cert.      
		X509ChainElementCollection chainElements = chain.ChainElements;
		for (int i = 1; i < chainElements.Count - 1; i++)
		{
			newChain.ChainPolicy.ExtraStore.Add(chainElements[i].Certificate);
		}
		var result = newChain.Build(chainElements[0].Certificate);
		
		// This is True and I want it to be False
		Console.WriteLine($"This is {result} and I want it to be False");
		
		return result;
	}
}

Configuration

Here is a sample app using .NET 5 (my code base is .NET core 3.1 with the same issue)

Here's a .NET Fiddle demonstrating the code above.
https://dotnetfiddle.net/2aybuI

Regression?

Tested on several versions of .NET and this bug has been around for a long time.

Other information

curl requests, for example, fail how I want my requests to fail if the cert chain is not valid.

 curl get https://incomplete-chain.badssl.com
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:--  0:00:02 --:--:--     0
curl: (6) Could not resolve host: get
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

openssl also correctly shows these incomplete-chains as invalid

$ openssl s_client -showcerts -connect incomplete-chain.badssl.com:443
CONNECTED(00000188)
---
Certificate chain
 0 s:C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com
   i:C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA
-----BEGIN CERTIFICATE-----
the certificate 
-----END CERTIFICATE-----
---
Server certificate
subject=C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com

issuer=C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA

---
No client certificate CA names sent
Peer signing digest: SHA512
Peer signature type: RSA
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 2414 bytes and written 455 bytes
Verification error: unable to verify the first certificate
---

@CarnaViire CarnaViire transferred this issue from dotnet/core Oct 4, 2021
@dotnet-issue-labeler dotnet-issue-labeler bot added area-System.Security untriaged New issue has not been triaged by the area owner labels Oct 4, 2021
@ghost
Copy link

ghost commented Oct 4, 2021

Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq, @GrabYourPitchforks
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

It appears that RemoteCertificateValidationCallback (tested with SslStream and HttpWebRequest ) incorrectly validates some certificates with chain issues (ex. https://incomplete-chain.badssl.com). The code I'm attempting to write validates certificates, similar to what this tool is doing https://www.sslshopper.com/ssl-checker.html.

There does not appear to be a way to detect these issues, which is why this feels like a bug to me.

Here is my attempt at overriding the default behavior, but by the time my code it hit, all 3 certificates look like they exist. (https://dotnetfiddle.net/2aybuI)

using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

public class Program
{
	public static void Main(string[] args)
	{
		var request = (HttpWebRequest)WebRequest.Create("https://incomplete-chain.badssl.com");
		request.AllowAutoRedirect = false;
		request.ServerCertificateValidationCallback = ServerCertificateValidationCallback;
		var response = (HttpWebResponse)request.GetResponse();
		response.Close();
	}

	private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
	{
		if (sslPolicyErrors != SslPolicyErrors.None)
		{
			return false;
		}

		var newChain = new X509Chain();
		newChain.ChainPolicy.DisableCertificateDownloads = true;
		
		// Skip the leaf cert and stop short of the root cert.      
		X509ChainElementCollection chainElements = chain.ChainElements;
		for (int i = 1; i < chainElements.Count - 1; i++)
		{
			newChain.ChainPolicy.ExtraStore.Add(chainElements[i].Certificate);
		}
		var result = newChain.Build(chainElements[0].Certificate);
		
		// This is True and I want it to be False
		Console.WriteLine($"This is {result} and I want it to be False");
		
		return result;
	}
}

Configuration

Here is a sample app using .NET 5 (my code base is .NET core 3.1 with the same issue)

Here's a .NET Fiddle demonstrating the code above.
https://dotnetfiddle.net/2aybuI

Regression?

Tested on several versions of .NET and this bug has been around for a long time.

Other information

curl requests, for example, fail how I want my requests to fail if the cert chain is not valid.

 curl get https://incomplete-chain.badssl.com
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:--  0:00:02 --:--:--     0
curl: (6) Could not resolve host: get
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

openssl also correctly shows these incomplete-chains as invalid

$ openssl s_client -showcerts -connect incomplete-chain.badssl.com:443
CONNECTED(00000188)
---
Certificate chain
 0 s:C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com
   i:C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA
-----BEGIN CERTIFICATE-----
the certificate 
-----END CERTIFICATE-----
---
Server certificate
subject=C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com

issuer=C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA

---
No client certificate CA names sent
Peer signing digest: SHA512
Peer signature type: RSA
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 2414 bytes and written 455 bytes
Verification error: unable to verify the first certificate
---

Author: stephenweaver
Assignees: -
Labels:

area-System.Security, untriaged

Milestone: -

@ghost
Copy link

ghost commented Oct 4, 2021

Tagging subscribers to this area: @dotnet/ncl, @vcsjones
See info in area-owners.md if you want to be subscribed.

Issue Details

Description

It appears that RemoteCertificateValidationCallback (tested with SslStream and HttpWebRequest ) incorrectly validates some certificates with chain issues (ex. https://incomplete-chain.badssl.com). The code I'm attempting to write validates certificates, similar to what this tool is doing https://www.sslshopper.com/ssl-checker.html.

There does not appear to be a way to detect these issues, which is why this feels like a bug to me.

Here is my attempt at overriding the default behavior, but by the time my code it hit, all 3 certificates look like they exist. (https://dotnetfiddle.net/2aybuI)

using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

public class Program
{
	public static void Main(string[] args)
	{
		var request = (HttpWebRequest)WebRequest.Create("https://incomplete-chain.badssl.com");
		request.AllowAutoRedirect = false;
		request.ServerCertificateValidationCallback = ServerCertificateValidationCallback;
		var response = (HttpWebResponse)request.GetResponse();
		response.Close();
	}

	private static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
	{
		if (sslPolicyErrors != SslPolicyErrors.None)
		{
			return false;
		}

		var newChain = new X509Chain();
		newChain.ChainPolicy.DisableCertificateDownloads = true;
		
		// Skip the leaf cert and stop short of the root cert.      
		X509ChainElementCollection chainElements = chain.ChainElements;
		for (int i = 1; i < chainElements.Count - 1; i++)
		{
			newChain.ChainPolicy.ExtraStore.Add(chainElements[i].Certificate);
		}
		var result = newChain.Build(chainElements[0].Certificate);
		
		// This is True and I want it to be False
		Console.WriteLine($"This is {result} and I want it to be False");
		
		return result;
	}
}

Configuration

Here is a sample app using .NET 5 (my code base is .NET core 3.1 with the same issue)

Here's a .NET Fiddle demonstrating the code above.
https://dotnetfiddle.net/2aybuI

Regression?

Tested on several versions of .NET and this bug has been around for a long time.

Other information

curl requests, for example, fail how I want my requests to fail if the cert chain is not valid.

 curl get https://incomplete-chain.badssl.com
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:--  0:00:02 --:--:--     0
curl: (6) Could not resolve host: get
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

openssl also correctly shows these incomplete-chains as invalid

$ openssl s_client -showcerts -connect incomplete-chain.badssl.com:443
CONNECTED(00000188)
---
Certificate chain
 0 s:C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com
   i:C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA
-----BEGIN CERTIFICATE-----
the certificate 
-----END CERTIFICATE-----
---
Server certificate
subject=C = US, ST = California, L = Walnut Creek, O = Lucas Garron Torres, CN = *.badssl.com

issuer=C = US, O = DigiCert Inc, CN = DigiCert SHA2 Secure Server CA

---
No client certificate CA names sent
Peer signing digest: SHA512
Peer signature type: RSA
Server Temp Key: ECDH, P-256, 256 bits
---
SSL handshake has read 2414 bytes and written 455 bytes
Verification error: unable to verify the first certificate
---

Author: stephenweaver
Assignees: -
Labels:

area-System.Net.Security, untriaged

Milestone: -

@bartonjs
Copy link
Member

bartonjs commented Oct 4, 2021

Tangent: Not specifically the problem you're talking about, but

if (sslPolicyErrors != SslPolicyErrors.None)
{
    return false;
}

means that if the system hadn't already filled in the holes you're not going to look at it.

@bartonjs
Copy link
Member

bartonjs commented Oct 4, 2021

.NET's certificate chains, on Windows, are built via Windows CAPI's CertGetCertificateChain . The CAPI chain engine has a lot of places that it looks for filling in holes:

  • The collection that we provide from chain.ChainPolicy.ExtraStore
  • The CurrentUser\CA (intermediate CAs) store
  • The LocalMachine\CA store
  • The CurrentUser\Root (trusted root CAs) store
  • The LocalMachine\Root store
  • The CurrentUser\ThirdPartyRoot store
    • NOT the LocalMachine\ThirdPartRoot store... for... reasons.
  • The CurrentUser\My ("Personal") store
  • The LocalMachine\My store
  • HTTP or LDAP based caIssuers URIs from the certificate's Authority Information Access extension
    • This can be disabled, CERT_CHAIN_DISABLE_AIA, which we call cert.ChainPolicy.DisableCertificateDownloads
  • Maybe a "things I've recently downloaded" cache
  • Maybe other caches

Of those, only one can be turned off.

That means that you can't really use X509Chain alone to determine this scenario. What you really want to know is "did the contents from the handshake look like the chain, except that it was missing the root?". And that means that you'd need to see what was actually put on the wire.

It might be true that chain.ChainPolicy.ExtraStore in the callback already matches what was on the handshake (order and content)... but it might also (instead) be the case that SChannel already built the chain once, filled in any missing pieces, and then we already got the normalized chain. I'm not sure. If it's the normalized chain already (and maybe "anyways"), then maybe we have a need for API exposing what was on the wire.

@stephenweaver
Copy link
Author

Hey @bartonjs , you're absolutely correct. Do you know of a way to see what was actually put on the wire? Everything I can find in the docs abstracts this away from the developer.

I don't really care if the system I'm running on is smart enough to figure out if the request safe, I need to figure out if every system/language/platform is also going come to the same conclusion. (The curl and openssl request are simple examples of this)

Yep, this is just the default behavior, and I'm cool with returning early here.

if (sslPolicyErrors != SslPolicyErrors.None)
{
    return false;
}

@bartonjs
Copy link
Member

bartonjs commented Oct 4, 2021

The one thing to investigate is if chain.ChainPolicy.ExtraStore looks like what went on the wire (after the end-entity/leaf cert). e.g. for the incomplete-chain.badssl.com test it would be empty. If so, then huzzah. If not, then there's probably no current way to get the raw form.

@stephenweaver
Copy link
Author

stephenweaver commented Oct 6, 2021

Yeah it looks like if this is the only way to access the cert info, then this currently isn't possible.

@karelz karelz added this to the 7.0.0 milestone Oct 19, 2021
@karelz karelz added enhancement Product code improvement that does NOT require public API changes/additions and removed untriaged New issue has not been triaged by the area owner labels Oct 19, 2021
@karelz
Copy link
Member

karelz commented Oct 19, 2021

Triage: Unclear if it should happen in SslStream or in X509Chain. We should try to solve it in 7.0.

@shamork
Copy link

shamork commented Dec 31, 2021

@stephenweaver

Possible work around:

  1. implement the SSL and TLS handshake, and check the cert
    ref: [TestSSLServer ](https://github.com/pornin/TestSSLServer )

  2. windows.security.cryptography.certificates.chainbuildingparameters.networkretrievalenabled
    ref: https://docs.microsoft.com/en-us/uwp/api/windows.security.cryptography.certificates.chainbuildingparameters.networkretrievalenabled?view=winrt-10240#Windows_Security_Cryptography_Certificates_ChainBuildingParameters_NetworkRetrievalEnabled

I'm working on the first one. Just failed with the ChainPolicy.DisableCertificateDownloads today.
May i should write the tool with other language ......

@shamork
Copy link

shamork commented Jan 1, 2022

@stephenweaver

Possible work around:

  1. implement the SSL and TLS handshake, and check the cert
    ref: [TestSSLServer ](https://github.com/pornin/TestSSLServer )
  2. windows.security.cryptography.certificates.chainbuildingparameters.networkretrievalenabled
    ref: https://docs.microsoft.com/en-us/uwp/api/windows.security.cryptography.certificates.chainbuildingparameters.networkretrievalenabled?view=winrt-10240#Windows_Security_Cryptography_Certificates_ChainBuildingParameters_NetworkRetrievalEnabled

I'm working on the first one. Just failed with the ChainPolicy.DisableCertificateDownloads today. May i should write the tool with other language ......

the third work around, which is proved works fine:
use js running in NodeJs to detect the incomplete-chain.badssl.com
use Jering.Javascript.NodeJS to interop with NodeJs
done

https://github.com/shamork/detect-incomplete-chain

@wfurt
Copy link
Member

wfurt commented May 4, 2022

@bartonjs is right and the chain.ChainPolicy.ExtraStore contains certificates from the wire. Since the peer should send everything but the root, it should be as simple as waling the chain and verifying that all intermediates are in the collection.

I don't think new chain or rebuild is necessary as that can still get certificates from cache - even with download disabled.

maybe I'm missing something but there seems to be no work left.

@stephenweaver
Copy link
Author

hey @wfurt, sorry if this is a dumb question, but I just want to clarify. Does "no work left" mean it's already been fixed in the dotnet 7 branch? or it should already be working in dotnet 6?

@wfurt
Copy link
Member

wfurt commented May 5, 2022

That depends on "working" means. :) The chain.ChainPolicy.ExtraStore behavior exist for a while e.g. it is in 6.0. It is not documented (and I'm not sure it should since that is somewhat internal to SslStream). If ability to determine what certificates were sent on wire is sufficient, (and I feel it should) then I see no work left.

Note, that main purpose of ServerCertificateValidationCallback is to validate certificate and gives caller ability to terminate TLS session or proceed forward. It feels like you are trying to bend it to do something else.

I'm also don't think the claim "incorrectly validates" is valid. The AIA in certificates exists for a reason so using it does not not seems incorrect. There are already asks to have some more control over the validation so I feel there is no need for another generic issue.

@stephenweaver
Copy link
Author

stephenweaver commented May 5, 2022

Okay, so it sounds like we could kinda get it work by doing this (code below). But we'd have to rely on comparing Subject and Issuer names rather than the .Build function. (personally, I still think something like the code from the original post should work 😞 )

I also noticed the order of certificates is reversed in some environments like dotnetfiddle. I'm assuming that's something dotnetfiddle is doing? and that under normal circumstances the order of the certificates is deterministic?
Example Link : https://dotnetfiddle.net/4g5FdJ

Code also below the outputs.

Local Output (works)

Certificate Subjects:
CN=GTS Root R1, O=Google Trust Services LLC, C=US
CN=GTS CA 1C3, O=Google Trust Services LLC, C=US
CN=www.google.com
This chain is valid

Certificate Subjects:
CN=*.badssl.com, O=Lucas Garron Torres, L=Walnut Creek, S=California, C=US
This chain is not valid

Dotnetfiddle Output (does not work)

Certificate Subjects:
CN=www.google.com
CN=GTS CA 1C3, O=Google Trust Services LLC, C=US
CN=GTS Root R1, O=Google Trust Services LLC, C=US
This chain is not valid

Certificate Subjects:
CN=*.badssl.com, O=Lucas Garron Torres, L=Walnut Creek, S=California, C=US
This chain is not valid

Code

using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates;

// Valid Chain
Connect("www.google.com");
// Invalid Chain
Connect("incomplete-chain.badssl.com");

static void Connect(string url)
{
    try
    {
        new SslStream(new TcpClient(url, 443).GetStream(), false,
        new RemoteCertificateValidationCallback(ServerCertificateValidationCallback))
        .AuthenticateAsClient(url);
    }
    catch { }
}

static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    if (sslPolicyErrors != SslPolicyErrors.None)
    {
        return false;
    }

    var certs = chain.ChainPolicy.ExtraStore;

    Console.WriteLine("Certificate Subjects:");
    foreach (var x in certs)
    {
        Console.WriteLine(x.Subject);
    }

    // Add root certificate
    var store = GetRootCertificates().Select(x => x.Subject).ToList();

    // Assert each parent's subject is the child's issuer
    var result = false;

    for (var i = certs.Count - 1; i >= 0; i--)
    {
        if (i > 0)
        {
            if (certs[i].Issuer != certs[i - 1].Subject)
            {
                result = false;
                break;
            }
        }

        if (store.Contains(certs[i].Issuer))
        {
            result = true;
            break;
        }
    }

    Console.WriteLine($"This chain is {(result ? "" : "not ")}valid");
    Console.WriteLine();

    return result;
}

static List<X509Certificate2> GetRootCertificates()
{
    var certificates = new List<X509Certificate2>();

    var stores = new[]
    {
        new X509Store(StoreName.Root, StoreLocation.LocalMachine),
        new X509Store(StoreName.Root, StoreLocation.CurrentUser)
    };

    foreach (var store in stores)
    {
        try
        {
            store.Open(OpenFlags.ReadOnly);

            foreach (X509Certificate2 x509 in store.Certificates)
            {
                certificates.Add(x509);
            }
        }
        finally
        {
            store.Close();
        }
    }

    return certificates;
}

@wfurt
Copy link
Member

wfurt commented May 6, 2022

There was never attempt to order the certificates anyhow. There were some platform differences (like extra leaf cert) I tried to fix in 7.0. Tls 1.3 explicitly states that receiver should make no assumption about the order

 Note: Prior to TLS 1.3, "certificate_list" ordering required each
   certificate to certify the one immediately preceding it; however,
   some implementations allowed some flexibility.  Servers sometimes
   send both a current and deprecated intermediate for transitional
   purposes, and others are simply configured incorrectly, but these
   cases can nonetheless be validated properly.  For maximum
   compatibility, all implementations SHOULD be prepared to handle
   potentially extraneous certificates and arbitrary orderings from any
   TLS version, with the exception of the end-entity certificate which
   MUST be first.

@wfurt
Copy link
Member

wfurt commented May 6, 2022

BTW here is what I was thinking. It should work on Windows and Linux. on macOS OS gives me all certificates even if there were not sent on wire. I don't think there is anything we can do about it. The .Equals is not cryptographically safe.

static bool ServerCertificateValidationCallback(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)

{
    if (sslPolicyErrors != SslPolicyErrors.None)
    {

        return false;

    }

    foreach (X509ChainElement element in chain.ChainElements)
    {
        if (certificate.Equals(element.Certificate))
        {
            Console.WriteLine("Skipping self {0}", element.Certificate.Subject);
            continue;
        }

        if (element.Certificate.Subject == element.Certificate.Issuer)
        {
            Console.WriteLine("Find root");
            break;
        }

        if (!chain.ChainPolicy.ExtraStore.Contains(element.Certificate))
        {
            Console.WriteLine("Certificate {0} was not sent on wire", element.Certificate.Subject);
            return false;
        }
    }

    Console.WriteLine("Client sent whole chain");

    return true;
}

@stephenweaver
Copy link
Author

stephenweaver commented May 7, 2022

Thanks, that information helps. I'll play around with it some more next week. Oh that's smarter..just compare all them in the chain with the ones sent from extra store... got it. We just won't break when we find the root because it could be first in the list.

@stephenweaver
Copy link
Author

So in order to detect all the chain issues, I can't find a way other than more or less what I suggested above. With the modification being accounting for certs in any order.

@wfurt
Copy link
Member

wfurt commented May 11, 2022

I'm not sure I fully understand. You still have the original chain to detect issues. I assumed this issue is primarily about ability to know if peer sent the certificate as it should or if they come from caching/AIA/local store.

I'm not really sure what we could/should do here. The validation difference with other implementations was also noted in #55322 but I don't think we would change that. We may add know to disable AIA on client but if the complete chain can be constructed, SslStream will use it.

@stephenweaver
Copy link
Author

stephenweaver commented May 11, 2022

Yeah, that's correct. We just need to be able to detect if a certificate in the chain isn't being sent, but should be sent.

Some websites' root certificate is signed by a trusted CA, but that trusted issuer certificate is not actually sent. This appears to be a valid use case and the main reason we can't ONLY check to see if each certificate in the chain is also in the ExtraStore (as was suggested in the code snippet you provided). Some example of sites I found that do this are netflix.com, salesforce.com, icann.org, oracle.com, mysql.com.

I understand if you need to close this as functioning as intended. I get that this was never really the intended use case, but if all the other tools (ex: ssl checker, curl, node, etc) see a certificate chain (ex: https://incomplete-chain.badssl.com) as invalid, it would be nice for dotnet to have the same functionality build in.

With that being said, the ExtraStore property does provide us with a workaround, so I'm content enough with that.

@wfurt
Copy link
Member

wfurt commented May 11, 2022

The trusted root should not be sent. You either have root in your trust or you won't be able to validate anyway. Sending root CA does not really help and it is only waste. It should be valid to send it but the sample code I posted simply ignores root.

For TLS 1.2

   certificate_list
      This is a sequence (chain) of certificates.  The sender's
      certificate MUST come first in the list.  Each following
      certificate MUST directly certify the one preceding it.  Because
      certificate validation requires that root keys be distributed
      independently, the self-signed certificate that specifies the root
      certificate authority MAY be omitted from the chain, under the
      assumption that the remote end must already possess it in order to
      validate it in any case.

TLS 1.3 only adds note that order should not mater.

As far as the link: It clearly states The certificate chain sent by this site is missing an intermediate certificate. This will cause a certificate error unless the browser already has the intermediate certificate in a cache or implements [AIA fetching](https://tools.ietf.org/html/rfc3280#section-4.2.2.1).

I agree that adding control for AIA would be useful. That is already tracked by #59979.

@wfurt
Copy link
Member

wfurt commented Jul 15, 2022

Client and server now have option to pass in X509ChainPolicy via CertificateChainPolicy in ssl options. If present, chain constructed inside SslStream will respect the setting including DisableCertificateDownloads. I think the chain would still build if intermediates are already cached and available in the system but that is as close as we can get IMHO.

@wfurt wfurt closed this as completed Jul 15, 2022
@ghost ghost locked as resolved and limited conversation to collaborators Aug 15, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-System.Net.Security enhancement Product code improvement that does NOT require public API changes/additions
Projects
None yet
Development

No branches or pull requests

6 participants