first i'm an complete newby on c#, so im just searching on the web for posibilities
What i want to get: I want a button which retrieves a list of installed certificates in the personal store.
i tried already a little, but get messages about missing references etc. So i hope someone can give me a litte advice how to achieve this.
what i found on the web is:
using System.Security.Cryptography.X509Certificates;
public static X509Certificate2 selectCert(StoreName store, StoreLocation location, string windowTitle, string windowMsg)
{
X509Certificate2 certSelected = null;
X509Store x509Store = new X509Store(store, location);
x509Store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection col = x509Store.Certificates;
X509Certificate2Collection sel = X509Certificate2UI.SelectFromCollection(col, windowTitle, windowMsg, X509SelectionFlag.SingleSelection);
if (sel.Count > 0)
{
X509Certificate2Enumerator en = sel.GetEnumerator();
en.MoveNext();
certSelected = en.Current;
}
x509Store.Close();
return certSelected;
}
yours
(i use visual studio...)
Add a reference to System.Security.dll to use the X509Certificate2UI class.
you can use foreach
if (sel.Count > 0){
foreach(var cert in sel){
certSelected = cert ;
}
}
you can make it even shorte when using linq
using System.Linq;
...
if (sel.Count > 0){
return sel.FirstOrDefault();
}
Related
I I'm having trouble making a filter that deletes any cert that doesn't have firendlyname from my certificate store in general.
Im using c# console.
I tried that but it doesn't seem to be doing anything. Still not doing anything about what I want, does anyone have a solution, thank you.
private static void RemoveUnwantedCert()
{
// Open the Root store
string RootStoreName = "Root";
StoreLocation RootStoreLocation = StoreLocation.LocalMachine;
X509Store RootStore = new X509Store(RootStoreName, RootStoreLocation);
RootStore.Open(OpenFlags.ReadOnly);
// Get all certificates in the Root store
X509Certificate2Collection certificates = RootStore.Certificates;
// Loop through all the certificates in the Root store
foreach (X509Certificate2 certificate in certificates)
{
if (certificate.FriendlyName == "None")
try
{
// Open the Root store again, this time with ReadWrite permissions
RootStore.Open(OpenFlags.ReadWrite);
// Remove the certificate from the Root store
RootStore.Remove(certificate);
// Close the Root store
RootStore.Close();
}
catch (Exception) { }
// Break out of the loop
break;
}
// Close the Root store
RootStore.Close();
```
`
I tried that but it doesn't seem to be doing anything. Still not doing anything about what I want, does anyone have a solution, thank you.
To remove a certificate and all certificates in the chain -
X509Certificate2Collection col = store.Certificates.Find(X509FindType.FindBySubjectName, "yoursubjectname", false);
var chain = new X509Chain();
chain.Build(col[0]);
var allCertsInChain = new X509Certificate2Collection();
foreach (var entry in chain.ChainElements)
{
allCertsInChain.Add(entry.Certificate);
}
store.RemoveRange(allCertsInChain);
Thanks but i just want delete a specified one.
All, I run into an issue where the service account my ASP.NET web forms application (.Net Framework 4.6.1) runs under cannot load the X509Certificate(.pfx) from the personal store on the windows 2012 R2 server .Here is how I imported the certificate to the certificate store
I Logged into the server using the service account(domain\username) ,used mmc snap in to import the certificate to Current User Personal Certificate Store (please see screenshot at the end)
This is the code I am using to load the certificate in C#.But the certificate
is null
public X509Certificate2 Load()
{
X509Certificate2 x509Certificate = null;
var store = new X509Store(StoreName.My,StoreLocation.CurrentUser);
string thumbPrint = StripTheSpacesAndMakeItUpper(ConfigurationManager.AppSettings["pfxthumbPrint"]);
store.Open(OpenFlags.ReadOnly);
var certCollection = store.Certificates;
foreach (var x509 in certCollection)
{
if (x509.Thumbprint.Equals(thumbPrint))
{
x509Certificate = x509;
break;
}
}
store.Close();
return x509Certificate;
}
private string StripTheSpacesAndMakeItUpper(string thumbPrint)
{
if(!string.IsNullOrWhiteSpace(thumbPrint))
{
return Regex.Replace(thumbPrint, #"\s|\W", "").ToUpper();
}
return thumbPrint;
}
Any suggestions on why the method Load returns null ?
]3
I don't know how you set the value of ConfigurationManager.AppSettings["pfxthumbPrint"]. I guess you double clicked the certificate in your CurrentUser store and copied the thumbprint from the Details tab, right? If that is the case, you copied also one invisible character from the beginning of the thumbprint.
The saddest thing is that this character (I don't know what it is) is not visible in your app.config/web.config. The only way to get rid of if is to delete the first quote character with the first character of the thumbprint and type them in again manually. Or delete entire thumbprint and the quotes and type them again if you wish.
Instead of
if (x509.Thumbprint.Equals(x509CertificateFriendlyName))
Shouldn't it be
if (x509.Thumbprint.Equals(thumbPrint))
...?
Also, you appear to have x509Certificate declared as a local variable and then you discard it. Did you intend to assign the value to an instance variable perhaps? I don't even see a return statement.
Also, you're not disposing your store, although that probably isn't the cause of your issue.
Here's a different version that addresses these issues, and will also eliminate any invisible characters in the configuration entry (see pepo's answer for why).
public X509Certificate2 Load()
{
var thumbPrintFromConfig = ConfigurationManager.AppSettings["pfxthumbPrint"]
var thumbPrint = Regex.Replace(thumbPrintFromConfig.ToUpper(),"[^A-F0-9]",""); //Keep only hex digits
return Load(thumbPrint);
}
private X509Certificate2 Load(string thumbPrint)
{
using (var store = new X509Store(StoreName.My,StoreLocation.CurrentUser))
{
store.Open(OpenFlags.ReadOnly);
var cert = store
.Certificates
.OfType<X509Certificate2>()
.Where(x => x.Thumbprint == thumbPrint)
.Single();
store.Close();
return cert;
}
}
I'm running into the following and after feeling like I've exhausted various avenues of research on Google and Stack Overflow I decided to just ask my own question about it.
I'm trying to generate a personal certificate (using BouncyCastle) based on a CA certificate that I already have and own. After generating the certificate, placing it in the 'My' store, I then attempt to update my IIS website's SSL binding to use this new certificate.
What I'm noticing is that the updates to the IIS website (using ServerManager) are not throwing exception, yet when I go to the IIS Manager console I notice the website's binding has no SSL certificate selected. When I attempt to select the certificate that I created (shows up fine as a viable option) I get the following error message:
A specified logon session does not exist. It may already have been terminated. (Exception from HRESULT: 0x80070520)
As a test I exported my generated certificate (with the private key) and reinstalled it via the wizard and then once again tried setting up the binding (via IIS Manager) which worked.
Because of this behavior I assumed it was an issue with how I was generating or adding the certificate to the store. I was hoping someone may have some idea of what the issue I'm having may be. The following are the relevant functions (I believe) used in creating the certificate, adding it to the store, and updating the website's binding programmatically:
Main function the generates that get the CA certificate private key, generates the personal self-signed certificate, and updates the sites binding:
public static bool GenerateServerCertificate(
X509Certificate2 CACert,
bool addToStore,
DateTime validUntil)
{
try
{
if (CACert.PrivateKey == null)
{
throw new CryptoException("Authority certificate has no private key");
}
var key = DotNetUtilities.GetKeyPair(CACert.PrivateKey).Private;
byte[] certHash = GenerateCertificateBasedOnCAPrivateKey(
addToStore,
key,
validUntil);
using (ServerManager manager = new ServerManager())
{
Site site = manager.Sites.Where(q => q.Name == "My Site").FirstOrDefault();
if (site == null)
{
return false;
}
foreach (Binding binding in site.Bindings)
{
if (binding.Protocol == "https")
{
binding.CertificateHash = certHash;
binding.CertificateStoreName = "MY";
}
}
manager.CommitChanges();
}
}
catch(Exception ex)
{
LOG.Error("Error generating certitifcate", ex);
return false;
}
return true;
}
Generating the certificate based on the CA private key:
public static byte[] GenerateCertificateBasedOnCAPrivateKey(
bool addToStore,
AsymmetricKeyParameter issuerPrivKey,
DateTime validUntil,
int keyStrength = 2048)
{
string subjectName = $"CN={CertSubjectName}";
// Generating Random Numbers
CryptoApiRandomGenerator randomGenerator = new CryptoApiRandomGenerator();
SecureRandom random = new SecureRandom(randomGenerator);
ISignatureFactory signatureFactory = new Asn1SignatureFactory("SHA512WITHRSA", issuerPrivKey, random);
// The Certificate Generator
X509V3CertificateGenerator certificateGenerator = new X509V3CertificateGenerator();
certificateGenerator.AddExtension(
X509Extensions.ExtendedKeyUsage,
true,
new ExtendedKeyUsage((new List<DerObjectIdentifier> { new DerObjectIdentifier("1.3.6.1.5.5.7.3.1") })));
// Serial Number
BigInteger serialNumber = BigIntegers.CreateRandomInRange(BigInteger.One, BigInteger.ValueOf(Int64.MaxValue), random);
certificateGenerator.SetSerialNumber(serialNumber);
// Issuer and Subject Name
X509Name subjectDN = new X509Name(subjectName);
X509Name issuerDN = new X509Name(CACertificateName);
certificateGenerator.SetIssuerDN(issuerDN);
certificateGenerator.SetSubjectDN(subjectDN);
// Valid For
DateTime notBefore = DateTime.UtcNow.Date;
DateTime notAfter = validUntil > notBefore ? validUntil : notBefore.AddYears(1);
certificateGenerator.SetNotBefore(notBefore);
certificateGenerator.SetNotAfter(notAfter);
// Subject Public Key
AsymmetricCipherKeyPair subjectKeyPair;
var keyGenerationParameters = new KeyGenerationParameters(random, keyStrength);
var keyPairGenerator = new RsaKeyPairGenerator();
keyPairGenerator.Init(keyGenerationParameters);
subjectKeyPair = keyPairGenerator.GenerateKeyPair();
certificateGenerator.SetPublicKey(subjectKeyPair.Public);
// Generating the Certificate
Org.BouncyCastle.X509.X509Certificate certificate = certificateGenerator.Generate(signatureFactory);
// correcponding private key
PrivateKeyInfo info = PrivateKeyInfoFactory.CreatePrivateKeyInfo(subjectKeyPair.Private);
// merge into X509Certificate2
X509Certificate2 x509 = new X509Certificate2(certificate.GetEncoded());
Asn1Sequence seq = (Asn1Sequence)Asn1Object.FromByteArray(info.ParsePrivateKey().GetDerEncoded());
if (seq.Count != 9)
{
throw new PemException("Malformed sequence in RSA private key");
}
RsaPrivateKeyStructure rsa = RsaPrivateKeyStructure.GetInstance(seq);
RsaPrivateCrtKeyParameters rsaparams = new RsaPrivateCrtKeyParameters(
rsa.Modulus,
rsa.PublicExponent,
rsa.PrivateExponent,
rsa.Prime1,
rsa.Prime2,
rsa.Exponent1,
rsa.Exponent2,
rsa.Coefficient);
x509.PrivateKey = DotNetUtilities.ToRSA(rsaparams);
if (addToStore)
{
// Add certificate to the Personal store
AddCertToStore(x509, StoreName.My, StoreLocation.LocalMachine, "Certificate Friendly Name");
}
return x509.GetCertHash();
}
Adding the certificate to the store:
private static void AddCertToStore(X509Certificate2 cert, StoreName storeName, StoreLocation storeLocation, string friendlyName)
{
X509Store store = new X509Store(storeName, storeLocation);
try
{
store.Open(OpenFlags.ReadWrite);
store.Add(cert);
if (!string.IsNullOrWhiteSpace(friendlyName)) {
var certs = store.Certificates.Find(X509FindType.FindBySubjectDistinguishedName, cert.Subject, true);
if (certs.Count > 0)
{
certs[0].FriendlyName = friendlyName;
}
}
}
finally
{
store.Close();
}
}
Just a final note, I have tried a few things from what I've seen on various sites in regards to that error (doesn't seem very clear what the issue is):
This works on a different box (my personal development machine) but I hit these snags on a server machine (running Windows Server 2012 R2)
The IIS Help dialog informs me the machine is running IIS 8.5
Verified the validity generated certificate and the CA certificate with CertUtil.exe
Verified the generated certificate and the CA certificate had a private key that could be found
Verified administrators (and eventually even my logged in account) had access to where the private key file for both the CA certificate and the generated certificate.
Any ideas what my issue could be?
Update:
I was able to get some results by doing the following:
Export my certificate to a file programmatically by doing File.WriteAllBytes(filePath, cert.Export(X509ContentType.Pkcs12, password));
Then I import this certificate file to the store by doing:
var cert = new X509Certificate2(certFilePath, certPassword, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// My original AddCertToStore function
AddCertToStore(cert, StoreName.My, StoreLocation.LocalMachine, "Friendly Name");
Finally, I set the binding as I was doing earlier:
using (ServerManager manager = new ServerManager())
{
Site site = manager.Sites.Where(q => q.Name == "My Site").FirstOrDefault();
if (site == null)
{
return false;
}
foreach (Binding binding in site.Bindings)
{
if (binding.Protocol == "https")
{
binding.CertificateHash = certHash;
binding.CertificateStoreName = "MY";
}
}
manager.CommitChanges();
}
Doing it this way works, but I don't see why I would have export the certificate to a file, THEN load it into a X509Certificate2 object, add to the store, and finally set up the binding.
The ToRSA method most likely creates an ephemeral RSA key, so when the references are all gone the key gets deleted. Exporting the ephemeral structure into a PFX then re-importing it with PersistKeySet is one way to turn it into a persisted key. Others exist, but that one is one of the less convoluted ones.
You don't actually have to write it to a file, though.
byte[] pkcs12Blob = cert.Export(X509ContentType.Pkcs12, password);
ver certWithPersistedKey = new X509Certificate2(pkcs12Blob, password, allTheFlagsYouAlreadySet);
There are also other subtleties going on, like setting the PrivateKey property has different behaviors for a cert instance that was loaded from a store and one which was loaded from bytes... the PFX/PKCS#12 export/import works around all of those.
For us, it was related to an invalid certificate. We went to IIS >> Server Certificates and exported the certificate from there.
The certificate was correctly bound to IIS site after that.
I need to get a X509 Certificate by Serial Number, I have the serial number and I am looping through them and i see the serial number in the collection I need but it is never found.
Here is my debug code just ot make sure I am seeing the proper serial numbers:
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 cert in store.Certificates)
{
System.Web.HttpContext.Current.Response.Write (cert.SerialNumber + "=" + oauthCertificateFindValue + "<br/>");
if (cert.SerialNumber == oauthCertificateFindValue)
{
System.Web.HttpContext.Current.Response.Write("<br/>FOUND FOUND FOUND<br/>");
}
}
Here is the output from this code:
0091ED5F0CAED6AD52=0091ED5F0CAED6AD52
3D3233116A894CB244DB359DF99E7862=0091ED5F0CAED6AD52
Clearly the first one I loop though matches the serial number but the if always fails and what I really need to work based on this serial number also fails:
X509Certificate2Collection certificateCollection = store.Certificates.Find(x509FindType, oauthCertificateFindValue, false);
if (certificateCollection.Count == 0)
{
throw new ApplicationException(string.Format("An OAuth certificate matching the X509FindType '{0}' and Value '{1}' cannot be found in the local certificate store.", oauthCertificateFindType, oauthCertificateFindValue));
}
return certificateCollection[0];
What am I doing wrong here?
It would appear that there are two invisible characters in the certificate serial number of the certificate you're trying to find, which is why they aren't matching. You should be able to confirm this if you change the output from your foreach loop to:
System.Web.HttpContext.Current.Response.Write (string.Format("{0} (Length: {1}) = {2} (Length: {3})<br/>", cert.SerialNumber, cert.SerialNumber.Length oauthCertificateFindValue, oauthCertificateFindValue.Length);
You'll most likely see that the values look the same, but their lengths differ (indicating the presence of these invisible characters).
You will need to update your search value to match the certificate's serial number including the invisible characters.
The first X509Store you have opened that was not closed yet and from the same you tried to get the certificate which was already read out.
First close the store in code where you are matching the serials and the code where you are reading from store, reopen the store.
Below code worked for me. Just verify once.
private static void CompareCertSerialse()
{
string oauthCertificateFindValue = "7C00001851CBFF5E9F563E236F000000001851";
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
foreach (X509Certificate2 cert in store.Certificates)
{
Console.WriteLine(cert.SerialNumber + "=" + oauthCertificateFindValue);
if (cert.SerialNumber == oauthCertificateFindValue)
{
Console.WriteLine("FOUND FOUND FOUND>");
//Close the store
store.Close();
GetCert(oauthCertificateFindValue);
}
}
}
private static X509Certificate2 GetCert(string oauthCertificateFindValue)
{
//Reopen the store
X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection certificateCollection = store.Certificates.Find(X509FindType.FindBySerialNumber, oauthCertificateFindValue, false);
if (certificateCollection.Count == 0)
{
Console.WriteLine("Nothing Found");
}
return certificateCollection[0];
}
How can I programmatically get when X509Certificate is revoked? I can get information if certificate is revoked, but i need to get when is revoked, i think that CRL list have that info, but can someone tell me how to read that.
Revocation status is checked by (a) obtaining CRL lists and checking if the certificate is listed there, and (b) sending an OCSP request to the server to check the same.
.NET doesn't let you do this. CryptoAPI might have some means for these operations, but the easiest is to use third-party library for .NET. BouncyCastle claims to have some support for OCSP and CRLs, and our SecureBlackbox provides complete support (both client and server components are available) for OCSP and CRL, and also we provide a component which performs complete certificate validation (with all CRL and OCSP checks and HTTP and LDAP communication when needed) with one method call.
The CRL is stored as an OID in the extensions property of the X509Certificate object. The OID FriendlyName and Value are 'CRL Distribution Points' and '2.5.29.31'. Searching the certificate's extensions for an OID with value 2.5.29.31, you can then parse the raw data and get the distribution point(s).
I found the following code sample here. I tested it on both publicly sign certs and internal Microsoft CA certs; it returns the URL or LDAP connection string.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace System.Security.Cryptography.X509Certificates
{
public static class X509Certificate2Extensions
{
/// <summary>
/// Returns an array of CRL distribution points for X509Certificate2 object.
/// </summary>
/// <param name="certificate">X509Certificate2 object.</param>
/// <returns>Array of CRL distribution points.</returns>
public static string[] GetCrlDistributionPoints(this X509Certificate2 certificate)
{
X509Extension ext = certificate.Extensions.Cast<X509Extension>().FirstOrDefault(
e => e.Oid.Value == "2.5.29.31");
if (ext == null || ext.RawData == null || ext.RawData.Length < 11)
return EmptyStrings;
int prev = -2;
List<string> items = new List<string>();
while (prev != -1 && ext.RawData.Length > prev + 1)
{
int next = IndexOf(ext.RawData, 0x86, prev == -2 ? 8 : prev + 1);
if (next == -1)
{
if (prev >= 0)
{
string item = Encoding.UTF8.GetString(ext.RawData, prev + 2, ext.RawData.Length - (prev + 2));
items.Add(item);
}
break;
}
if (prev >= 0 && next > prev)
{
string item = Encoding.UTF8.GetString(ext.RawData, prev + 2, next - (prev + 2));
items.Add(item);
}
prev = next;
}
return items.ToArray();
}
static int IndexOf(byte[] instance, byte item, int start)
{
for (int i = start, l = instance.Length; i < l; i++)
if (instance[i] == item)
return i;
return -1;
}
static string[] EmptyStrings = new string[0];
}
}
use this API from x509.h file use openssl 1.0 / or above version
X509_CRL_get0_by_cert(X509_CRL *crl, X509_REVOKED **ret, X509 *x);
X in the certificate u want to check ;
Ret is the Address of the revocation structure where reason for the revocation and all stored
crl is the CRL .
For future readers.
As already was said, .NET currently do not expose public classes nor for X.509 certificate revocation lists, nor for OCSP messaging. Of course, you can write your own code or to use 3rd party libraries.
You can try my own CryptoAPI managed extensions from PowerShell PKI module project (PKI.Core.dll library). There is a support for X509 CRL managed class (built on top of CryptoAPI native functions): X509CRL2 class. RevokedCertificates property stores an array of revoked certificates. In addition, library includes OCSP messaging classes (completely managed) stored in PKI.OCSP namespace. If your certificate contains OCSP links in the AIA extension, then you can easyly construct OCSP request from X509Certificate2 object by instantiating OCSPRequest object and invoking OCSPRequest.SendRequest method. Return object is an instance of OCSPResponse class.
Basically, the code woul look as this:
using System;
using System.Security.Cryptography.X509Certificates;
using PKI.OCSP;
public class Class1 {
public static DateTime? GetrevocationDate(X509Certificate2 cert) {
OCSPRequest request = new OCSPRequest(cert);
OCSPResponse response = request.SendRequest();
if (response.Responses[0].CertStatus == CertificateStatus.Revoked) {
return response.Responses[0].RevocationInfo.RevocationDate;
}
return null;
}
}
NULL would mean that the certificate is not revoked.
with X509 CRL the code would look as this:
using System;
using System.Security.Cryptography.X509Certificates;
public class Class1 {
// crlRawData could a type of System.String and pass the path to a CRL file there.
public static DateTime? GetrevocationDate(X509Certificate2 cert, Byte[] crlRawData) {
X509CRL2 crl = new X509CRL2(crlRawData);
X509CRLEntry entry = crl.RevokedCertificates[cert.SerialNumber];
if (entry != null) {
return entry.RevocationDate;
}
return null;
}
}
I don't have the reputation to upvote #Hive's answer above but it was exactly what I needed, except for the language. I've posted my PowerShell port below:
$cert = (Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -match "SUBJECT NAME*"})
function IndexOfByte([byte[]]$instance, [byte]$item, [int]$start)
{
$len = $instance.Length
for ($i = $start; $i -lt $len; $i++) {
if ($instance[$i] -eq $item) { return $i }
}
return -1;
}
$crls = $cert.Extensions | ? { $_.Oid.FriendlyName -eq "CRL Distribution Points" }
$prev = -2;
[System.Collections.ArrayList]$items = #();
while ($prev -ne -1 -and $crls.RawData.Length -gt $prev + 1)
{
if($prev -eq -2) { $y = 8 } else {$y = $prev + 1}
$next = IndexOfByte -instance $crls.RawData -item 0x86 -start $y
if ($next -eq -1) {
if ($prev -ge 0) {
$item = [system.Text.Encoding]::UTF8.GetString($crls.RawData, $prev + 2, $crls.RawData.Length - ($prev + 2));
$items.Add($item);
}
break;
}
if ($prev -ge 0 -and $next -gt $prev) {
$item = [system.Text.Encoding]::UTF8.GetString($crls.RawData, $prev + 2, $next - ($prev + 2));
$items.Add($item);
}
$prev = $next;
}
Write-Host "Certificate CRLs: `n$($items | out-string)"
This could help you:
http://blogs.msdn.com/b/alejacma/archive/2010/05/10/how-to-get-info-from-client-certificates-issued-by-a-ca-c.aspx
Regards.
The first step is to extract the CRL distribution points from the certificate, and then match your certificate's serial number against the content of the CRL from the distribution point.
Here's an alternative way to extract the CRL distribution points with fewer magic numbers and bit twiddling. (tested in .NET Core 2.1)
var path = "<path to signed file>";
// get certificate
var cert = new X509Certificate2(path);
// extract the CRL distribution points information
var crlInfo = cert.Extensions["2.5.29.31"];
var crlDistribitionPoints = new AsnEncodedData(crlInfo.Oid, crlInfo.RawData).Format(false);
Console.Writeline(crlDistribitionPoints);
When you say revoked, do you mean invalid? If its revoked I wouldn't expect it to arrive at the request in your code as the web server will have got in the way first.
If you use the x509certificate2, which is derived from x509certificate, then you have a lot more properties which you can check; there are a number of examples on the link below.
http://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509certificate2.aspx