C# SSL/TLS Client/Server App Remote Certificate Chain Errors

Viewed 796

Alright so I know that this question has been asked many times but somehow I wasn't able to find the correct answer that actually works. It is most likely due to my lack of experience/knowledge with the X.509 cert management, so I apologize in advance if I'm posting a duplicate.

Here's the deal. I've googled now for 3-4 days, tried various of things, even studied https://www.rfc-editor.org/rfc/rfc5280 for a while, went over https://docs.microsoft.com/en-us/powershell/module/pkiclient/new-selfsignedcertificate?view=win10-ps documentation & related links, tried so many combinations but non seemed to work, and I'm pretty sure I'm missing some minor details that slipped through.

Essentially I have a very small TCP client/server app written in C# (.NET Framework 4.7). I strongly believe the server side is working properly, however, the client side seems to reject the certificate sent from the server upon validation. I will jump into the code.

Server side logic:

var pfxCert = new X509Certificate2(@"C:\...\Cert-EndUser.pfx", "password-2");
var listener = new TcpListener(IPAddress.Any, 20482);
listener.Start();
var client = await Listener.AcceptTcpClientAsync();
var sslStream = new SslStream(client.GetStream(), leaveInnerStreamOpen: false);
sslStream.AuthenticateAsServer
(
    pfxCert,
    clientCertificateRequired: false,
    SslProtocols.Tls12,
    checkCertificateRevocation: true
);

Client side logic:

string serverName = @"www.my-domain.com";   // works, tested
string ip = "111.11.111.11";                // works, tested (obviously this is just dummy ip)

TcpClient client = new TcpClient();
client.Connect(IPAddress.Parse(ip), 20482);

SslStream sslStream = new SslStream
(
    client.GetStream(),
    leaveInnerStreamOpen: false,
    new RemoteCertificateValidationCallback(ValidateServerCertificate),
    userCertificateSelectionCallback: null
);

try
{
    sslStream.AuthenticateAsClient
    (
        serverName,
        clientCertificates: null,
        SslProtocols.Tls12,
        checkCertificateRevocation: true
    );
}
catch (AuthenticationException e)
{
    System.Windows.Forms.MessageBox.Show($"Exception: {e.Message}");
    client.Close();
    return;
}

Server cert validation function (for now):

public static bool ValidateServerCertificate
(
    object sender,
    X509Certificate certificate,
    X509Chain chain,
    SslPolicyErrors sslPolicyErrors
)
{
    // sslPolicyErrors equals to SslPolicyErrors.RemoteCertificateChainErrors here
    if (sslPolicyErrors == SslPolicyErrors.None)
        return true;
    return false;
}

Okay, as for the certificates, I'm just trying to make my own testing certificates just to have some base setup. Essentially, I am using windows powershell to make some (with New-SelfSignedCertificate command). Firstly I create a "root CA" certificate (let's call it "Cert-CA") which is self signed, then I create another certificate (called "Cert-EndUser" - the one im using in the server logic) which is signed by the Cert-CA. Then I install the Cert-CA certificate on the client side in the Trusted Root CA folder. I even tried to do it via code like this (although manual installation had no different behavior, and this code worked the same as if I would install it manually):

using (var cer = new X509Certificate2(@"Cert-CA.cer"))
using (var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser))
{
    store.Open(OpenFlags.ReadWrite);
    store.Add(cer);
    store.Close();
}

So in a nutshell, Cert-CA is installed on the client machine, and Cert-EndUser is used in the communication (server uses it obviously), but Cert-EndUser is signed by Cert-CA, so that client side app can validate it. I can provide the powershell scripts I wrote which create both certificates:

Cert-CA creation (both .pfx + .cer):

### [S] EDITABLE SECTION ###

$Params = @{
    Type = "Custom"
    Provider = "Microsoft Enhanced Cryptographic Provider v1.0"
    Subject = "C=..., ST=..., L=..., O=MyOrg, OU=OrgUnit, CN=MyOrg Trusted Root CA"
    DnsName = "www.my-domain.com"
    FriendlyName = "MyOrg-CA"
    HashAlgorithm = "SHA512"
    KeyAlgorithm = "RSA"
    KeyLength = 4096
    NotAfter = (Get-Date).AddYears(10)
    KeySpec = "Signature"
    KeyUsage = "CertSign","CRLSign","DigitalSignature"
    KeyUsageProperty = "Sign"
    TextExtension = @("2.5.29.19={critical}{text}cA=true")
    CertStoreLocation = "Cert:\CurrentUser\My"
}

$CertName = "Cert-CA"
$Password = "password-1"

### [E] EDITABLE SECTION ###

$CurrentDir = Get-Location
Set-Location Cert:\CurrentUser\My

$Cert = New-SelfSignedCertificate @Params
$CertLoc = "Cert:\CurrentUser\My\$($Cert.Thumbprint)"

$pwd = ConvertTo-SecureString -String $Password -Force -AsPlainText
$pfx = "$($CurrentDir)\$($CertName).pfx"
$cer = "$($CurrentDir)\$($CertName).cer"

Export-PfxCertificate -Cert $CertLoc -FilePath $pfx -Password $pwd
Export-Certificate -Cert $Cert -FilePath $cer

Cert-EndUser creation (both .pfx + .cer):

### [S] EDITABLE SECTION ###

$Params = @{
    Type = "SSLServerAuthentication","CodeSigningCert"
    Provider = "Microsoft Enhanced Cryptographic Provider v1.0"
    Subject = "C=.., ST=.., L=.., O=MyOrg, OU=Administration, CN=MyOrg Administration"
    DnsName = "www.my-domain.com"
    FriendlyName = "MyOrg"
    HashAlgorithm = "SHA512"
    KeyAlgorithm = "RSA"
    KeyLength = 4096
    NotAfter = (Get-Date).AddYears(2)
    KeySpec = "KeyExchange"
    KeyUsage = "DataEncipherment","DigitalSignature","KeyAgreement","KeyEncipherment"
    KeyUsageProperty = "KeyAgreement","Decrypt"
    TextExtension = @("2.5.29.19={text}cA=false", "2.5.29.37={text}1.3.6.1.5.5.7.3.2,1.3.6.1.5.5.7.3.1,1.3.6.1.5.5.7.3.4,1.3.6.1.5.5.7.3.3,1.3.6.1.5.5.7.3.8")
    CertStoreLocation = "Cert:\CurrentUser\My"
}

$CertName = "Cert-EndUser"
$Password = "password-2" # the one also used in server logic

$SignerThumb = "THUMBPRINT_OF_CERT-CA"

### [E] EDITABLE SECTION ###

$CurrentDir = Get-Location
Set-Location Cert:\CurrentUser\My

$Params.Signer = Get-ChildItem -Path "Cert:\CurrentUser\My\$($SignerThumb)"

$Cert = New-SelfSignedCertificate @Params
$CertLoc = "Cert:\CurrentUser\My\$($Cert.Thumbprint)"

$pwd = ConvertTo-SecureString -String $Password -Force -AsPlainText
$pfx = "$($CurrentDir)\$($CertName).pfx"
$cer = "$($CurrentDir)\$($CertName).cer"

Export-PfxCertificate -Cert $CertLoc -FilePath $pfx -Password $pwd
Export-Certificate -Cert $Cert -FilePath $cer

As I stated, server side seems to work properly, but on clinet side, validation fails (and I obviously don't want to use that "fix" where they just return true in ValidateServerCertificate callback and turn the security off, in that case I'd use plain old Sockets and be done with it).

What am I missing?

0 Answers
Related