dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.91k stars 4.63k forks source link

Windows PKCS12 import allows arbitrary key association #45696

Open vcsjones opened 3 years ago

vcsjones commented 3 years ago

Importing a PKCS12 ("PFX") file on Windows seems very permissive as to what kind of keys and certificate matching is done if the certificate and key contains a LocalKeyId.

For algorithm-to-algorithm, it will work and associate the key when in fact the key does not match the certificate's public key. So GetRSA{Public,Private}Key will return a key that does not match the certificate.

Repro case for matching algorithms ```c# using System; using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using RSA rsa = RSA.Create(); using RSA rsa2 = RSA.Create(); Pkcs9LocalKeyId kid = new Pkcs9LocalKeyId(new byte[] { 1 }); CertificateRequest req = new CertificateRequest("CN=blah", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); using X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now); Pkcs12Builder builder = new Pkcs12Builder(); Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); Pkcs12SafeContents certContents = new Pkcs12SafeContents(); string password = "YabbaDabbaDoo"; PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 2000); Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(rsa2, password, pbeParameters); Pkcs12SafeBag certBag = certContents.AddCertificate(cert); keyBag.Attributes.Add(kid); certBag.Attributes.Add(kid); builder.AddSafeContentsEncrypted(keyContents, password, pbeParameters); builder.AddSafeContentsUnencrypted(certContents); builder.SealWithMac(password, HashAlgorithmName.SHA1, 2000); byte[] pfxBytes = builder.Encode(); using var imported = new X509Certificate2(pfxBytes, password, X509KeyStorageFlags.Exportable); using var importedKey = imported.GetRSAPrivateKey(); // Certificate public key does not match the RSA key. Console.WriteLine(imported.GetPublicKey().AsSpan().SequenceEqual(importedKey.ExportRSAPublicKey())); ```

It gets a little weirder when you have two completely unrelated key algorithms. For example, the certificate is RSA, and the key is ECDSA, and a LocalKeyId associate the two, the import will still succeed.

The certificate will have HasPrivateKey as true. If the certificate has an RSA SPKI, calling GetRSAPrivateKey() will throw:

Unhandled exception. System.ArgumentException: Keys used with the RSACng algorithm must have an algorithm group of RSA. (Parameter 'key') 
   at System.Security.Cryptography.RSACng..ctor(CngKey key)
   at Internal.Cryptography.Pal.CertificatePal.<>c.b__67_1(CngKey cngKey)
   at Internal.Cryptography.Pal.CertificatePal.GetPrivateKey[T](Func`2 createCsp, Func`2 createCng)
   at Internal.Cryptography.Pal.CertificatePal.GetRSAPrivateKey()
   at Internal.Cryptography.Pal.CertificateExtensionsCommon.GetPrivateKey[T](X509Certificate2 certificate, Predicate`1 matchesConstraints)
   at System.Security.Cryptography.X509Certificates.RSACertificateExtensions.GetRSAPrivateKey(X509Certificate2 certificate)
Repro case for mismatched algorithms ```c# using System; using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using RSA rsa = RSA.Create(); using ECDsa ecdsa = ECDsa.Create(); Pkcs9LocalKeyId kid = new Pkcs9LocalKeyId(new byte[] { 1 }); CertificateRequest req = new CertificateRequest("CN=blah", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); using X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now); Pkcs12Builder builder = new Pkcs12Builder(); Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); Pkcs12SafeContents certContents = new Pkcs12SafeContents(); string password = "YabbaDabbaDoo"; PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 2000); Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(ecdsa, password, pbeParameters); Pkcs12SafeBag certBag = certContents.AddCertificate(cert); keyBag.Attributes.Add(kid); certBag.Attributes.Add(kid); builder.AddSafeContentsEncrypted(keyContents, password, pbeParameters); builder.AddSafeContentsUnencrypted(certContents); builder.SealWithMac(password, HashAlgorithmName.SHA1, 2000); byte[] pfxBytes = builder.Encode(); using var imported = new X509Certificate2(pfxBytes, password, X509KeyStorageFlags.Exportable); _ = imported.GetRSAPrivateKey(); // BOOM ```
ghost commented 3 years ago

Tagging subscribers to this area: @bartonjs, @vcsjones, @krwq See info in area-owners.md if you want to be subscribed.

Issue Details
Importing a PKCS12 ("PFX") file on Windows seems very permissive as to what kind of keys and certificate matching is done if the certificate and key contains a LocalKeyId. For algorithm-to-algorithm, it will work and associate the key when in fact the key does not match the certificate's public key. So `GetRSA{Public,Private}Key` will return a key that does not match the certificate.
Repro case for matching algorithms ```c# using System; using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using RSA rsa = RSA.Create(); using RSA rsa2 = RSA.Create(); Pkcs9LocalKeyId kid = new Pkcs9LocalKeyId(new byte[] { 1 }); CertificateRequest req = new CertificateRequest("CN=blah", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); using X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now); Pkcs12Builder builder = new Pkcs12Builder(); Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); Pkcs12SafeContents certContents = new Pkcs12SafeContents(); string password = "YabbaDabbaDoo"; PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 2000); Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(rsa2, password, pbeParameters); Pkcs12SafeBag certBag = certContents.AddCertificate(cert); keyBag.Attributes.Add(kid); certBag.Attributes.Add(kid); builder.AddSafeContentsEncrypted(keyContents, password, pbeParameters); builder.AddSafeContentsUnencrypted(certContents); builder.SealWithMac(password, HashAlgorithmName.SHA1, 2000); byte[] pfxBytes = builder.Encode(); using var imported = new X509Certificate2(pfxBytes, password, X509KeyStorageFlags.Exportable); using var importedKey = imported.GetRSAPrivateKey(); // Certificate public key does not match the RSA key. Console.WriteLine(imported.GetPublicKey().AsSpan().SequenceEqual(importedKey.ExportRSAPublicKey())); ```
It gets a little weirder when you have two completely unrelated key algorithms. For example, the certificate is RSA, and the key is ECDSA, and a LocalKeyId associate the two, the import will still succeed. The certificate will have `HasPrivateKey` as `true`. If the certificate has an RSA SPKI, calling `GetRSAPrivateKey()` will throw:
Unhandled exception. System.ArgumentException: Keys used with the RSACng algorithm must have an algorithm group of RSA. (Parameter 'key') 
   at System.Security.Cryptography.RSACng..ctor(CngKey key)
   at Internal.Cryptography.Pal.CertificatePal.<>c.b__67_1(CngKey cngKey)
   at Internal.Cryptography.Pal.CertificatePal.GetPrivateKey[T](Func`2 createCsp, Func`2 createCng)
   at Internal.Cryptography.Pal.CertificatePal.GetRSAPrivateKey()
   at Internal.Cryptography.Pal.CertificateExtensionsCommon.GetPrivateKey[T](X509Certificate2 certificate, Predicate`1 matchesConstraints)
   at System.Security.Cryptography.X509Certificates.RSACertificateExtensions.GetRSAPrivateKey(X509Certificate2 certificate)
Repro case for mismatched algorithms ```c# using System; using System.Security.Cryptography; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.X509Certificates; using RSA rsa = RSA.Create(); using ECDsa ecdsa = ECDsa.Create(); Pkcs9LocalKeyId kid = new Pkcs9LocalKeyId(new byte[] { 1 }); CertificateRequest req = new CertificateRequest("CN=blah", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); using X509Certificate2 cert = req.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now); Pkcs12Builder builder = new Pkcs12Builder(); Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); Pkcs12SafeContents certContents = new Pkcs12SafeContents(); string password = "YabbaDabbaDoo"; PbeParameters pbeParameters = new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 2000); Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(ecdsa, password, pbeParameters); Pkcs12SafeBag certBag = certContents.AddCertificate(cert); keyBag.Attributes.Add(kid); certBag.Attributes.Add(kid); builder.AddSafeContentsEncrypted(keyContents, password, pbeParameters); builder.AddSafeContentsUnencrypted(certContents); builder.SealWithMac(password, HashAlgorithmName.SHA1, 2000); byte[] pfxBytes = builder.Encode(); using var imported = new X509Certificate2(pfxBytes, password, X509KeyStorageFlags.Exportable); _ = imported.GetRSAPrivateKey(); // BOOM ```
Author: vcsjones
Assignees: -
Labels: `area-System.Security`, `untriaged`
Milestone: -
vcsjones commented 3 years ago

Found this while working on #44535. Seems like behavior we don't want to replicate in Linux / macOS.

This was tested in Windows and assumed to not work, but that is because the test was coded to use ephemeral key imports, which was preventing the import, not this specific scenario.

vcsjones commented 3 years ago

Hm, well, the former case of RSA-and-RSA seems to be tested here:

https://github.com/dotnet/runtime/blob/eb25a59de3ea4fa1c24544e876554be96afa5280/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs#L198

So it appears to be a known, albeit odd, behavior. The latter case however seems odd. I'm not sure it is expected that GetRSAPrivateKey will throw. Granted this is also a pretty specific case of a bad PKCS12 file, so perhaps all of this is acceptable behavior.