SSL Client Authentication with certificate using c#
Asked Answered
S

2

8

I need to create a c# application that has to send API request to a server using SSL. I need to create the Client authentication. I already have the server CA certificate, the client certificate (cer), the client private key (pem) and passphrase. I can't find an example on how to create the client connection. Can someone suggest me where to start with a small code well explained? In my hand I have the client certificate (PEM), the Client Provate Key and the Passphrase for the Client Key. I have no idea where to start to write the code to send a request to the server

Slipcase answered 29/10, 2020 at 7:29 Comment(5)
You should be using for SSL either TLS 1.2/1.3. Nothing special is done for the connection. The TLS authentication is automatically performed when the URL contains HTTPS before the HTTP request is made. So you can use any http client. In some cases you have to specifically add one instruction to specify the TLS version if you operating system doesn't automatically try 1.2/1.3.Thrawn
Take a look here.Otway
Or here https://mcmap.net/q/223082/-add-client-certificate-to-net-core-httpclientBently
Hi Lorenzo, concerning the client private key, are you trying to sign the data that you wish to send via HTTPS to the server? And is the CA certificate self-signed and was it used to create both the X509 CA certificate as well as the client certificate?Cluck
@Cluck yes the CA certificate is self signed and used to create the other certificates. The data are signedSlipcase
J
5

Some time ago I've created this POC for client authentication with certificate in .Net Core. It uses idunno.Authentication package that is now build-in in .Net Core. My POC probably is bit outdated now, but it can be a good starting point for you.

First create an extension method to add certificate to HttpClientHandler:

public static class HttpClientHandlerExtensions
{
    public static HttpClientHandler AddClientCertificate(this HttpClientHandler handler, X509Certificate2 certificate)
    {
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        handler.ClientCertificates.Add(certificate);

        return handler;
    }
}

Then another extension method to add certificate to IHttpClientBuilder

    public static IHttpClientBuilder AddClientCertificate(this IHttpClientBuilder httpClientBuilder, X509Certificate2 certificate)
    {
        httpClientBuilder.ConfigureHttpMessageHandlerBuilder(builder =>
        {
            if (builder.PrimaryHandler is HttpClientHandler handler)
            {
                handler.AddClientCertificate(certificate);
            }
            else
            {
                throw new InvalidOperationException($"Only {typeof(HttpClientHandler).FullName} handler type is supported. Actual type: {builder.PrimaryHandler.GetType().FullName}");
            }
        });

        return httpClientBuilder;
    }

Then load the certificate and register HttpClient in HttpClientFactory

        var cert = CertificateFinder.FindBySubject("your-subject");
        services
            .AddHttpClient("ClientWithCertificate", client => { client.BaseAddress = new Uri(ServerUrl); })
            .AddClientCertificate(cert);

Now when you will use client created by factory it will automatically send your certificate with the request;

public async Task SendRequest()
{
    var client = _httpClientFactory.CreateClient("ClientWithCertificate");
    ....
}
Juratory answered 14/11, 2020 at 20:19 Comment(3)
I will check soon!Slipcase
This (and next) section in official docs shows how to send certificate with request, all previous sections describe how to configure server side to require and validate it.Juratory
I will check in a couple of days if possible and if it works I will thank youSlipcase
C
3

There are A LOT of options here, so I am not 100% sure which way to go based on the succinctness of the question. I created a basic aspnet.core WebApi project which has the "weather forecast" controller as a test. There is a lot of error checking not shown here, and there are a lot of assumptions about how keys and certificates are or are not being stored, or even what OS this is intended for (not that the OS matters as much, but the key stores are different).

Also note that certificates created with OpenSsl do not contain the private key in the certificate for the web server. You'd have to combine the certificate and private key into a Pkcs12/PFX format for that.

For example (for the web server, not necessarily the client, but you could use the PFX anywhere really...).

openssl pkcs12 -export -out so-selfsigned-ca-root-x509.pfx -inkey so-root-ca-rsa-private-key.pem -in so-selfsigned-ca-root-x509.pem

Consider this Main method in a console app. The only non-BCL package I added to this (for the private PEM key) was Portable.BouncyCastle. If you are using .NET Core 5.0 (released just a few days ago) there are PEM options there. Assuming you are not there yet, this sample is using NetCoreApp 3.1.

The appSettings.json example file:
{
  "HttpClientRsaArtifacts": {
    "ClientCertificateFilename": "so-x509-client-cert.pem",
    "ClientPrivateKeyFilename": "so-client-private-key.pem"
  }
}


