241 lines
7.4 KiB
C#
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);
|
|
}
|
|
}
|