ssl-certificatebouncycastlepublic-key-encryptionx509certificate2

My exportable private key isn't exportable



Note: the answer can come in either VB.NET or C#. I have no preference for this Q&A.


I'm using the Certes and Bouncy Castle-based code below to finalize, generate and import a Let's Encrypt TLS certificate into my CurrentUser store. The problem is that even though I'm marking the private key as exportable, it's not found in the resulting certificate.

As we can see, the option to export the private key is disabled, even though the store indicates that the key is present.

enter image description here

enter image description here

I've worked through this example, and the code seems to be doing pretty much the same thing as mine. The only difference that stands out is that it's loading up an RsaKeyPairGenerator to generate a new key pair. That's not applicable in my case, since I'm not creating my own certificate and I already have the cert and key Byte arrays.

Am I missing something or doing something wrong in my code? I'm not spotting it. How do I generate and store an X509Certificate2 instance whose private key is visible and exportable?

Here's my code:


Public Class Tls
  Public Shared Async Function ImportAsync(NewCertificate As X509Certificate2) As Task(Of Result)
    Dim oCollection As X509Certificate2Collection
    Dim oQuery As Func(Of X509Certificate2, Boolean)

    Await Task.CompletedTask

    oQuery = Function(OldCertificate) OldCertificate.Subject = NewCertificate.Subject

    Using oStore As New X509Store(StoreName.My, StoreLocation.CurrentUser)
      oStore.Open(OpenFlags.ReadWrite)
      oCollection = New X509Certificate2Collection(oStore.Certificates.Where(oQuery).ToArray)
      oStore.RemoveRange(oCollection)
      oStore.Add(NewCertificate)
    End Using

    Return Result.Ok
  End Function

  Public Shared Async Function GenerateAsync(Order As IOrderContext, CityCode As String) As Task(Of Result(Of X509Certificate2))
    Dim oCertificateChain As CertificateChain
    Dim oCertificate As X509Certificate2
    Dim oPrivateKey As IKey
    Dim oInfo As CsrInfo

    oInfo = New CsrInfo With {
      .OrganizationUnit = "MyUnit",
      .Organization = "MyOrg",
      .CountryName = "MyCountry",
      .Locality = "MyTown",
      .State = "MyState"
    }

    oPrivateKey = KeyFactory.NewKey(KeyAlgorithm.RS256)
    oCertificateChain = Await Order.Generate(oInfo, oPrivateKey)
    oCertificate = GetSignedCertificate(oCertificateChain.Certificate.ToDer, oPrivateKey.ToDer, CityCode)

    Return oCertificate.ToResult
  End Function

  Private Shared Function GetSignedCertificate(Certificate As Byte(), PrivateKey As Byte(), Password As String) As X509Certificate2
    Dim oUnsignedCertificate As X509.X509Certificate
    Dim oSignedCertificate As X509Certificate2
    Dim oCertificateParser As X509CertificateParser
    Dim oCertificateEntry As X509CertificateEntry
    Dim oPrivateKey As PrivateKeyInfo
    Dim oParameter As AsymmetricKeyParameter
    Dim oKeyEntry As AsymmetricKeyEntry
    Dim oPfxStore As Pkcs12Store
    Dim sSubject As String
    Dim oRandom As SecureRandom
    Dim eFlags As X509KeyStorageFlags

    ' Prepare the private key
    oPrivateKey = PrivateKeyInfo.GetInstance(PrivateKey)
    oParameter = PrivateKeyFactory.CreateKey(oPrivateKey)
    oKeyEntry = New AsymmetricKeyEntry(oParameter)
    oRandom = New SecureRandom
    eFlags = X509KeyStorageFlags.Exportable Or X509KeyStorageFlags.PersistKeySet Or X509KeyStorageFlags.UserKeySet

    ' Convert to PKCS#12 format
    oCertificateParser = New X509CertificateParser
    oUnsignedCertificate = oCertificateParser.ReadCertificate(Certificate)
    oCertificateEntry = New X509CertificateEntry(oUnsignedCertificate)
    sSubject = oUnsignedCertificate.SubjectDN.ToString
    oPfxStore = New Pkcs12Store
    oPfxStore.SetCertificateEntry(sSubject, oCertificateEntry)
    oPfxStore.SetKeyEntry($"{sSubject}_key", oKeyEntry, {oCertificateEntry})

    Using oStream As New MemoryStream
      oPfxStore.Save(oStream, Password.ToCharArray, oRandom)
      oSignedCertificate = New X509Certificate2(oStream.ToArray, Password, eFlags)
    End Using

    Return oSignedCertificate
  End Function
End Class

Solution

  • After much cobbling together of snippets from various and sundry sources (too many to recall), I came up with this. It doesn't include nearly as much Bouncy Castle stuff, but it works.

    Private Shared Function GetSignedCertificate(Certificate As Byte(), PrivateKey As Byte()) As X509Certificate2
      Dim oSecondaryRsa As RSACryptoServiceProvider
      Dim oCertificate As X509Certificate2
      Dim oPrimaryRsa As RSACryptoServiceProvider
      Dim oParameters As CspParameters
      Dim oPrivateKey As PrivateKeyInfo
      Dim oParameter As AsymmetricKeyParameter
      Dim oKeyEntry As AsymmetricKeyEntry
      Dim eFlags As X509KeyStorageFlags
    
      oPrivateKey = PrivateKeyInfo.GetInstance(PrivateKey)
      oParameter = PrivateKeyFactory.CreateKey(oPrivateKey)
      oKeyEntry = New AsymmetricKeyEntry(oParameter)
      oSecondaryRsa = DotNetUtilities.ToRSA(DirectCast(oKeyEntry.Key, RsaPrivateCrtKeyParameters))
    
      oParameters = New CspParameters With
      {
        .KeyContainerName = Guid.NewGuid.ToString.ToUpperInvariant,
        .ProviderType = 1,
        .Flags = CspProviderFlags.UseMachineKeyStore
      }
    
      oPrimaryRsa = New RSACryptoServiceProvider(oParameters)
      oPrimaryRsa.ImportCspBlob(oSecondaryRsa.ExportCspBlob(True))
      oPrimaryRsa.PersistKeyInCsp = True
    
      eFlags = X509KeyStorageFlags.Exportable Or X509KeyStorageFlags.PersistKeySet Or X509KeyStorageFlags.UserKeySet
      oCertificate = New X509Certificate2(Certificate, String.Empty, eFlags)
    
      Return oCertificate.CopyWithPrivateKey(oPrimaryRsa)
    End Function