private static async Task Main(string[] args)
{
    IConfiguration config = new ConfigurationBuilder().AddJsonFile("appSettings.json").Build();

    const string mainAppSettingsKey = "HttpClientRsaArtifacts";
    var clientCertificateFileName = config[$"{mainAppSettingsKey}:ClientCertificateFilename"];
    var clientPrivKeyFileName = config[$"{mainAppSettingsKey}:ClientPrivateKeyFilename"];

    var clientCertificate = new X509Certificate2(clientCertificateFileName);
    var httpClientHandler = new HttpClientHandler();
    httpClientHandler.ClientCertificates.Add(clientCertificate);
    httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
    httpClientHandler.ServerCertificateCustomValidationCallback = ByPassCertErrorsForTestPurposesDoNotDoThisInTheWild;
    httpClientHandler.CheckCertificateRevocationList = false;

    var httpClient = new HttpClient(httpClientHandler);
    httpClient.BaseAddress = new Uri("https://localhost:5001/");

    var httpRequestMessage = new HttpRequestMessage(
        HttpMethod.Get,
        "weatherforecast");

    // This is "the connection" (and API call)
    using var response = await httpClient.SendAsync(
        httpRequestMessage,
        HttpCompletionOption.ResponseHeadersRead);

    var stream = await response.Content.ReadAsStreamAsync();
    var jsonDocument = await JsonDocument.ParseAsync(stream);

    var options = new JsonSerializerOptions
    {
        WriteIndented = true,
    };

    Console.WriteLine(
        JsonSerializer.Serialize(
            jsonDocument,
            options));
}


private static bool ByPassCertErrorsForTestPurposesDoNotDoThisInTheWild(
    HttpRequestMessage httpRequestMsg,
    X509Certificate2 certificate,
    X509Chain x509Chain,
    SslPolicyErrors policyErrors)
{
    var certificateIsTestCert = certificate.Subject.Equals("O=Internet Widgits Pty Ltd, S=Silicon Valley, C=US");

    return certificateIsTestCert && x509Chain.ChainElements.Count == 1 &&
           x509Chain.ChainStatus[0].Status == X509ChainStatusFlags.UntrustedRoot;
}

If you want to load a private key from a PEM file, you can use Bouncy Castle to easily do that. For example, to import a private key from a PEM file and then use it to create an RSA instance for signing data or a hash, you can get the RSA instance like this:

private static RSA LoadClientPrivateKeyFromPemFile(string clientPrivateKeyFileName)
{
    if (!File.Exists(clientPrivateKeyFileName))
    {
        throw new FileNotFoundException(
            "The client private key PEM file could not be found",
            clientPrivateKeyFileName);
    }

    var clientPrivateKeyPemText = File.ReadAllText(clientPrivateKeyFileName);
    using var reader = new StringReader(clientPrivateKeyPemText);

    var pemReader = new PemReader(reader);
    var keyParam = pemReader.ReadObject();

    // GET THE PRIVATE KEY PARAMETERS
    RsaPrivateCrtKeyParameters privateKeyParams = null;

    // This is the case if the PEM file has is a "traditional" RSA PKCS#1 content
    // The private key file with begin and end with -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY-----
    if (keyParam is AsymmetricCipherKeyPair asymmetricCipherKeyPair)
    {
        privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricCipherKeyPair.Private;
    }

    // This is to check if it is a Pkcs#8 PRIVATE KEY ONLY or a public key (-----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----)
    if (keyParam is AsymmetricKeyParameter asymmetricKeyParameter)
    {
        privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter;
    }

    var rsaPrivateKeyParameters = DotNetUtilities.ToRSAParameters(privateKeyParams);

    // CREATE A NEW RSA INSTANCE WITH THE PRIVATE KEY PARAMETERS (THIS IS THE PRIVATE KEY)
    return RSA.Create(rsaPrivateKeyParameters);
}

Lastly, if you want to sign data with the private key (obtained using the sample above from a PEM file), you can use the standard encryption and signing methods on the System.Security.Cryptography.RSA class. E.g.

var signedData = rsaInstanceWithPrivateKey.SignData(
    data,
    HashAlgorithmName.SHA256,
    RSASignaturePadding.Pkcs1);

...and then add that to an HttpRequestMessage as ByteArrayContent before invoking SendAsync with the HttpRequestMessage.

var byteArrayContent = new ByteArrayContent(signedData);

var httpRequestMessage = new HttpRequestMessage(
    HttpMethod.Post,
    "/myapiuri");

httpRequestMessage.Content = byteArrayContent;

You had mentioned that you used the same private key to create everything, so if that is the case on the webserver side you'd be able to verify the signature and and decrypt what you sent from the client in this example.

