Accept only self signed SSL-Certificate C# - c#

I have a selfprogrammed API running on a Windows Server 2012 with a self signed SSL-Certificate. Now I want to communicate with the webservice via HTTPS.
The comunication is only at a local network, but i still want the conection to be secure.
Is there a way to accept only my self signed certificate? I found a lot of solutions to accept all certificates, but I only want mine to be accepted.
I already thought about adding it to the windows accepted certificates, but since the program consuming the webservice is user by serveral users on diffrent PC's and I do not have administration rights on all off them.
Is it even possible to have a secure connection the way I want it to be?

Yes, as Gusman suggests, you should implement your own method for the ServerCertificateValidationCallback. You can compare the thumbprint of the cert to validate whether it is the one you want to trust. Something like this should work:
public static class CertificateValidator
{
public static string TrustedThumbprint { get; set; }
public static bool ValidateSslCertificate(
object sender,
X509Certificate certificate,
X509Chain chain,
SslPolicyErrors errors)
{
// Wrap certificate to access thumbprint.
var certificate2 = new X509Certificate2(certificate);
// Only accept certificate with trusted thumbprint.
if (certificate2.Thumbprint.Equals(
TrustedThumbprint, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// In all other cases, don't trust the certificate.
return false;
}
}
In your startup code, include the following to wire this up:
// Read your trusted thumbprint from some secure storage.
// Don't check it into source control! ;-)
CertificateValidator.TrustedThumbprint = SomeSecureConfig.Get["TrustedApiThumbprint"];
// Set the callback used to validate certificates.
ServicePointManager.ServerCertificateValidationCallback += CertificateValidator.ValidateSslCertificate;

Related

The remote certificate was rejected by the provided RemoteCertificateValidationCallback - how to get more details?

I have prepared a test case for my problem - a very simple .NET 6 Console app:
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace CheckCert
{
internal class Program
{
public const string jsonUrl = "https://wordsbyfarber.com/de/top-5";
static void Main(string[] args)
{
Console.WriteLine($"jsonUrl = {jsonUrl}");
HttpClientHandler handler = new()
{
ServerCertificateCustomValidationCallback = BackendCaValidation,
CheckCertificateRevocationList = true,
};
HttpClient httpClient = new(handler);
string jsonStr = httpClient.GetStringAsync(new Uri(jsonUrl)).Result;
Console.WriteLine($"jsonStr = {jsonStr}");
}
private static bool BackendCaValidation(HttpRequestMessage message,
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors)
{
Console.WriteLine($"sslPolicyErrors = {sslPolicyErrors}");
return SslPolicyErrors.None == sslPolicyErrors;
}
}
}
When I run it, it works as expected, will print SslPolicyErrors.None and the JSON content from my private website, which uses a Let's Encrypt certificate.
However when I change the jsonUrl to the URL of my work server, which I am better not sharing in public, then I end up with SslPolicyErrors.RemoteCertificateChainErrors and a System.AggregateException.
The exception says "look at the inner exception".
So inspect the inner exception and it says:
The remote certificate was rejected by the provided RemoteCertificateValidationCallback
So I keep looking at the certificate and the chain displayed by the Microsoft Edge browser - both for my private website and for the work server.
The work server uses a certificate issued by a self-signed work CA (and there is an intermediate certificate inbetween). All 3 certificates are not expired yet.
My question is: how to get more information here?
Why exactly do I get a SslPolicyErrors.RemoteCertificateChainErrors? is it because of that self-signed corporate CA or maybe because of some signing algorithm?
And also - similar code works for us in another project (an Azure Service Fabric application) without failing. I wonder, what could be the difference?
UPDATE:
I have followed the suggestion by Mr. Spiller (thank you!) and have added the code:
Console.WriteLine("-----------------------------------");
foreach (X509ChainStatus status in chain.ChainStatus)
{
Console.WriteLine($"status = {status.Status}");
}
Now my private Let's Encrypt secured URL looks like this (why is there no chain printed? I can see the chain in the web browser):
And the "faulty" corporate URL looks like this:
My main question is: how to make my app work against the corporate URL, without making it insecure?
I.e. I would probably have to accept the returned SslPolicyErrors.RemoteCertificateChainErrors in my app, but can I still perform some checks?
The parameter X509Chain? chain has a property ChainStatus which you can use to get the status for each element of the certification chain.
Each element in turn has a property Status of type System.Security.Cryptography.X509Certificates.X509ChainStatusFlags (cf. documentation) that should give you the status of each particular element of the certification chain.
In your case one (the only?) element most likely has the status UntrustedRoot.
If you want to connect to your corporate server even though the certificate is not trusted, you can simply return true from the callback. I.e. in BackendCaValidation check whether you are talking to the corporate server and return true even though sslPolicyErrors is not None.
The other (preferred?) way is to trust your corporate CA system-wide. I.e. add the CA to the cert store of your operating system and mark it as trusted.

ASP.NET Web API: verify if client has specific certifcate installed

We want to create a self signed certificate and manually install it on client PC's, with the private key marked as non-exportable.
The client calls a ASP.NET Web API, and there we want to check if that specific certificate is installed.
We want to do this as an additional method of authentication, to make sure that only valid clients can call the Web API. We know this is'nt waterproof because it is still possible to export the private key, but we have other additional authentication mechanisms such as a user password.
How can we achieve this? Or are there better ways to achieve our goal?
For Me After I created the self signed certificate in the Server , I added he Certificate Thumbprint to my Client App
private void setAppLocalSSL()
{
ServicePointManager.ServerCertificateValidationCallback += delegate (
object sender,
X509Certificate cert,
X509Chain chain,
SslPolicyErrors sslPolicyErrors)
{
if (sslPolicyErrors == SslPolicyErrors.None)
{
return true; //Is valid
}
// this is the Thumbprint of my local cert
// your Cert Thumbprint instead of "0000000000000000"
if (cert.GetCertHashString().ToLower() == "0000000000000000")
{
return true;
}
return false;
};
}
call setAppLocalSSL onload
Note : this is a WPF App.

Best practices for using ServerCertificateValidationCallback

I am working on a project that uses some HTTP communication between two back-end servers. Servers are using X509 certificates for authentication. Needless to say, when server A (client) establishes connection to server B (server), there is a SSL/TLS validation error, since certificates used are not from trusted 3rd party authority.
Normally, the way to handle it is using ServicePointManager.ServerCertificateValidationCallback, such as:
ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, error) =>
{
return cert.GetCertHashString() == "xxxxxxxxxxxxxxxx";
};
That approach works, except it's not ideal. What it essentially does is override validation procedure for EVERY http request done by the application. So, if another class will try to run HTTP request, it will fail. Also, if another class overrides ServicePointManager.ServerCertificateValidationCallback for its own purposes, then my communication starts failing out of sudden.
The only solution which comes to mind, is creating a separate AppDomain to perform client HTTP requests. That would work, but really - it's silly to have to do that only so that one can perform HTTP requests. Overhead will be staggering.
With that in mind, have anyone researched if there is a better practice in .NET, which would allow accessing web services, while handling client SSL/TLS validation without affecting other web clients?
An acceptable (safe) methodology working in .NET 4.5+ is to use HttpWebRequest.ServerCertificateValidationCallback. Assigning that callback on a specific instance of request will change the validation logic just for the request, not influencing other requests.
var request = (HttpWebRequest)WebRequest.Create("https://...");
request.ServerCertificateValidationCallback +=
(sender, cert, chain, error) =>
{
return cert.GetCertHashString() == "xxxxxxxxxxxxxxxx";
};
An alternative for code that does not use HttpWebRequest, and for environments where you can't install trusted certificates in the certificate store: Check the callback's error parameter, which will contain any error that were detected prior to the callback. This way, you can ignore errors for specific hash strings, but still accept other certificates that pass validation.
ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, error) =>
{
if (cert.GetCertHashString() == "xxxxxxxxxxxxxxxx")
{
return true;
}
else
{
return error == SslPolicyErrors.None;
}
};
Reference:
https://msdn.microsoft.com/en-us/library/system.net.security.remotecertificatevalidationcallback(v=vs.110).aspx
Note that this will still affect other web client instances in the same appdomain (they will all accept the specified hash string), but at least it won't block other certificates.
The straight-forward approach for this scenario should be to install the two self-generated certificates in the trusted root stores on the client machines. You will get a security warning when you do this because the certificates can't be authenticated with Thawte or similar but after that regular secure communication should work. IIRC, you need to install the full (both public and private key) version in trusted root for this to work.
Bit late to the party, I know, but another option is to use a class inheriting IDisposable that can be put into a using(){} block around your code:
public class ServicePointManagerX509Helper : IDisposable
{
private readonly SecurityProtocolType _originalProtocol;
public ServicePointManagerX509Helper()
{
_originalProtocol = ServicePointManager.SecurityProtocol;
ServicePointManager.ServerCertificateValidationCallback += TrustingCallBack;
}
public void Dispose()
{
ServicePointManager.SecurityProtocol = _originalProtocol;
ServicePointManager.ServerCertificateValidationCallback -= TrustingCallBack;
}
private bool TrustingCallBack(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
// The logic for acceptance of your certificates here
return true;
}
}
Used in this fashion:
using (new ServicePointManagerX509Helper())
{
// Your code here
}

