src/System.Private.ServiceModel/tools/CertificateGenerator/CertificateGenerator.cs (455 lines of code) (raw):
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using Org.BouncyCastle.Asn1;
using Org.BouncyCastle.Asn1.X509;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Generators;
using Org.BouncyCastle.Crypto.Operators;
using Org.BouncyCastle.Crypto.Prng;
using Org.BouncyCastle.Math;
using Org.BouncyCastle.Pkcs;
using Org.BouncyCastle.Security;
using Org.BouncyCastle.X509;
using Org.BouncyCastle.X509.Extension;
using X509Certificate2 = System.Security.Cryptography.X509Certificates.X509Certificate2;
using X509KeyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags;
namespace WcfTestCommon
{
// NOT THREADSAFE. Callers should lock before doing work with this class if multithreaded operation is expected
public class CertificateGenerator
{
private bool _isInitialized;
// Settable properties prior to initialization
private string _crlUri;
private string _crlServiceUri;
private string _crlUriRelativePath;
private string _password;
private TimeSpan _validityPeriod = TimeSpan.FromDays(1);
// This can't be too short as there might be a time skew between machines,
// but also can't be too long, as the CRL is cached by the machine
private TimeSpan _crlValidityGracePeriodStart = TimeSpan.FromMinutes(5);
private TimeSpan _crlValidityGracePeriodEnd = TimeSpan.FromMinutes(5);
// Give the cert a grace period in case there's a time skew between machines
private readonly TimeSpan _gracePeriod = TimeSpan.FromHours(1);
private const string _authorityCanonicalName = "DO_NOT_TRUST_WcfBridgeRootCA";
private readonly string _signatureAlgorithm = Org.BouncyCastle.Asn1.Pkcs.PkcsObjectIdentifiers.Sha256WithRsaEncryption.Id;
private const string _upnObjectId = "1.3.6.1.4.1.311.20.2.3";
private const int _keyLengthInBits = 2048;
private static readonly X509V3CertificateGenerator s_certGenerator = new X509V3CertificateGenerator();
private static readonly X509V2CrlGenerator s_crlGenerator = new X509V2CrlGenerator();
// key: serial number, value: revocation time
private static Dictionary<string, DateTime> s_revokedCertificates = new Dictionary<string, DateTime>();
private RsaKeyPairGenerator _keyPairGenerator;
private SecureRandom _random;
private DateTime _initializationDateTime;
private DateTime _defaultValidityNotBefore;
private DateTime _defaultValidityNotAfter;
// We need to hang onto the _authorityKeyPair and _authorityCertificate - all certificates generated
// by this instance will be signed by this Authority certificate and private key
private AsymmetricCipherKeyPair _authorityKeyPair;
private X509CertificateContainer _authorityCertificate;
public void Initialize()
{
if (!_isInitialized)
{
if (string.IsNullOrWhiteSpace(_authorityCanonicalName))
{
throw new ArgumentException("AuthorityCanonicalName must not be an empty string or only whitespace", "AuthorityCanonicalName");
}
if (string.IsNullOrWhiteSpace(_password))
{
throw new ArgumentException("Password must not be an empty string or only whitespace", "Password");
}
Uri dummy;
if (string.IsNullOrWhiteSpace(_crlUriRelativePath) && !Uri.TryCreate(_crlUriRelativePath, UriKind.Relative, out dummy))
{
throw new ArgumentException("CrlUri must be a valid relative URI", "CrlUriRelativePath");
}
_crlUri = string.Format("http://{0}{1}", _crlServiceUri, _crlUriRelativePath);
_initializationDateTime = DateTime.UtcNow;
_defaultValidityNotBefore = _initializationDateTime.Subtract(_gracePeriod);
_defaultValidityNotAfter = _initializationDateTime.Add(_validityPeriod);
_random = new SecureRandom(new CryptoApiRandomGenerator());
_keyPairGenerator = new RsaKeyPairGenerator();
_keyPairGenerator.Init(new KeyGenerationParameters(_random, _keyLengthInBits));
_authorityKeyPair = _keyPairGenerator.GenerateKeyPair();
_isInitialized = true;
Trace.WriteLine("[CertificateGenerator] initialized with the following configuration:");
Trace.WriteLine(string.Format(" {0} = {1}", "AuthorityCanonicalName", _authorityCanonicalName));
Trace.WriteLine(string.Format(" {0} = {1}", "CrlUri", _crlUri));
Trace.WriteLine(string.Format(" {0} = {1}", "Password", _password));
Trace.WriteLine(string.Format(" {0} = {1}", "ValidityPeriod", _validityPeriod));
Trace.WriteLine(string.Format(" {0} = {1}", "Valid to", _defaultValidityNotAfter));
_authorityCertificate = CreateCertificate(isAuthority: true, isMachineCert: false, signingCertificate: null, certificateCreationSettings: null);
}
}
public void Reset()
{
s_certGenerator.Reset();
s_crlGenerator.Reset();
_authorityCertificate = null;
_isInitialized = false;
}
public X509CertificateContainer AuthorityCertificate
{
get
{
EnsureInitialized();
return _authorityCertificate;
}
}
public byte[] CrlEncoded
{
get
{
EnsureInitialized();
return CreateCrl(_authorityCertificate.InternalCertificate).GetEncoded();
}
}
public bool Initialized
{
get { return _isInitialized; }
}
public string AuthorityCanonicalName
{
get { return _authorityCanonicalName; }
}
public string AuthorityDistinguishedName
{
get
{
EnsureInitialized();
return CreateX509Name(_authorityCanonicalName).ToString();
}
}
public string CertificatePassword
{
get { return _password; }
set
{
EnsureNotInitialized("CertificatePassword");
_password = value;
}
}
public string CrlUri
{
get
{
EnsureInitialized();
return _crlUri;
}
}
public string CrlServiceUri
{
get
{
return _crlServiceUri;
}
set
{
EnsureNotInitialized("CrlServiceUri");
_crlServiceUri = value;
}
}
public string CrlUriRelativePath
{
get
{
return _crlUriRelativePath;
}
set
{
EnsureNotInitialized("CrlUriRelativePath");
_crlUriRelativePath = value;
}
}
public List<string> RevokedCertificates
{
get
{
List<string> retVal = new List<string>(s_revokedCertificates.Keys);
return retVal;
}
}
public TimeSpan ValidityPeriod
{
get { return _validityPeriod; }
set
{
EnsureNotInitialized("ValidityPeriod");
_validityPeriod = value;
}
}
public X509CertificateContainer CreateMachineCertificate(CertificateCreationSettings creationSettings)
{
EnsureInitialized();
return CreateCertificate(false, true, _authorityCertificate.InternalCertificate, creationSettings);
}
public X509CertificateContainer CreateUserCertificate(CertificateCreationSettings creationSettings)
{
EnsureInitialized();
return CreateCertificate(false, false, _authorityCertificate.InternalCertificate, creationSettings);
}
// Only the ctor should be calling with isAuthority = true
// if isAuthority, value for isMachineCert doesn't matter
private X509CertificateContainer CreateCertificate(bool isAuthority, bool isMachineCert, X509Certificate signingCertificate, CertificateCreationSettings certificateCreationSettings)
{
if (certificateCreationSettings == null)
{
if (isAuthority)
{
certificateCreationSettings = new CertificateCreationSettings();
}
else
{
throw new Exception("Parameter certificateCreationSettings cannot be null when isAuthority is false");
}
}
// Set to default cert creation settings if not set
if (certificateCreationSettings.ValidityNotBefore == default(DateTime))
{
certificateCreationSettings.ValidityNotBefore = _defaultValidityNotBefore;
}
if (certificateCreationSettings.ValidityNotAfter == default(DateTime))
{
certificateCreationSettings.ValidityNotAfter = _defaultValidityNotAfter;
}
if (!isAuthority ^ (signingCertificate != null))
{
throw new ArgumentException("Either isAuthority == true or signingCertificate is not null");
}
string subject = certificateCreationSettings.Subject;
// If certificateCreationSettings.SubjectAlternativeNames == null, then we should add exactly one SubjectAlternativeName == Subject
// so that the default certificate generated is compatible with mainline scenarios
// However, if certificateCreationSettings.SubjectAlternativeNames == string[0], then allow this as this is a legit scenario we want to test out
if (certificateCreationSettings.SubjectAlternativeNames == null)
{
certificateCreationSettings.SubjectAlternativeNames = new string[1] { subject };
}
string[] subjectAlternativeNames = certificateCreationSettings.SubjectAlternativeNames;
if (!isAuthority && string.IsNullOrWhiteSpace(subject))
{
throw new ArgumentException("Certificate Subject must not be an empty string or only whitespace", "creationSettings.Subject");
}
EnsureInitialized();
s_certGenerator.Reset();
// Tag on the generation time to prevent caching of the cert CRL in Linux
X509Name authorityX509Name = CreateX509Name(string.Format("{0} {1}", _authorityCanonicalName, DateTime.Now.ToString("s")));
var serialNum = new BigInteger(64 /*sizeInBits*/, _random).Abs();
var keyPair = isAuthority ? _authorityKeyPair : _keyPairGenerator.GenerateKeyPair();
if (isAuthority)
{
s_certGenerator.SetIssuerDN(authorityX509Name);
s_certGenerator.SetSubjectDN(authorityX509Name);
var authorityKeyIdentifier = new AuthorityKeyIdentifier(
SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(_authorityKeyPair.Public),
new GeneralNames(new GeneralName(authorityX509Name)),
serialNum);
s_certGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, authorityKeyIdentifier);
s_certGenerator.AddExtension(X509Extensions.KeyUsage, false, new KeyUsage(X509KeyUsage.DigitalSignature | X509KeyUsage.KeyAgreement | X509KeyUsage.KeyCertSign | X509KeyUsage.KeyEncipherment | X509KeyUsage.CrlSign));
}
else
{
X509Name subjectName = CreateX509Name(subject);
s_certGenerator.SetIssuerDN(PrincipalUtilities.GetSubjectX509Principal(signingCertificate));
s_certGenerator.SetSubjectDN(subjectName);
s_certGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure(_authorityKeyPair.Public));
s_certGenerator.AddExtension(X509Extensions.KeyUsage, false, new KeyUsage(X509KeyUsage.DigitalSignature | X509KeyUsage.KeyAgreement | X509KeyUsage.KeyEncipherment));
}
s_certGenerator.AddExtension(X509Extensions.SubjectKeyIdentifier, false, new SubjectKeyIdentifierStructure(keyPair.Public));
s_certGenerator.SetSerialNumber(serialNum);
s_certGenerator.SetNotBefore(certificateCreationSettings.ValidityNotBefore);
s_certGenerator.SetNotAfter(certificateCreationSettings.ValidityNotAfter);
s_certGenerator.SetPublicKey(keyPair.Public);
s_certGenerator.AddExtension(X509Extensions.BasicConstraints, true, new BasicConstraints(isAuthority));
if (certificateCreationSettings.EKU == null || certificateCreationSettings.EKU.Count == 0)
{
s_certGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, false, new ExtendedKeyUsage(KeyPurposeID.IdKPServerAuth, KeyPurposeID.IdKPClientAuth));
}
else
{
s_certGenerator.AddExtension(X509Extensions.ExtendedKeyUsage, false, new ExtendedKeyUsage(certificateCreationSettings.EKU));
}
if (!isAuthority)
{
if (isMachineCert)
{
List<Asn1Encodable> subjectAlternativeNamesAsAsn1EncodableList = new List<Asn1Encodable>();
// All endpoints should also be in the Subject Alt Names
for (int i = 0; i < subjectAlternativeNames.Length; i++)
{
if (!string.IsNullOrWhiteSpace(subjectAlternativeNames[i]))
{
// Machine certs can have additional DNS names
subjectAlternativeNamesAsAsn1EncodableList.Add(new GeneralName(GeneralName.DnsName, subjectAlternativeNames[i]));
}
}
s_certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, true, new DerSequence(subjectAlternativeNamesAsAsn1EncodableList.ToArray()));
}
else
{
if (subjectAlternativeNames.Length > 1)
{
var subjectAlternativeNamesAsAsn1EncodableList = new Asn1EncodableVector();
// Only add a SAN for the user if there are any
for (int i = 1; i < subjectAlternativeNames.Length; i++)
{
if (!string.IsNullOrWhiteSpace(subjectAlternativeNames[i]))
{
Asn1EncodableVector otherNames = new Asn1EncodableVector();
otherNames.Add(new DerObjectIdentifier(_upnObjectId));
otherNames.Add(new DerTaggedObject(true, 0, new DerUtf8String(subjectAlternativeNames[i])));
Asn1Object genName = new DerTaggedObject(false, 0, new DerSequence(otherNames));
subjectAlternativeNamesAsAsn1EncodableList.Add(genName);
}
}
s_certGenerator.AddExtension(X509Extensions.SubjectAlternativeName, true, new DerSequence(subjectAlternativeNamesAsAsn1EncodableList));
}
}
}
if (isAuthority || certificateCreationSettings.IncludeCrlDistributionPoint)
{
var crlDistributionPoints = new DistributionPoint[1]
{
new DistributionPoint(
new DistributionPointName(
new GeneralNames(
new GeneralName(
GeneralName.UniformResourceIdentifier, string.Format("{0}", _crlUri, serialNum.ToString(radix: 16))))),
null,
null)
};
var revocationListExtension = new CrlDistPoint(crlDistributionPoints);
s_certGenerator.AddExtension(X509Extensions.CrlDistributionPoints, false, revocationListExtension);
}
ISignatureFactory signatureFactory = new Asn1SignatureFactory(_signatureAlgorithm, _authorityKeyPair.Private, _random);
X509Certificate cert = s_certGenerator.Generate(signatureFactory);
switch (certificateCreationSettings.ValidityType)
{
case CertificateValidityType.Revoked:
RevokeCertificateBySerialNumber(serialNum.ToString(radix: 16));
break;
case CertificateValidityType.Expired:
break;
default:
EnsureCertificateIsValid(cert);
break;
}
// For now, given that we don't know what format to return it in, preserve the formats so we have
// the flexibility to do what we need to
X509CertificateContainer container = new X509CertificateContainer();
X509CertificateEntry[] chain = new X509CertificateEntry[1];
chain[0] = new X509CertificateEntry(cert);
Pkcs12Store store = new Pkcs12StoreBuilder().Build();
store.SetKeyEntry(
certificateCreationSettings.FriendlyName != null ? certificateCreationSettings.FriendlyName : string.Empty,
new AsymmetricKeyEntry(keyPair.Private),
chain);
using (MemoryStream stream = new MemoryStream())
{
store.Save(stream, _password.ToCharArray(), _random);
container.Pfx = stream.ToArray();
}
X509Certificate2 outputCert;
if (isAuthority)
{
// don't hand out the private key for the cert when it's the authority
outputCert = new X509Certificate2(cert.GetEncoded());
}
else
{
// Otherwise, allow encode with the private key. note that X509Certificate2.RawData will not provide the private key
// you will have to re-export this cert if needed
outputCert = new X509Certificate2(container.Pfx, _password, X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet);
}
container.Subject = subject;
container.InternalCertificate = cert;
container.Certificate = outputCert;
container.Thumbprint = outputCert.Thumbprint;
Trace.WriteLine("[CertificateGenerator] generated a certificate:");
Trace.WriteLine(string.Format(" {0} = {1}", "isAuthority", isAuthority));
if (!isAuthority)
{
Trace.WriteLine(string.Format(" {0} = {1}", "Signed by", signingCertificate.SubjectDN));
Trace.WriteLine(string.Format(" {0} = {1}", "Subject (CN) ", subject));
Trace.WriteLine(string.Format(" {0} = {1}", "Subject Alt names ", string.Join(", ", subjectAlternativeNames)));
Trace.WriteLine(string.Format(" {0} = {1}", "Friendly Name ", certificateCreationSettings.FriendlyName));
}
Trace.WriteLine(string.Format(" {0} = {1}", "HasPrivateKey:", outputCert.HasPrivateKey));
Trace.WriteLine(string.Format(" {0} = {1}", "Thumbprint", outputCert.Thumbprint));
Trace.WriteLine(string.Format(" {0} = {1}", "CertificateValidityType", certificateCreationSettings.ValidityType));
return container;
}
private X509Crl CreateCrl(X509Certificate signingCertificate)
{
EnsureInitialized();
s_crlGenerator.Reset();
DateTime now = DateTime.UtcNow;
DateTime updateTime = now.Subtract(_crlValidityGracePeriodEnd);
// Ensure that the update time for the CRL is no greater than the earliest time that the CA is valid for
if (_defaultValidityNotBefore > now.Subtract(_crlValidityGracePeriodEnd))
{
updateTime = _defaultValidityNotBefore;
}
s_crlGenerator.SetThisUpdate(updateTime);
//There is no need to update CRL.
s_crlGenerator.SetNextUpdate(now.Add(ValidityPeriod));
s_crlGenerator.SetIssuerDN(PrincipalUtilities.GetSubjectX509Principal(signingCertificate));
s_crlGenerator.AddExtension(X509Extensions.AuthorityKeyIdentifier, false, new AuthorityKeyIdentifierStructure(signingCertificate));
BigInteger crlNumber = new BigInteger(64 /*bits for the number*/, _random).Abs();
s_crlGenerator.AddExtension(X509Extensions.CrlNumber, false, new CrlNumber(crlNumber));
foreach (var kvp in s_revokedCertificates)
{
s_crlGenerator.AddCrlEntry(new BigInteger(kvp.Key, 16), kvp.Value, CrlReason.CessationOfOperation);
}
ISignatureFactory signatureFactory = new Asn1SignatureFactory(_signatureAlgorithm, _authorityKeyPair.Private, _random);
X509Crl crl = s_crlGenerator.Generate(signatureFactory);
crl.Verify(_authorityKeyPair.Public);
Trace.WriteLine(string.Format("[CertificateGenerator] has created a Certificate Revocation List :"));
Trace.WriteLine(string.Format(" {0} = {1}", "Issuer", crl.IssuerDN));
Trace.WriteLine(string.Format(" {0} = {1}", "CRL Number", crlNumber));
return crl;
}
// Throws an exception if the certificate is invalid
private void EnsureCertificateIsValid(X509Certificate certificate)
{
certificate.CheckValidity(DateTime.UtcNow);
certificate.Verify(_authorityKeyPair.Public);
}
private void EnsureInitialized()
{
if (!_isInitialized)
{
Initialize();
}
}
private void EnsureNotInitialized(string paramName)
{
if (_isInitialized)
{
throw new ArgumentException(paramName + " cannot be set as the generator has already been initialized.", paramName);
}
}
private static X509Name CreateX509Name(string canonicalName)
{
X509Name authorityX509Name;
IList authorityKeyIdOrder = new ArrayList();
IDictionary authorityKeyIdName = new Hashtable();
authorityKeyIdOrder.Add(X509Name.OU);
authorityKeyIdOrder.Add(X509Name.O);
authorityKeyIdOrder.Add(X509Name.CN);
authorityKeyIdName.Add(X509Name.CN, canonicalName);
authorityKeyIdName.Add(X509Name.O, "DO_NOT_TRUST");
authorityKeyIdName.Add(X509Name.OU, "Created by https://github.com/dotnet/wcf");
authorityX509Name = new X509Name(authorityKeyIdOrder, authorityKeyIdName);
return authorityX509Name;
}
public bool RevokeCertificateBySerialNumber(string serialNum)
{
bool success = false;
BigInteger serialNumBigInt = null;
try
{
serialNumBigInt = new BigInteger(str: serialNum, radix: 16);
success = true;
}
catch (FormatException)
{
Trace.WriteLine("[CertificateGenerator] RevokeCertificateBySerialNumber:");
Trace.WriteLine(string.Format(" Invalid serial number specified: '{0}'", serialNum));
}
if (success && !s_revokedCertificates.ContainsKey(serialNum))
{
s_revokedCertificates.Add(serialNum, DateTime.UtcNow);
}
// Note that we don't actually check against the thumbprints here, we just go ahead and stick the serial
// number into the CRL without checking whether or not we've ever generated it
Trace.WriteLine(string.Format("[CertificateGenerator] Revoke certificate with serial number {0}: ", success ? "succeeded" : "FAILED"));
return success;
}
}
public class X509CertificateContainer
{
public string Subject { get; internal set; }
internal X509Certificate InternalCertificate { get; set; }
public X509Certificate2 Certificate { get; internal set; }
internal byte[] Pfx { get; set; }
public string Thumbprint { get; internal set; }
}
}