Again, there are a lot of options and nuances here.

With the Bouncy Castle PEM reader you can inject an IPasswordFinder implementation with the password.

For example:

/// <summary>
/// Required when using the Bouncy Castle PEM reader for PEM artifacts with passwords.
/// </summary>
class BcPemPasswordFinder : IPasswordFinder
{
    private readonly string m_password;

    public BcPemPasswordFinder(string password)
    {
        m_password = password;
    }

    /// <summary>
    /// Required by the IPasswordFinder interface
    /// </summary>
    /// <returns>System.Char[].</returns>
    public char[] GetPassword()
    {
        return m_password.ToCharArray();
    }
}

Here is a modified version of the LoadClientPrivateKeyFromPemFile I originally posted (the password is hard-coded for brevity in this example) where you can inject the IPasswordFinder into the instance.

private static RSA LoadClientPrivateKeyFromPemFile(string clientPrivateKeyFileName)
{
    if (!File.Exists(clientPrivateKeyFileName))
    {
        throw new FileNotFoundException(
            "The client private key PEM file could not be found",
            clientPrivateKeyFileName);
    }

    var clientPrivateKeyPemText = File.ReadAllText(clientPrivateKeyFileName);
    using var reader = new StringReader(clientPrivateKeyPemText);

    // Instantiate password finder here
    var passwordFinder = new BcPemPasswordFinder("P@ssword");

    // Pass the IPasswordFinder instance into the PEM PemReader...
    var pemReader = new PemReader(reader, passwordFinder);
    var keyParam = pemReader.ReadObject();

    // GET THE PRIVATE KEY PARAMETERS
    RsaPrivateCrtKeyParameters privateKeyParams = null;

    // This is the case if the PEM file has is a "traditional" RSA PKCS#1 content
    // The private key file with begin and end with -----BEGIN RSA PRIVATE KEY----- and -----END RSA PRIVATE KEY-----
    if (keyParam is AsymmetricCipherKeyPair asymmetricCipherKeyPair)
    {
        privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricCipherKeyPair.Private;
    }

    // This is to check if it is a Pkcs#8 PRIVATE KEY ONLY or a public key (-----BEGIN PRIVATE KEY----- and -----END PRIVATE KEY-----)
    if (keyParam is AsymmetricKeyParameter asymmetricKeyParameter)
    {
        privateKeyParams = (RsaPrivateCrtKeyParameters)asymmetricKeyParameter;
    }

    var rsaPrivateKeyParameters = DotNetUtilities.ToRSAParameters(privateKeyParams);

    // CREATE A NEW RSA INSTANCE WITH THE PRIVATE KEY PARAMETERS (THIS IS THE PRIVATE KEY)
    return RSA.Create(rsaPrivateKeyParameters);
}
Cluck answered 15/11, 2020 at 4:6 Comment(6)
To test the API at the moment I'm using Curl and it works and I'm using this curl command. : curl -k --key LMIS_JPN_CLIENT_CERT_KEY.pem --cert LMIS_JPN_CLIENT_CERT_PEM.cer:pass1556677 -H "Accept: application/json" 127.0.0.1/SiteInfoSlipcase
That line is just to pull the name of the private key filename out of the appSettings.json shown at the top of the example. You could also hard-code the name of the private key file name (just a configuration option). At a high-level, the private key is used to [sign] data (or a hash of it), which gives the receiver with the corresponding public key of the key pair assurance that (e.g.) the data was sent by the owner of the private key. It is the flip-side of encryption, where typically the public key is used to encrypt data and only the private key can decrypt it.Cluck
I'm frustradet... with python I can manage everything with one line (in this case I removed the passphrase) Doesn't exists something similar for c#?... requests.get('127.0.0.1/SiteInfo', cert=('cer.cer', 'pem.pem'), verify=False)Slipcase
C# is a little “closer to the metal”, so to speak. A strength of general purpose languages is that you can create virtually any custom API you wish for (e.g. something that resembles your expression there) if it doesn’t exist out of the box. But it does require some effort. I am not aware of anything like the Python-like API shown in your example, but there may be a NUGET package out there that someone has written that approximates that syntax.Cluck
Sorry if I come back to clientPrivKeyFileName . Now , after some hours :-) I removed the passphrase.. so now I should use the certificate file and the provateKey file. In you r first example clientPrivKeyFileName is assigned but never used in the code.Slipcase
Without being certain of how you wanted to use the private key (e.g. signing), all I could do was add the code and method to import it successfully for whatever you needed it for. SO is suggesting this gets moved to chat.Cluck

© 2022 - 2024 — McMap. All rights reserved.