Search code examples
c#sslssl-certificatepfxsslstream

Verifying the chain of a self-signed certificate when using SslStream


I have a chain.pem

-----BEGIN CERTIFICATE-----
// My server cert signed by intemediate CA
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
// My intermediate cert signed by root CA
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
// My self signed root cert
-----END CERTIFICATE-----

as well as a server.key.pem

-----BEGIN RSA PRIVATE KEY-----
// Private key for server cert
-----END RSA PRIVATE KEY-----

From there, I generate a pfx file - which has the server cert with its private key along with the rest of the chain.

openssl pkcs12 -export -out certificate.pfx -inkey server.key.pem -in chain.pem

I leave the export password blank

Next I host an TcpListener with an SslStream

namespace fun_with_ssl
{
    internal class Program
    {
        public static int Main(string[] args)
        {
            var serverCertificate = new X509Certificate2("certificate.pfx");
            var listener = new TcpListener(IPAddress.Any, 1443);
            listener.Start();

            while (true)
            {
                using (var client = listener.AcceptTcpClient())
                using (var sslStream = new SslStream(client.GetStream(), false))
                {
                    sslStream.AuthenticateAsServer(serverCertificate, false, SslProtocols.Tls12, false);
                    //send/receive from the sslStream
                }
            }
        }
    }
}

But when I try to check the chain from openssl, it fails

openssl s_client -connect 127.0.0.1:1443 -CAfile ca.cert.pem

CONNECTED(00000005)
depth=0 CN = SERVER
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = SERVER
verify error:num=21:unable to verify the first certificate
verify return:1
---
Certificate chain
 0 s:CN = SERVER
   i:CN = Intermediate
---
Server certificate
-----BEGIN CERTIFICATE-----
// My Server certificate
-----END CERTIFICATE-----
subject=CN = SERVER

issuer=CN = Intermediate

---
No client certificate CA names sent
Client Certificate Types: RSA sign, DSA sign, ECDSA sign
Requested Signature Algorithms: RSA+SHA256:RSA+SHA384:RSA+SHA1:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA1:DSA+SHA1:RSA+SHA512:ECDSA+SHA512
Shared Requested Signature Algorithms: RSA+SHA256:RSA+SHA384:RSA+SHA1:ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA1:DSA+SHA1:RSA+SHA512:ECDSA+SHA512
Peer signing digest: SHA256
Peer signature type: RSA
Server Temp Key: ECDH, P-384, 384 bits
---
SSL handshake has read 1439 bytes and written 481 bytes
Verification error: unable to verify the first certificate
---
New, TLSv1.2, Cipher is ECDHE-RSA-AES256-GCM-SHA384
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
No ALPN negotiated
SSL-Session:
    Protocol  : TLSv1.2
    Cipher    : ECDHE-RSA-AES256-GCM-SHA384
    Session-ID: E82C0000B86186D0051CFE6290C12F0D62C4D376B7E40437029B8B85687C4B18
    Session-ID-ctx:
    Master-Key: 13681EAE940F241726072A4586A96A9FEEEF29B8309B9122FA2F07AC7C9F949128CB66D0F9C430E1D2480E61E287C578
    PSK identity: None
    PSK identity hint: None
    SRP username: None
    Start Time: 1566533377
    Timeout   : 7200 (sec)
    Verify return code: 21 (unable to verify the first certificate)
    Extended master secret: yes
---
140266287337920:error:14094416:SSL routines:ssl3_read_bytes:sslv3 alert certificate unknown:../ssl/record/rec_layer_s3.c:1528:SSL alert number 46

I doesn't seem as though it is presenting the intermediate or the root certificate so that it can verify the chain. What am I missing here?

In my scenario, the client would have the root certificate public key.


Solution

    • Even if the PFX contained the entire chain, using the single-certificate constructor makes it only load the cert which had a private key, and the rest are discarded.
    • Even if the load-the-PFX-as-a-collection method is used, SslStream only uses a collection to find an acceptable server certificate (has a private key and the proper EKU value), then ignores the rest.

    The intermediate certificate is only sent when it can be found, via system ambient context, by X509Chain. If your certificate is not public-trust then the best answer for your scenario is to add the intermediate (and, optionally, the root) to the CurrentUser\CA (X509StoreName.CertificateAuthority) certificate store. The "CA" store doesn't provide trust, it's just a grab bag of all the intermediate issuer CAs the system has seen, and the system uses it as a cache when building new chains.

    You can do this programmatically at startup by

    X509Certificate2 serverCertificate = null;
    
    using (X509Store store = new X509Store(StoreName.CertificateAuthority, StoreLocation.CurrentUser))
    {
        store.Open(OpenFlags.ReadWrite);
    
        X509Certificate2Collection coll = new X509Certificate2Collection();
        coll.Import("certificate.pfx");
    
        foreach (X509Certificate2 cert in coll)
        {
            if (cert.HasPrivateKey)
            {
                // Maybe apply more complex logic if you really expect multiple private-key certs.
                if (serverCertificate == null)
                {
                    serverCertificate = cert;
                }
                else
                {
                    cert.Dispose();
                }
            }
            else
            {
                // This handles duplicates (as long as no custom properties have been applied using MMC)
                store.Add(cert);
                cert.Dispose();
            }
        }
    }
    
    // tcpListener, et al.
    

    Other options: Feed the whole collection into X509Chain.ChainPolicy.ExtraStore, call X509Chain.Build on serverCert, only add the certs after the first one (and optionally not the last one)... just depends on how much extra stuff is expected to be in the PFX.