Created
January 14, 2021 07:30
-
-
Save borland/5cf356d76904bbb7e83c156e9359dca6 to your computer and use it in GitHub Desktop.
Given a public key, create a signed certificate tied back to a certificate authority; .NET Core using BouncyCastle
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Org.BouncyCastle.Asn1; | |
using Org.BouncyCastle.Asn1.X509; | |
using Org.BouncyCastle.Crypto; | |
using Org.BouncyCastle.Crypto.Operators; | |
using Org.BouncyCastle.Crypto.Parameters; | |
using Org.BouncyCastle.Math; | |
using Org.BouncyCastle.X509; | |
using System; | |
using System.Diagnostics; | |
using System.Linq; | |
using System.Security.Cryptography; | |
using System.Security.Cryptography.X509Certificates; | |
namespace Services | |
{ | |
public class CertificateSigningService | |
{ | |
// ReSharper disable once InconsistentNaming | |
const int CERT_VALID_YEARS = 20; | |
// ReSharper disable once InconsistentNaming | |
const string szOID_SUBJECT_KEY_IDENTIFIER = "2.5.29.14"; | |
// ReSharper disable once InconsistentNaming | |
const string x509NameInvalidChars = ",+=\"\r<>#;"; | |
private readonly X509Certificate2 m_issuerCertificate; | |
static CertificateSigningService() | |
{ | |
// Add 's' as a valid alternative to 'st' for X509Names (bouncy castle only accepts 'st') | |
X509Name.DefaultLookup.Add("s", X509Name.ST); | |
} | |
public CertificateSigningService(string path, string password) | |
{ | |
m_issuerCertificate = GetIssuerCertificate(path, password) ?? | |
throw new InvalidOperationException("Can't load issuer certificate"); | |
} | |
public byte[] CreateSignedClientCertificate(string name, byte[] clientPublicKey) | |
{ | |
return GenerateClientCertificate(m_issuerCertificate, name, clientPublicKey) ?? | |
throw new InvalidOperationException("Can't generate client certificate"); | |
} | |
/// <summary> | |
/// Retrieve the issuer (CA) certificate that will sign the client certificate | |
/// </summary> | |
/// <returns>The issuer certificate</returns> | |
public static X509Certificate2 GetIssuerCertificate(string path, string password) => | |
new X509Certificate2(path, password, X509KeyStorageFlags.Exportable | X509KeyStorageFlags.PersistKeySet); | |
/// <summary> | |
/// Generate a client certificate using the specified issuer certificate | |
/// </summary> | |
/// <param name="issuerCertificate">The issuer certificate</param> | |
/// <param name="name">The name of the client certificate</param> | |
/// <param name="clientPublicKey">The public key of the client certificate</param> | |
/// <returns>Raw data representing the signed client certificate</returns> | |
public static byte[] GenerateClientCertificate(X509Certificate2 issuerCertificate, string name, byte[] clientPublicKey) | |
{ | |
// Remove characters that are not valid in an X500 name | |
var cleanedName = new string(name.Where(ch => x509NameInvalidChars.IndexOf(ch) == -1).ToArray()); | |
// Derive a certificate subject name from the item name | |
return CreateClientCertificate(issuerCertificate, $"CN={cleanedName}, OU=YourOrganization", Guid.NewGuid().ToByteArray(), clientPublicKey); | |
} | |
/// <summary> | |
/// Create a client certificate containing the specified public key signed by the specified issuer | |
/// </summary> | |
/// <param name="issuer">The issuer certificate</param> | |
/// <param name="subjectName">The subject name for the client certificate</param> | |
/// <param name="serialNumber">The certificate serial number</param> | |
/// <param name="publicKey">The asn1 (PKCS#1) encoded public key bytes</param> | |
/// <returns>The client certificate bytes, null for error</returns> | |
public static byte[] CreateClientCertificate(X509Certificate2 issuer, string subjectName, byte[] serialNumber, byte[] publicKey) | |
{ | |
// make sure we have proper input parameters | |
Debug.Assert(issuer != null); | |
Debug.Assert(subjectName != null); | |
Debug.Assert(serialNumber != null); | |
Debug.Assert(publicKey != null); | |
if (publicKey.Length != CertificateHelpers.PublicKeyExpectedLength) | |
{ | |
throw new ArgumentException($"Public key has invalid length of {publicKey.Length}", nameof(publicKey)); | |
} | |
var clientCert = new X509V3CertificateGenerator(); | |
// serial number - we force this to be a positive number by clearing the sign flag; Negative serial numbers aren't allowed | |
serialNumber[0] = (byte)(serialNumber[0] & 0b01111111); | |
clientCert.SetSerialNumber(new BigInteger(serialNumber)); | |
// we must reverse the order of the fields in the string representation; Apparently bouncy castle has a different opinion than does nginx or openssl | |
// https://stackoverflow.com/a/39841564/234 (for the string representation of a DN there are two standards: OpenSSL shows the attributes by default as they are actually stored in the certificate, while RFC 2253/4514 reverses the order) | |
var reversedSubjectName = string.Join(", ", issuer.Subject.Split(',').Select(s => s.Trim()).Reverse()); | |
// issuer and subject | |
clientCert.SetIssuerDN(new X509Name(reversedSubjectName)); | |
clientCert.SetSubjectDN(new X509Name(subjectName)); | |
// valid dates - cert lasts about 20 years | |
var validFrom = new DateTime(2000, 1, 1).ToUniversalTime(); | |
clientCert.SetNotBefore(validFrom); | |
var validTo = DateTime.UtcNow.AddYears(CERT_VALID_YEARS); | |
clientCert.SetNotAfter(validTo); | |
// client public key - the modulus and exponent make up the RSA public key | |
var clientAsnEnumerator = Asn1Sequence.GetInstance(publicKey).GetEnumerator(); | |
var modulus = (DerInteger)(clientAsnEnumerator.MoveNext() ? | |
clientAsnEnumerator.Current : | |
throw new MissingMemberException("Cannot get modulus for client certificate")); | |
var exponent = (DerInteger)(clientAsnEnumerator.MoveNext() ? | |
clientAsnEnumerator.Current : | |
throw new MissingMemberException("Cannot get exponent for client certificate")); | |
var publicKeyParameters = new RsaKeyParameters(false, modulus.Value, exponent.Value); | |
clientCert.SetPublicKey(publicKeyParameters); | |
// extension - Basic Constraints - Not a CA (client certificate) | |
clientCert.AddExtension( | |
X509Extensions.BasicConstraints, | |
true, | |
new BasicConstraints(false)); | |
// extension - Key Usage - Key can encipher Data & Keys and create digital signatures | |
clientCert.AddExtension( | |
X509Extensions.KeyUsage, | |
true, | |
new X509KeyUsage( | |
X509KeyUsage.DataEncipherment | | |
X509KeyUsage.DigitalSignature | | |
X509KeyUsage.KeyEncipherment)); | |
// extension - Extended key usage - Only for client authentication | |
clientCert.AddExtension( | |
X509Extensions.ExtendedKeyUsage, | |
true, | |
new ExtendedKeyUsage(KeyPurposeID.IdKPClientAuth)); | |
// extension - Subject key identifier - SHA1 hash of the public key bytes | |
byte[] publicKeyHash; | |
using (var sha = SHA1.Create()) { | |
publicKeyHash = sha.ComputeHash(publicKey); | |
} | |
clientCert.AddExtension( | |
X509Extensions.SubjectKeyIdentifier, | |
false, | |
new SubjectKeyIdentifier(publicKeyHash)); | |
// extension - Authority key identifier - The issuer subject identifier | |
foreach (System.Security.Cryptography.X509Certificates.X509Extension extension in issuer.Extensions) { | |
if (extension.Oid.Value == szOID_SUBJECT_KEY_IDENTIFIER) { | |
clientCert.AddExtension(X509Extensions.AuthorityKeyIdentifier, | |
false, | |
new AuthorityKeyIdentifier(extension.RawData.Skip(2).ToArray())); // remove the first 2 bytes since they are an asn1 header | |
break; | |
} | |
} | |
// issuer private key - export the key from .net crypto to bouncycastle | |
var issuerKeyPair = GetRsaKeyPair(issuer.GetRSAPrivateKey().ExportParameters(true)); | |
// generate and sign the cert using the issuer private key | |
var asnSignatureFactory = new Asn1SignatureFactory("SHA256WITHRSA", issuerKeyPair.Private); | |
var signedClientCert = clientCert.Generate(asnSignatureFactory); | |
// check and verify the validity of the certificate | |
signedClientCert.CheckValidity(); | |
signedClientCert.Verify(issuerKeyPair.Public); | |
return signedClientCert.GetEncoded(); | |
} | |
private static AsymmetricCipherKeyPair GetRsaKeyPair(RSAParameters rp) | |
{ | |
BigInteger modulus = new BigInteger(1, rp.Modulus); | |
BigInteger pubExp = new BigInteger(1, rp.Exponent); | |
RsaKeyParameters pubKey = new RsaKeyParameters( | |
false, | |
modulus, | |
pubExp); | |
RsaPrivateCrtKeyParameters privKey = new RsaPrivateCrtKeyParameters( | |
modulus, | |
pubExp, | |
new BigInteger(1, rp.D), | |
new BigInteger(1, rp.P), | |
new BigInteger(1, rp.Q), | |
new BigInteger(1, rp.DP), | |
new BigInteger(1, rp.DQ), | |
new BigInteger(1, rp.InverseQ)); | |
return new AsymmetricCipherKeyPair(pubKey, privKey); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment