diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/AndroidCertificatePal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/AndroidCertificatePal.cs index 658c2da9e1a76..2a987506f1203 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/AndroidCertificatePal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/AndroidCertificatePal.cs @@ -52,6 +52,7 @@ public static ICertificatePal FromBlob(ReadOnlySpan rawData, SafePasswordH { Debug.Assert(password != null); + bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); X509ContentType contentType = X509Certificate2.GetCertContentType(rawData); switch (contentType) @@ -62,7 +63,7 @@ public static ICertificatePal FromBlob(ReadOnlySpan rawData, SafePasswordH // We don't support determining this on Android right now, so we throw. throw new CryptographicException(SR.Cryptography_X509_PKCS7_NoSigner); case X509ContentType.Pkcs12: - return ReadPkcs12(rawData, password); + return ReadPkcs12(rawData, password, ephemeralSpecified); case X509ContentType.Cert: default: { @@ -104,11 +105,11 @@ internal static bool TryReadX509(ReadOnlySpan rawData, [NotNullWhen(true)] return true; } - private static ICertificatePal ReadPkcs12(ReadOnlySpan rawData, SafePasswordHandle password) + private static ICertificatePal ReadPkcs12(ReadOnlySpan rawData, SafePasswordHandle password, bool ephemeralSpecified) { using (var reader = new AndroidPkcs12Reader(rawData)) { - reader.Decrypt(password); + reader.Decrypt(password, ephemeralSpecified); UnixPkcs12Reader.CertAndKey certAndKey = reader.GetSingleCert(); AndroidCertificatePal pal = (AndroidCertificatePal)certAndKey.Cert!; diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.cs index 09ec3ce538955..6187979d905f8 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.cs @@ -23,9 +23,11 @@ public static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle Debug.Assert(password != null); X509ContentType contentType = X509Certificate2.GetCertContentType(rawData); + bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); + if (contentType == X509ContentType.Pkcs12) { - ICertificatePal[] certPals = ReadPkcs12Collection(rawData, password); + ICertificatePal[] certPals = ReadPkcs12Collection(rawData, password, ephemeralSpecified); return new AndroidCertLoader(certPals); } else @@ -108,11 +110,14 @@ public static IStorePal FromSystemStore(string storeName, StoreLocation storeLoc throw new CryptographicException(message, new PlatformNotSupportedException(message)); } - private static ICertificatePal[] ReadPkcs12Collection(ReadOnlySpan rawData, SafePasswordHandle password) + private static ICertificatePal[] ReadPkcs12Collection( + ReadOnlySpan rawData, + SafePasswordHandle password, + bool ephemeralSpecified) { using (var reader = new AndroidPkcs12Reader(rawData)) { - reader.Decrypt(password); + reader.Decrypt(password, ephemeralSpecified); ICertificatePal[] certs = new ICertificatePal[reader.GetCertCount()]; int idx = 0; diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.Pkcs12.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.Pkcs12.cs index 4febd7080f082..1eaa3f8323adc 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.Pkcs12.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.Pkcs12.cs @@ -19,7 +19,7 @@ internal sealed partial class AppleCertificatePal : ICertificatePal { using (ApplePkcs12Reader reader = new ApplePkcs12Reader(rawData)) { - reader.Decrypt(password); + reader.Decrypt(password, ephemeralSpecified: false); UnixPkcs12Reader.CertAndKey certAndKey = reader.GetSingleCert(); AppleCertificatePal pal = (AppleCertificatePal)certAndKey.Cert!; diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.cs index 86bbebc594d47..e7cbefeb5b4d5 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.cs @@ -47,7 +47,7 @@ public static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle ? Interop.AppleCrypto.SecKeychainCopyDefault() : Interop.AppleCrypto.CreateTemporaryKeychain(); - return ImportPkcs12(rawData, password, exportable, keychain); + return ImportPkcs12(rawData, password, exportable, ephemeralSpecified: false, keychain); } SafeCFArrayHandle certs = Interop.AppleCrypto.X509ImportCollection( @@ -64,13 +64,14 @@ public static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle ReadOnlySpan rawData, SafePasswordHandle password, bool exportable, + bool ephemeralSpecified, SafeKeychainHandle keychain) { ApplePkcs12Reader reader = new ApplePkcs12Reader(rawData); try { - reader.Decrypt(password); + reader.Decrypt(password, ephemeralSpecified); return new ApplePkcs12CertLoader(reader, keychain, password, exportable); } catch diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs index e5561d9279c68..942c6251f6cba 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs @@ -49,12 +49,13 @@ public static ICertificatePal FromBlob(ReadOnlySpan rawData, SafePasswordH ICertificatePal? cert; Exception? openSslException; + bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); if (TryReadX509Der(rawData, out cert) || TryReadX509Pem(rawData, out cert) || PkcsFormatReader.TryReadPkcs7Der(rawData, out cert) || PkcsFormatReader.TryReadPkcs7Pem(rawData, out cert) || - PkcsFormatReader.TryReadPkcs12(rawData, password, out cert, out openSslException)) + PkcsFormatReader.TryReadPkcs12(rawData, password, ephemeralSpecified, out cert, out openSslException)) { if (cert == null) { @@ -73,6 +74,7 @@ public static ICertificatePal FromBlob(ReadOnlySpan rawData, SafePasswordH public static ICertificatePal FromFile(string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags) { ICertificatePal? pal; + bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); // If we can't open the file, fail right away. using (SafeBioHandle fileBio = Interop.Crypto.BioNewFile(fileName, "rb")) @@ -87,6 +89,7 @@ public static ICertificatePal FromFile(string fileName, SafePasswordHandle passw PkcsFormatReader.TryReadPkcs12( File.ReadAllBytes(fileName), password, + ephemeralSpecified, out pal, out Exception? exception); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/PkcsFormatReader.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/PkcsFormatReader.cs index 31a65038d598b..f73b59373233d 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/PkcsFormatReader.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/PkcsFormatReader.cs @@ -253,24 +253,49 @@ internal static bool TryReadPkcs7Pem(SafeBioHandle bio, [NotNullWhen(true)] out return true; } - internal static bool TryReadPkcs12(ReadOnlySpan rawData, SafePasswordHandle password, [NotNullWhen(true)] out ICertificatePal? certPal, out Exception? openSslException) + internal static bool TryReadPkcs12( + ReadOnlySpan rawData, + SafePasswordHandle password, + bool ephemeralSpecified, + [NotNullWhen(true)] out ICertificatePal? certPal, + out Exception? openSslException) { List? ignored; - return TryReadPkcs12(rawData, password, true, out certPal!, out ignored, out openSslException); + return TryReadPkcs12( + rawData, + password, + single: true, + ephemeralSpecified, + out certPal!, + out ignored, + out openSslException); } - internal static bool TryReadPkcs12(ReadOnlySpan rawData, SafePasswordHandle password, [NotNullWhen(true)] out List? certPals, out Exception? openSslException) + internal static bool TryReadPkcs12( + ReadOnlySpan rawData, + SafePasswordHandle password, + bool ephemeralSpecified, + [NotNullWhen(true)] out List? certPals, + out Exception? openSslException) { ICertificatePal? ignored; - return TryReadPkcs12(rawData, password, false, out ignored, out certPals!, out openSslException); + return TryReadPkcs12( + rawData, + password, + single: false, + ephemeralSpecified, + out ignored, + out certPals!, + out openSslException); } private static bool TryReadPkcs12( ReadOnlySpan rawData, SafePasswordHandle password, bool single, + bool ephemeralSpecified, out ICertificatePal? readPal, out List? readCerts, out Exception? openSslException) @@ -287,7 +312,7 @@ internal static bool TryReadPkcs12(ReadOnlySpan rawData, SafePasswordHandl using (pfx) { - return TryReadPkcs12(pfx, password, single, out readPal, out readCerts); + return TryReadPkcs12(pfx, password, single, ephemeralSpecified, out readPal, out readCerts); } } @@ -295,10 +320,11 @@ internal static bool TryReadPkcs12(ReadOnlySpan rawData, SafePasswordHandl OpenSslPkcs12Reader pfx, SafePasswordHandle password, bool single, + bool ephemeralSpecified, out ICertificatePal? readPal, out List? readCerts) { - pfx.Decrypt(password); + pfx.Decrypt(password, ephemeralSpecified); if (single) { diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs index 749beed10edc8..bef72007c383a 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs @@ -23,6 +23,7 @@ public static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle Debug.Assert(password != null); ICertificatePal? singleCert; + bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); if (OpenSslX509CertificateReader.TryReadX509Der(rawData, out singleCert) || OpenSslX509CertificateReader.TryReadX509Pem(rawData, out singleCert)) @@ -39,7 +40,7 @@ public static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle if (PkcsFormatReader.TryReadPkcs7Der(rawData, out certPals) || PkcsFormatReader.TryReadPkcs7Pem(rawData, out certPals) || - PkcsFormatReader.TryReadPkcs12(rawData, password, out certPals, out openSslException)) + PkcsFormatReader.TryReadPkcs12(rawData, password, ephemeralSpecified, out certPals, out openSslException)) { Debug.Assert(certPals != null); @@ -52,15 +53,21 @@ public static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle public static ILoaderPal FromFile(string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags) { + bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); + using (SafeBioHandle bio = Interop.Crypto.BioNewFile(fileName, "rb")) { Interop.Crypto.CheckValidOpenSslHandle(bio); - return FromBio(fileName, bio, password); + return FromBio(fileName, bio, password, ephemeralSpecified); } } - private static ILoaderPal FromBio(string fileName, SafeBioHandle bio, SafePasswordHandle password) + private static ILoaderPal FromBio( + string fileName, + SafeBioHandle bio, + SafePasswordHandle password, + bool ephemeralSpecified) { int bioPosition = Interop.Crypto.BioTell(bio); Debug.Assert(bioPosition >= 0); @@ -104,7 +111,7 @@ private static ILoaderPal FromBio(string fileName, SafeBioHandle bio, SafePasswo // Capture the exception so in case of failure, the call to BioSeek does not override it. Exception? openSslException; byte[] data = File.ReadAllBytes(fileName); - if (PkcsFormatReader.TryReadPkcs12(data, password, out certPals, out openSslException)) + if (PkcsFormatReader.TryReadPkcs12(data, password, ephemeralSpecified, out certPals, out openSslException)) { return ListToLoaderPal(certPals); } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixPkcs12Reader.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixPkcs12Reader.cs index 5d4783dbc29e8..bf6cb9ce630d0 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixPkcs12Reader.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixPkcs12Reader.cs @@ -26,6 +26,7 @@ internal abstract class UnixPkcs12Reader : IDisposable private CertAndKey[]? _certs; private int _certCount; private PointerMemoryManager? _tmpManager; + private bool _allowDoubleBind; protected abstract ICertificatePalCore ReadX509Der(ReadOnlyMemory data); protected abstract AsymmetricAlgorithm LoadKey(ReadOnlyMemory safeBagBagValue); @@ -180,11 +181,13 @@ private static void ReturnRentedContentInfos(ContentInfoAsn[] rentedContents) ArrayPool.Shared.Return(rentedContents, clearArray: true); } - public void Decrypt(SafePasswordHandle password) + public void Decrypt(SafePasswordHandle password, bool ephemeralSpecified) { ReadOnlyMemory authSafeContents = Helpers.DecodeOctetStringAsMemory(_pfxAsn.AuthSafe.Content); + _allowDoubleBind = !ephemeralSpecified; + bool hasRef = false; password.DangerousAddRef(ref hasRef); @@ -314,6 +317,7 @@ private void Decrypt(ReadOnlySpan password, ReadOnlyMemory authSafeC ExtractPrivateKeys(password, keyBags, keyBagIdx, keys, publicKeyInfos); BuildCertsWithKeys( + password, certBags, certBagAttrs, certs, @@ -497,6 +501,7 @@ private static ContentInfoAsn[] DecodeSafeContents(ReadOnlyMemory authSafe } private void BuildCertsWithKeys( + ReadOnlySpan password, CertBagAsn[] certBags, AttributeAsn[]?[] certBagAttrs, CertAndKey[] certs, @@ -550,13 +555,26 @@ private static ContentInfoAsn[] DecodeSafeContents(ReadOnlyMemory authSafe if (matchingKeyIdx != -1) { + // Windows compat: + // If the PFX is loaded with EphemeralKeySet, don't allow double-bind. + // Otherwise, reload the key so a second instance is bound (avoiding one + // cert Dispose removing the key of another). if (keys[matchingKeyIdx] == null) { - throw new CryptographicException(SR.Cryptography_Pfx_BadKeyReference); + if (_allowDoubleBind) + { + certs[certBagIdx].Key = LoadKey(keyBags[matchingKeyIdx], password); + } + else + { + throw new CryptographicException(SR.Cryptography_Pfx_BadKeyReference); + } + } + else + { + certs[certBagIdx].Key = keys[matchingKeyIdx]; + keys[matchingKeyIdx] = null; } - - certs[certBagIdx].Key = keys[matchingKeyIdx]; - keys[matchingKeyIdx] = null; } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/AppleCertificatePal.ImportExport.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/AppleCertificatePal.ImportExport.cs index dfdb5b554f4a7..ad4baa38cc3f7 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/AppleCertificatePal.ImportExport.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/AppleCertificatePal.ImportExport.cs @@ -103,6 +103,8 @@ internal static X509ContentType GetDerCertContentType(ReadOnlySpan rawData { Debug.Assert(password != null); + bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); + if (contentType == X509ContentType.Pkcs7) { throw new CryptographicException( @@ -116,7 +118,7 @@ internal static X509ContentType GetDerCertContentType(ReadOnlySpan rawData // We ignore keyStorageFlags which is tracked in https://github.com/dotnet/runtime/issues/52434. // The keys are always imported as ephemeral and never persisted. Exportability is ignored for // the moment and it needs to be investigated how to map it to iOS keychain primitives. - return ImportPkcs12(rawData, password); + return ImportPkcs12(rawData, password, ephemeralSpecified); } SafeSecIdentityHandle identityHandle; diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/AppleCertificatePal.Pkcs12.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/AppleCertificatePal.Pkcs12.cs index 87e45b117e836..8ff5988f557d2 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/AppleCertificatePal.Pkcs12.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/AppleCertificatePal.Pkcs12.cs @@ -22,11 +22,12 @@ internal sealed partial class AppleCertificatePal : ICertificatePal private static AppleCertificatePal ImportPkcs12( ReadOnlySpan rawData, - SafePasswordHandle password) + SafePasswordHandle password, + bool ephemeralSpecified) { using (ApplePkcs12Reader reader = new ApplePkcs12Reader(rawData)) { - reader.Decrypt(password); + reader.Decrypt(password, ephemeralSpecified); return ImportPkcs12(reader.GetSingleCert()); } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/StorePal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/StorePal.cs index efb7d68349a00..47d7485c0f984 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/StorePal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.iOS/StorePal.cs @@ -37,6 +37,7 @@ public static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle return new CertCollectionLoader(certificateList); } + bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); X509ContentType contentType = AppleCertificatePal.GetDerCertContentType(rawData); if (contentType == X509ContentType.Pkcs7) @@ -52,7 +53,7 @@ public static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle try { - reader.Decrypt(password); + reader.Decrypt(password, ephemeralSpecified); return new ApplePkcs12CertLoader(reader, password); } catch diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs index d9a40d068f035..240c0209d21e5 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs @@ -29,7 +29,11 @@ public abstract partial class PfxFormatTests protected static readonly X509KeyStorageFlags s_exportableImportFlags = s_importFlags | X509KeyStorageFlags.Exportable; + protected static readonly X509KeyStorageFlags s_perphemeralImportFlags = + X509KeyStorageFlags.UserKeySet; + private static readonly Pkcs9LocalKeyId s_keyIdOne = new Pkcs9LocalKeyId(new byte[] { 1 }); + private static readonly Pkcs9LocalKeyId s_keyIdTwo = new Pkcs9LocalKeyId(new byte[] { 2 }); // Windows 7 and 8.1 both fail the PFX load if any key is unloadable (even if not referenced). // Windows 10 1903, 1809, and 1607 all only fail when an unloadable key was actually needed. @@ -41,10 +45,36 @@ public abstract partial class PfxFormatTests OperatingSystem.IsWindows() && !PlatformDetection.IsWindows10Version1607OrGreater; + private void ReadPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedCert, + Action otherWork = null) + { + ReadPfx(pfxBytes, correctPassword, expectedCert, s_importFlags, otherWork); + } + + private void ReadMultiPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedSingleCert, + X509Certificate2[] expectedOrder, + Action perCertOtherWork = null) + { + ReadMultiPfx( + pfxBytes, + correctPassword, + expectedSingleCert, + expectedOrder, + s_importFlags, + perCertOtherWork); + } + protected abstract void ReadPfx( byte[] pfxBytes, string correctPassword, X509Certificate2 expectedCert, + X509KeyStorageFlags nonExportFlags, Action otherWork = null); protected abstract void ReadMultiPfx( @@ -52,14 +82,26 @@ public abstract partial class PfxFormatTests string correctPassword, X509Certificate2 expectedSingleCert, X509Certificate2[] expectedOrder, + X509KeyStorageFlags nonExportFlags, Action perCertOtherWork = null); + private void ReadUnreadablePfx( + byte[] pfxBytes, + string bestPassword, + // NTE_FAIL + int win32Error = -2146893792, + int altWin32Error = 0) + { + ReadUnreadablePfx(pfxBytes, bestPassword, s_importFlags, win32Error, altWin32Error); + } + protected abstract void ReadEmptyPfx(byte[] pfxBytes, string correctPassword); protected abstract void ReadWrongPassword(byte[] pfxBytes, string wrongPassword); protected abstract void ReadUnreadablePfx( byte[] pfxBytes, string bestPassword, + X509KeyStorageFlags importFlags, // NTE_FAIL int win32Error = -2146893792, int altWin32Error = 0); @@ -657,7 +699,7 @@ public void SameCertTwice_NoKeys(bool addLocalKeyId) [Fact] public void TwoCerts_CrossedKeys() { - string pw = nameof(SameCertTwice_NoKeys); + string pw = nameof(TwoCerts_CrossedKeys); using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) using (var cert2 = new X509Certificate2(TestData.MsCertificate)) @@ -680,12 +722,48 @@ public void TwoCerts_CrossedKeys() byte[] pfxBytes = builder.Encode(); // Windows seems to be applying both the implicit match and the LocalKeyId match, - // so it detects two hits against the same key and fails. - ReadUnreadablePfx( - pfxBytes, - pw, - // NTE_BAD_DATA - -2146893819); + // so it detects two hits against the same key and fails for ephemeral import. + + if (s_importFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet)) + { + ReadUnreadablePfx( + pfxBytes, + pw, + // NTE_BAD_DATA + -2146893819); + } + else + { + // This is somewhat circular logic, but it's hard to bind cert2 with the wrong + // private key any other way. + + using (ImportedCollection coll = Cert.Import(pfxBytes, pw, s_perphemeralImportFlags)) + { + X509Certificate2 cert2WithKey = coll.Collection[0]; + + ReadMultiPfx( + pfxBytes, + pw, + cert2WithKey, + new[] { cert2WithKey, cert }, + x => + { + if (x.Equals(cert)) + { + CheckMultiBoundKeyConsistency(x); + } + else if (x.HasPrivateKey) + { + // On macOS cert2WithKey.HasPrivateKey is + // false because the SecIdentityRef won't + // bind the mismatched private key. + CheckMultiBoundKeyConsistencyFails(x); + } + }); + + AssertExtensions.SequenceEqual(cert2.RawData, cert2WithKey.RawData); + } + } } } @@ -724,14 +802,17 @@ public void CertAndKeyTwice(bool addLocalKeyId, bool crossIdentifiers) builder.SealWithMac(pw, s_digestAlgorithm, MacCount); byte[] pfxBytes = builder.Encode(); - if (addLocalKeyId) + // If we add the localKeyId values then everything works out, otherwise + // we end up doing SubjectPublicKeyInfo matching logic which says two certs + // mapped to one key. This fails for ephemeral import, and succeeds otherwise. + if (addLocalKeyId || !s_importFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet)) { ReadMultiPfx( pfxBytes, pw, cert, new[] { cert, cert }, - CheckKeyConsistency); + addLocalKeyId ? CheckKeyConsistency : CheckMultiBoundKeyConsistency); } else { @@ -774,11 +855,23 @@ public void CertAndKeyTwice_KeysUntagged() builder.SealWithMac(pw, s_digestAlgorithm, MacCount); byte[] pfxBytes = builder.Encode(); - ReadUnreadablePfx( - pfxBytes, - pw, - // NTE_BAD_DATA - -2146893819); + if (s_importFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet)) + { + ReadUnreadablePfx( + pfxBytes, + pw, + // NTE_BAD_DATA + -2146893819); + } + else + { + ReadMultiPfx( + pfxBytes, + pw, + cert, + new[] { cert, cert }, + CheckMultiBoundKeyConsistency); + } } } @@ -812,11 +905,88 @@ public void CertTwice_KeyOnce(bool addLocalKeyId) builder.SealWithMac(pw, s_digestAlgorithm, MacCount); byte[] pfxBytes = builder.Encode(); - ReadUnreadablePfx( + if (s_importFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet)) + { + ReadUnreadablePfx( + pfxBytes, + pw, + // NTE_BAD_DATA + -2146893819); + } + + // On Windows with perphemeral import the private key gets written then + // erased during import (cleaning resources associated with non-returned certs) + // so on Windows with a single-object load using the key will fail. + // + // The collection import is on a razors edge with the perphemeral import: + // once one of the certs gets disposed the other(s) can no longer open + // their private key. + + ReadMultiPfx( + pfxBytes, + pw, + cert, + new[] { cert, cert }, + s_perphemeralImportFlags, + CheckMultiBoundKeyConsistency); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CertTwice_KeyOnce_OtherCertBetter(bool addLocalKeyId) + { + string pw = nameof(CertTwice_KeyOnce); + + using (var rsaCert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (var ecCert = new X509Certificate2(TestData.ECDsaP256_DigitalSignature_Pfx_Windows, "Test", s_exportableImportFlags)) + using (RSA rsa = rsaCert.GetRSAPrivateKey()) + using (ECDsa ecdsa = ecCert.GetECDsaPrivateKey()) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + + Pkcs12SafeBag rsaKeyBag = keyContents.AddShroudedKey(rsa, pw, s_windowsPbe); + Pkcs12SafeBag ecKeyBag = keyContents.AddShroudedKey(ecdsa, pw, s_windowsPbe); + Pkcs12SafeBag certBag = certContents.AddCertificate(ecCert); + Pkcs12SafeBag certBag2 = certContents.AddCertificate(ecCert); + Pkcs12SafeBag rsaCertBag = certContents.AddCertificate(rsaCert); + + if (addLocalKeyId) + { + certBag.Attributes.Add(s_keyIdOne); + certBag2.Attributes.Add(s_keyIdOne); + ecKeyBag.Attributes.Add(s_keyIdOne); + rsaCertBag.Attributes.Add(s_keyIdTwo); + rsaKeyBag.Attributes.Add(s_keyIdTwo); + } + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + if (s_importFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet)) + { + ReadUnreadablePfx( + pfxBytes, + pw, + // NTE_BAD_DATA + -2146893819); + } + + // Because cert2 is what gets returned from the single cert ctor, the + // multiply referenced key doesn't cause a runtime problem on Windows. + + ReadMultiPfx( pfxBytes, pw, - // NTE_BAD_DATA - -2146893819); + rsaCert, + new[] { rsaCert, ecCert, ecCert }, + s_perphemeralImportFlags, + CheckKeyConsistency); } } @@ -928,18 +1098,46 @@ public void TwoCerts_TwoKeys_ManySafeContentsValues(bool invertCertOrder, bool i private static void CheckKeyConsistency(X509Certificate2 cert) { - using (RSA priv = cert.GetRSAPrivateKey()) - using (RSA pub = cert.GetRSAPublicKey()) + byte[] data = { 2, 7, 4 }; + + switch (cert.GetKeyAlgorithm()) { - byte[] data = { 2, 7, 4 }; - byte[] signature = priv.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + case "1.2.840.113549.1.1.1": + { + using (RSA priv = cert.GetRSAPrivateKey()) + using (RSA pub = cert.GetRSAPublicKey()) + { + byte[] signature = priv.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - Assert.True( - pub.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), - "Cert public key verifies signature from cert private key"); + Assert.True( + pub.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), + "Cert public key verifies signature from cert private key"); + } + + break; + } + default: + { + using (ECDsa priv = cert.GetECDsaPrivateKey()) + using (ECDsa pub = cert.GetECDsaPublicKey()) + { + byte[] signature = priv.SignData(data, HashAlgorithmName.SHA256); + + Assert.True( + pub.VerifyData(data, signature, HashAlgorithmName.SHA256), + "Cert public key verifies signature from cert private key"); + } + + break; + } } } + protected virtual void CheckMultiBoundKeyConsistency(X509Certificate2 cert) + { + CheckKeyConsistency(cert); + } + private static void CheckKeyConsistencyFails(X509Certificate2 cert) { using (RSA priv = cert.GetRSAPrivateKey()) @@ -954,6 +1152,11 @@ private static void CheckKeyConsistencyFails(X509Certificate2 cert) } } + protected virtual void CheckMultiBoundKeyConsistencyFails(X509Certificate2 cert) + { + CheckKeyConsistencyFails(cert); + } + private static void AddContents( Pkcs12SafeContents contents, Pkcs12Builder builder, diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_Collection.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_Collection.cs index 37ca1cc302329..c25d3391ddb9f 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_Collection.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_Collection.cs @@ -12,10 +12,13 @@ public sealed class PfxFormatTests_Collection : PfxFormatTests byte[] pfxBytes, string correctPassword, X509Certificate2 expectedCert, + X509KeyStorageFlags nonExportFlags, Action otherWork) { - ReadPfx(pfxBytes, correctPassword, expectedCert, null, otherWork, s_importFlags); - ReadPfx(pfxBytes, correctPassword, expectedCert, null, otherWork, s_exportableImportFlags); + X509KeyStorageFlags exportFlags = nonExportFlags | X509KeyStorageFlags.Exportable; + + ReadPfx(pfxBytes, correctPassword, expectedCert, null, otherWork, nonExportFlags); + ReadPfx(pfxBytes, correctPassword, expectedCert, null, otherWork, exportFlags); } protected override void ReadMultiPfx( @@ -23,6 +26,7 @@ public sealed class PfxFormatTests_Collection : PfxFormatTests string correctPassword, X509Certificate2 expectedSingleCert, X509Certificate2[] expectedOrder, + X509KeyStorageFlags nonExportFlags, Action perCertOtherWork) { ReadPfx( @@ -31,7 +35,7 @@ public sealed class PfxFormatTests_Collection : PfxFormatTests expectedSingleCert, expectedOrder, perCertOtherWork, - s_importFlags); + nonExportFlags); ReadPfx( pfxBytes, @@ -39,7 +43,7 @@ public sealed class PfxFormatTests_Collection : PfxFormatTests expectedSingleCert, expectedOrder, perCertOtherWork, - s_exportableImportFlags); + nonExportFlags | X509KeyStorageFlags.Exportable); } private void ReadPfx( @@ -89,13 +93,14 @@ protected override void ReadWrongPassword(byte[] pfxBytes, string wrongPassword) protected override void ReadUnreadablePfx( byte[] pfxBytes, string bestPassword, + X509KeyStorageFlags importFlags, int win32Error, int altWin32Error) { X509Certificate2Collection coll = new X509Certificate2Collection(); CryptographicException ex = Assert.ThrowsAny( - () => coll.Import(pfxBytes, bestPassword, s_importFlags)); + () => coll.Import(pfxBytes, bestPassword, importFlags)); if (OperatingSystem.IsWindows()) { diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_SingleCert.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_SingleCert.cs index 39f736d259df8..4e219e346a9b5 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_SingleCert.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_SingleCert.cs @@ -11,10 +11,13 @@ public sealed class PfxFormatTests_SingleCert : PfxFormatTests byte[] pfxBytes, string correctPassword, X509Certificate2 expectedCert, + X509KeyStorageFlags nonExportFlags, Action otherWork) { - ReadPfx(pfxBytes, correctPassword, expectedCert, otherWork, s_importFlags); - ReadPfx(pfxBytes, correctPassword, expectedCert, otherWork, s_exportableImportFlags); + X509KeyStorageFlags exportFlags = nonExportFlags | X509KeyStorageFlags.Exportable; + + ReadPfx(pfxBytes, correctPassword, expectedCert, otherWork, nonExportFlags); + ReadPfx(pfxBytes, correctPassword, expectedCert, otherWork, exportFlags); } protected override void ReadMultiPfx( @@ -22,10 +25,13 @@ public sealed class PfxFormatTests_SingleCert : PfxFormatTests string correctPassword, X509Certificate2 expectedSingleCert, X509Certificate2[] expectedOrder, + X509KeyStorageFlags nonExportFlags, Action perCertOtherWork) { - ReadPfx(pfxBytes, correctPassword, expectedSingleCert, perCertOtherWork, s_importFlags); - ReadPfx(pfxBytes, correctPassword, expectedSingleCert, perCertOtherWork, s_exportableImportFlags); + X509KeyStorageFlags exportFlags = nonExportFlags | X509KeyStorageFlags.Exportable; + + ReadPfx(pfxBytes, correctPassword, expectedSingleCert, perCertOtherWork, nonExportFlags); + ReadPfx(pfxBytes, correctPassword, expectedSingleCert, perCertOtherWork, exportFlags); } private void ReadPfx( @@ -62,11 +68,12 @@ protected override void ReadWrongPassword(byte[] pfxBytes, string wrongPassword) protected override void ReadUnreadablePfx( byte[] pfxBytes, string bestPassword, + X509KeyStorageFlags importFlags, int win32Error, int altWin32Error) { CryptographicException ex = Assert.ThrowsAny( - () => new X509Certificate2(pfxBytes, bestPassword, s_importFlags)); + () => new X509Certificate2(pfxBytes, bestPassword, importFlags)); if (OperatingSystem.IsWindows()) { @@ -80,5 +87,38 @@ protected override void ReadWrongPassword(byte[] pfxBytes, string wrongPassword) Assert.NotNull(ex.InnerException); } } + + private static void CheckBadKeyset(X509Certificate2 cert) + { + CryptographicException ex = Assert.ThrowsAny( + () => cert.GetRSAPrivateKey()); + + // NTE_BAD_KEYSET + Assert.Equal(-2146893802, ex.HResult); + } + + protected override void CheckMultiBoundKeyConsistency(X509Certificate2 cert) + { + if (PlatformDetection.IsWindows) + { + CheckBadKeyset(cert); + } + else + { + base.CheckMultiBoundKeyConsistency(cert); + } + } + + protected override void CheckMultiBoundKeyConsistencyFails(X509Certificate2 cert) + { + if (PlatformDetection.IsWindows) + { + CheckBadKeyset(cert); + } + else + { + base.CheckMultiBoundKeyConsistencyFails(cert); + } + } } }