Skip to content

Instantly share code, notes, and snippets.

@borland
Created January 14, 2021 07:30
Show Gist options
  • Save borland/5cf356d76904bbb7e83c156e9359dca6 to your computer and use it in GitHub Desktop.
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
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