Files
certmgr/certmgr/CertGen/CertificateGeneratorBase.cs

241 lines
7.4 KiB
C#

using System.Diagnostics;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using CertMgr.Core.Exceptions;
namespace CertMgr.CertGen;
internal abstract class CertificateGeneratorBase<TAlgorithm, TSettings> : ICertificateGenerator
where TAlgorithm : AsymmetricAlgorithm
where TSettings : GeneratorSettings
{
internal CertificateGeneratorBase(GeneratorType type, TSettings settings)
{
Type = type;
Settings = settings;
}
public ValueTask DisposeAsync()
{
return ValueTask.CompletedTask;
}
public GeneratorType Type { [DebuggerStepThrough] get; }
protected TSettings Settings { [DebuggerStepThrough] get; }
public async Task<X509Certificate2> CreateAsync(CertificateSettings settings, CancellationToken cancellationToken)
{
ValidateSettings(settings);
X509Certificate2 cert = await CreateInternalAsync(settings, cancellationToken);
return cert;
}
protected virtual void ValidateSettings(CertificateSettings settings)
{
if (settings.Issuer != null)
{
if (!settings.Issuer.HasPrivateKey)
{
throw new CertGenException("Certificate without private key cannot be used for signing");
}
}
}
protected HashAlgorithmName GetHashAlgorithm()
{
HashAlgorithmName han;
switch (Settings.HashAlgorithm)
{
case HashAlgorithm.Sha256:
han = HashAlgorithmName.SHA256;
break;
case HashAlgorithm.Sha384:
han = HashAlgorithmName.SHA384;
break;
case HashAlgorithm.Sha512:
han = HashAlgorithmName.SHA512;
break;
default:
throw new UnsupportedValueException(Settings.HashAlgorithm);
}
return han;
}
protected abstract TAlgorithm CreateKeyPair();
protected abstract CertificateRequest DoCreateRequest(string subjectName, TAlgorithm keypair);
protected abstract X509Certificate2 JoinPrivateKey(X509Certificate2 publicOnlyCert, TAlgorithm keypair);
protected abstract TAlgorithm? GetPrivateKey(X509Certificate2 cert);
private Task<X509Certificate2> CreateInternalAsync(CertificateSettings settings, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
X509Certificate2 cert;
using (TAlgorithm keypair = CreateKeyPair())
{
CertificateRequest request = CreateRequest(settings, keypair);
DateTimeOffset notBefore = DateTimeOffset.UtcNow.AddSeconds(-1);
DateTimeOffset notAfter = DateTimeOffset.UtcNow.Add(settings.ValidityPeriod);
if (settings.Issuer != null)
{
byte[] serial = GenerateSerial();
X509SignatureGenerator sgen = GetSignatureGenerator(settings.Issuer);
using (X509Certificate2 publicOnlyCert = request.Create(settings.Issuer.SubjectName, sgen, notBefore, notAfter, serial))
{
cert = JoinPrivateKey(publicOnlyCert, keypair);
}
}
else
{
cert = request.CreateSelfSigned(notBefore, notAfter);
}
}
return Task.FromResult(cert);
}
private CertificateRequest CreateRequest(CertificateSettings settings, TAlgorithm keys)
{
string commonName = CreateCommonName(settings.SubjectName);
CertificateRequest request = DoCreateRequest(commonName, keys);
SubjectAlternativeNameBuilder altNames = new SubjectAlternativeNameBuilder();
string subj = commonName.Substring("CN=".Length);
if (!settings.SubjectAlternateNames.Contains(new SubjectAlternateName(SANKind.DNS, subj)))
{
altNames.AddDnsName(subj);
}
foreach (SubjectAlternateName altName in settings.SubjectAlternateNames)
{
switch (altName.Kind)
{
case SANKind.DNS:
altNames.AddDnsName(altName.Value);
break;
case SANKind.IP:
if (IPAddress.TryParse(altName.Value, out IPAddress? ipAddress))
{
altNames.AddIpAddress(ipAddress);
}
break;
default:
throw new UnsupportedValueException(altName.Kind);
}
}
request.CertificateExtensions.Add(altNames.Build());
if (settings.IsCertificateAuthority)
{
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, X509SubjectKeyIdentifierHashAlgorithm.Sha1, false));
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(settings.IsCertificateAuthority, false, 0, false));
}
if (settings.KeyUsage != X509KeyUsageFlags.None)
{
request.CertificateExtensions.Add(new X509KeyUsageExtension(settings.KeyUsage, false));
}
return request;
}
private X509SignatureGenerator GetSignatureGenerator(X509Certificate2 issuerCertificate)
{
X509SignatureGenerator? sgen = null;
ECDsa? ecdsa = issuerCertificate.GetECDsaPrivateKey();
if (ecdsa != null)
{
sgen = X509SignatureGenerator.CreateForECDsa(ecdsa);
}
if (sgen == null)
{
RSA? rsa = issuerCertificate.GetRSAPrivateKey();
if (rsa != null)
{
sgen = X509SignatureGenerator.CreateForRSA(rsa, RSASignaturePadding.Pkcs1);
}
}
if (sgen == null)
{
DSA? dsa = issuerCertificate.GetDSAPrivateKey();
if (dsa != null)
{
throw new CertGenException("Unsupported type of DSA private-key: '{0}'", dsa.GetType().FullName);
}
else
{
try
{
AsymmetricAlgorithm? pk = issuerCertificate.PrivateKey;
throw new CertGenException("Unsupported type of private-key: '{0}'", pk?.GetType().FullName ?? "<null>");
}
catch (CertGenException)
{
throw;
}
catch
{
throw new CertGenException("Unsupported type of private-key");
}
throw new CertGenException("Unsupported type of private-key (no DSA or CAPI)");
}
}
return sgen;
}
private static byte[] GenerateSerial()
{
byte[] serial = new byte[12];
using (RandomNumberGenerator rng = RandomNumberGenerator.Create())
{
rng.GetNonZeroBytes(serial);
}
return serial;
}
private string CreateCommonName(string? subjectName)
{
if (subjectName == null)
{
throw new CertGenException("Cannot create common-name for certificate as subject-name is null");
}
string cn;
if (subjectName.StartsWith("CN=", StringComparison.OrdinalIgnoreCase))
{
cn = subjectName;
}
else
{
cn = string.Format("CN={0}", subjectName);
}
return cn;
}
public override string ToString()
{
return string.Format("Type = {0}", Type);
}
}