Verify Remote Server X509Certificate using CA Certificate File

I've generated a CA and multiple certificates (signed by CA) using OpenSSL and I have a .NET/C# client and server both using SslStream which each have their own certificates/keys, mutual authentication is enabled and revocation is disabled.
I'm using RemoteCertificateValidationCallback for SslStream to validate the remote server's certificate and I was hoping I could just load the CA's public certificate (as a file) in the program and use it to verify the remote certificate rather then actually installing the CA in the Windows Certificate Store. The problem is the X509Chain won't show anything else unless I install the CA into the store, either will the Windows CryptoAPI shell when I open a PEM version of one of the certificates.
My question is, how can I verify a certificate has been signed by my specific CA just by using the CA's public certificate file without using Windows certificate store or WCF when RemoteCertificateValidationCallback, X509Certificate and X509Chain don't seem to give me anything to work with?
Because the CA certificate is NOT in the root certificate store, you will have within the RemoteCertificateValidationCallback() an error flag of SslPolicyErrors.RemoteCertificateChainErrors ; a possibility is to validate explicitely the certificate chain against your own X509Certificate2Collection, since you are not using the local store.
if (sslPolicyErrors == SslPolicyErrors.RemoteCertificateChainErrors)
{
X509Chain chain0 = new X509Chain();
chain0.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
// add all your extra certificate chain
chain0.ChainPolicy.ExtraStore.Add(new X509Certificate2(PublicResource.my_ca));
chain0.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
isValid = chain0.Build((X509Certificate2)certificate);
}
You can also re-use the chain passed in the callback, add your extra certificate(s) in the ExtraStore collection, and validate with the AllowUnknownCertificateAuthority flag which is needed since you add untrusted certificate(s) to the chain.
You could also prevent the original error by adding programmatically the CA certificate in the trusted root store (of course it opens a popup, for it is a major security problem to globally add a new trusted CA root) :
var store = new X509Store(StoreName.Root, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadWrite);
X509Certificate2 ca_cert = new X509Certificate2(PublicResource.my_ca);
store.Add(ca_cert);
store.Close();
EDIT: For those who want to clearly test the chain with your CA :
Another possibility is to use the library BouncyCastle to build the certificate chain and validate the trust. The options are clear and errors are easy to understand. In cas of success it will build the chain, otherwise an exception is returned. Sample below :
// rootCerts : collection of CA
// currentCertificate : the one you want to test
var builderParams = new PkixBuilderParameters(rootCerts,
new X509CertStoreSelector { Certificate = currentCertificate });
// crls : The certificate revocation list
builderParams.IsRevocationEnabled = crls.Count != 0;
// validationDate : probably "now"
builderParams.Date = new DateTimeObject(validationDate);
// The indermediate certs are items necessary to create the certificate chain
builderParams.AddStore(X509StoreFactory.Create("Certificate/Collection", new X509CollectionStoreParameters(intermediateCerts)));
builderParams.AddStore(X509StoreFactory.Create("CRL/Collection", new X509CollectionStoreParameters(crls)));
try
{
PkixCertPathBuilderResult result = builder.Build(builderParams);
return result.CertPath.Certificates.Cast<X509Certificate>();
...
How can I verify a certificate has been signed by my specific CA just by using the CA's public certificate file without using Windows certificate store or WCF when RemoteCertificateValidationCallback, X509Certificate and X509Chain don't seem to give me anything to work with?
The following code will avoid the Windows certificate stores and validate the chain. Its a little different than JB's code, especially in the use of flags. The code below does not require AllowUnknownCertificateAuthority (but it does use X509RevocationMode.NoCheck since I don't have a CRL).
The name of the function does not matter. Below, VerifyServerCertificate is the same callback as RemoteCertificateValidationCallback in SslStream class. You can also use it for the ServerCertificateValidationCallback in ServicePointManager.
static bool VerifyServerCertificate(object sender, X509Certificate certificate,
X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
try
{
String CA_FILE = "ca-cert.der";
X509Certificate2 ca = new X509Certificate2(CA_FILE);
X509Chain chain2 = new X509Chain();
chain2.ChainPolicy.ExtraStore.Add(ca);
// Check all properties
chain2.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
// This setup does not have revocation information
chain2.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
// Build the chain
chain2.Build(new X509Certificate2(certificate));
// Are there any failures from building the chain?
if (chain2.ChainStatus.Length == 0)
return true;
// If there is a status, verify the status is NoError
bool result = chain2.ChainStatus[0].Status == X509ChainStatusFlags.NoError;
Debug.Assert(result == true);
return result;
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
return false;
}
I have not figured out how to use this chain (chain2 below) by default such that there's no need for the callback. That is, install it on the ssl socket and the connection will "just work". And I have not figured out how install it such that its passed into the callback. That is, I have to build the chain for each invocation of the callback. I think these are architectural defects in .Net, but I might be missing something obvious.

Validating a CAcert certificate in C#

I'm currently creating a C# program which will be fetching some data over https from my server. The server in question is using a CAcert certificate (http://www.cacert.org/), and I need a way of validating the servers certificate (checking the Subject and that it is signed by the cacert root certificate).
I'd like to do this without having to import the CAcert root as a trusted CA into the windows certificate store, some people might not like that, and AFAIK that requires admin.
I'm currently using a TcpClient and SslStream and not the WebRequest/WebResponse classes because I might move from using HTTP to using my own protocol some day, but if the task is easier using the *request classes I'll consider using them.
First you want to use the overloaded SslStream constructor:
SslStream(Stream innerStream, bool leaveInnerStreamOpen, RemoteCertificateValidationCallback userCertificateValidationCallback);
Then the RemoteCertificateValidationCallback method looks something like this:
public bool IsValid(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
... you logic here ...
}
You just need to simply walk the chain and look at the certificates until you find one you are willing to accept by verifying the public key:
foreach(X509ChainElement e in chain.ChainElements)
if( e.Certificate.Subject == "CN=XXX.xx" && e.Certificate.GetPublicKeyString() == "expected public key" )
return true;

Categories