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.
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
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