From d4fe4a7e987584bfa4904dd1027a873029b2005d Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Tue, 25 Jun 2024 15:10:25 -0700 Subject: [PATCH] Create new X509CertificateLoader The new certificate loader only loads one data type per method, unlike the previous loader mechanism (new X509Certiicate2(bytes, ...)). It also allows for caller configuration to control cost-of-work limits and some common usability gotchas around Windows PFX loading. This change adds the new loader, and changes the X509Certificate2 ctors to use it; a followup will mark the ctors as Obsolete and update usage in the dotnet/runtime codebase. --- .../Crypt32/Interop.CryptQueryObject.cs | 15 + .../Crypt32/Interop.PFXImportCertStore.cs | 3 + .../MemoryMappedFileMemoryManager.cs | 108 ++ .../Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs | 258 ----- .../DSASecurityTransforms.macOS.cs | 2 +- .../System/Security/Cryptography/Helpers.cs | 30 + .../Cryptography/IncrementalHash.netfx.cs | 20 + .../Security/Cryptography/KdfWorkLimiter.cs | 86 -- .../Cryptography/KeyFormatHelper.Encrypted.cs | 3 +- .../src/System/Security/Cryptography/Oids.cs | 2 + .../Cryptography/PasswordBasedEncryption.cs | 66 +- .../System/Security/Cryptography/Pkcs12Kdf.cs | 1 - .../Pkcs12LoadLimitExceededException.cs | 24 + .../X509Certificates/Pkcs12LoaderLimits.cs | 392 ++++++++ .../X509CertificateLoader.Pkcs12.cs | 944 ++++++++++++++++++ .../X509Certificates/X509CertificateLoader.cs | 741 ++++++++++++++ .../X509IterationCountExceededException.cs | 9 - .../System/Security/Cryptography/ByteUtils.cs | 11 + .../X509Certificates/TempFileHolder.cs | 9 +- .../X509Certificates/TestData.cs | 254 +++++ ...9CertificateLoaderPkcs12CollectionTests.cs | 763 ++++++++++++++ ...cateLoaderPkcs12Tests.WindowsAttributes.cs | 218 ++++ .../X509CertificateLoaderPkcs12Tests.cs | 739 ++++++++++++++ .../X509CertificateLoaderTests.cs | 356 +++++++ .../Microsoft.Bcl.Cryptography.Forwards.cs | 7 + .../ref/Microsoft.Bcl.Cryptography.cs | 48 + .../ref/Microsoft.Bcl.Cryptography.csproj | 7 +- .../src/Microsoft.Bcl.Cryptography.csproj | 237 ++++- .../Win32/SafeHandles/SafePasswordHandle.cs | 96 ++ .../src/Resources/Strings.resx | 45 + .../System/Security/Cryptography/Helpers.cs | 53 + .../Security/Cryptography/NetStandardShims.cs | 140 +++ .../PbeEncryptionAlgorithm.netstandard.cs | 16 + .../Cryptography/PbeParameters.netstandard.cs | 35 + .../X509CertificateLoader.ProcessedPkcs12.cs | 132 +++ .../X509CertificateLoader.netfx.cs | 105 ++ .../X509CertificateLoader.netstandard.cs | 53 + .../Microsoft.Bcl.Cryptography.Tests.csproj | 22 + .../tests/X509Certificates/TestFiles.cs | 43 + .../System.Security.Cryptography.Pkcs.csproj | 5 +- .../ref/System.Security.Cryptography.cs | 39 + .../src/Resources/Strings.resx | 6 + .../src/System.Security.Cryptography.csproj | 34 +- .../X509Certificates/AndroidCertificatePal.cs | 61 +- .../X509Certificates/AndroidPkcs12Reader.cs | 93 -- .../AppleCertificatePal.ImportExport.iOS.cs | 20 +- .../AppleCertificatePal.ImportExport.macOS.cs | 26 +- .../AppleCertificatePal.Keys.iOS.cs | 6 +- .../AppleCertificatePal.Pkcs12.iOS.cs | 21 +- .../AppleCertificatePal.Pkcs12.macOS.cs | 47 - .../ApplePkcs12CertLoader.iOS.cs | 41 - .../X509Certificates/ApplePkcs12Reader.iOS.cs | 68 -- .../ApplePkcs12Reader.macOS.cs | 111 -- .../CertificatePal.Windows.Import.cs | 157 +-- .../CertificatePal.Windows.cs | 2 +- .../LocalAppContextSwitches.cs | 2 +- .../X509Certificates/OpenSslPkcs12Reader.cs | 107 -- .../OpenSslPkcsFormatReader.cs | 114 --- .../OpenSslX509CertificateReader.cs | 63 +- .../X509Certificates/OpenSslX509Encoder.cs | 19 +- .../X509Certificates/StorePal.Android.cs | 50 +- .../X509Certificates/StorePal.OpenSsl.cs | 59 +- .../StorePal.Windows.Import.cs | 81 +- .../Cryptography/X509Certificates/StorePal.cs | 32 + .../X509Certificates/StorePal.iOS.cs | 20 +- .../StorePal.macOS.LoaderPal.cs | 76 -- .../X509Certificates/StorePal.macOS.cs | 50 +- .../X509Certificates/UnixPkcs12Reader.cs | 857 ---------------- .../X509Certificate.LegacyLimits.cs | 50 + .../X509Certificates/X509Certificate.cs | 123 +-- .../X509CertificateLoader.Android.cs | 111 ++ .../X509CertificateLoader.NotSupported.cs | 34 + .../X509CertificateLoader.OpenSsl.cs | 98 ++ .../X509CertificateLoader.Unix.cs | 620 ++++++++++++ .../X509CertificateLoader.Windows.cs | 321 ++++++ .../X509CertificateLoader.iOS.cs | 122 +++ .../X509CertificateLoader.macOS.cs | 232 +++++ .../X509CertificateLoader.netcore.cs | 146 +++ .../X509Certificates/X509Pal.Android.cs | 2 +- .../System.Security.Cryptography.Tests.csproj | 16 +- .../tests/X509Certificates/CtorTests.cs | 8 +- .../tests/X509Certificates/PfxFormatTests.cs | 128 ++- .../PfxFormatTests_Collection.cs | 24 +- .../PfxFormatTests_SingleCert.cs | 35 +- ...ionCountTests.CustomAppContextDataLimit.cs | 14 +- .../tests/X509Certificates/PfxTests.cs | 31 - 86 files changed, 7995 insertions(+), 2480 deletions(-) create mode 100644 src/libraries/Common/src/System/IO/MemoryMappedFiles/MemoryMappedFileMemoryManager.cs delete mode 100644 src/libraries/Common/src/System/Security/Cryptography/KdfWorkLimiter.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/X509Certificates/Pkcs12LoadLimitExceededException.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/X509Certificates/Pkcs12LoaderLimits.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Pkcs12.cs create mode 100644 src/libraries/Common/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.cs delete mode 100644 src/libraries/Common/src/System/Security/Cryptography/X509IterationCountExceededException.cs rename src/libraries/{System.Security.Cryptography/tests => Common/tests/System/Security/Cryptography}/X509Certificates/TempFileHolder.cs (87%) rename src/libraries/{System.Security.Cryptography/tests => Common/tests/System/Security/Cryptography}/X509Certificates/TestData.cs (94%) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12CollectionTests.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12Tests.WindowsAttributes.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12Tests.cs create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderTests.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/Helpers.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PbeEncryptionAlgorithm.netstandard.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PbeParameters.netstandard.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.ProcessedPkcs12.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netfx.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netstandard.cs create mode 100644 src/libraries/Microsoft.Bcl.Cryptography/tests/X509Certificates/TestFiles.cs delete mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidPkcs12Reader.cs delete mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12CertLoader.iOS.cs delete mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12Reader.iOS.cs delete mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12Reader.macOS.cs delete mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslPkcs12Reader.cs delete mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/UnixPkcs12Reader.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.LegacyLimits.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Android.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.NotSupported.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.OpenSsl.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Unix.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Windows.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.iOS.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.macOS.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netcore.cs diff --git a/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CryptQueryObject.cs b/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CryptQueryObject.cs index f7cf15985b686..c170c89d8e054 100644 --- a/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CryptQueryObject.cs +++ b/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.CryptQueryObject.cs @@ -56,5 +56,20 @@ internal static unsafe partial bool CryptQueryObject( IntPtr phMsg, IntPtr ppvContext ); + + [LibraryImport(Libraries.Crypt32, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static unsafe partial bool CryptQueryObject( + CertQueryObjectType dwObjectType, + void* pvObject, + ExpectedContentTypeFlags dwExpectedContentTypeFlags, + ExpectedFormatTypeFlags dwExpectedFormatTypeFlags, + int dwFlags, // reserved - always pass 0 + IntPtr pdwMsgAndCertEncodingType, + out ContentType pdwContentType, + IntPtr pdwFormatType, + IntPtr phCertStore, + IntPtr phMsg, + out SafeCertContextHandle ppvContext); } } diff --git a/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.PFXImportCertStore.cs b/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.PFXImportCertStore.cs index 7df9fb7e1474f..dcc641c64f9ed 100644 --- a/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.PFXImportCertStore.cs +++ b/src/libraries/Common/src/Interop/Windows/Crypt32/Interop.PFXImportCertStore.cs @@ -10,5 +10,8 @@ internal static partial class Crypt32 { [LibraryImport(Libraries.Crypt32, SetLastError = true)] internal static partial SafeCertStoreHandle PFXImportCertStore(ref DATA_BLOB pPFX, SafePasswordHandle password, PfxCertStoreFlags dwFlags); + + [LibraryImport(Libraries.Crypt32, SetLastError = true)] + internal static unsafe partial SafeCertStoreHandle PFXImportCertStore(ref DATA_BLOB pPFX, char* password, PfxCertStoreFlags dwFlags); } } diff --git a/src/libraries/Common/src/System/IO/MemoryMappedFiles/MemoryMappedFileMemoryManager.cs b/src/libraries/Common/src/System/IO/MemoryMappedFiles/MemoryMappedFileMemoryManager.cs new file mode 100644 index 0000000000000..10fb24e05443d --- /dev/null +++ b/src/libraries/Common/src/System/IO/MemoryMappedFiles/MemoryMappedFileMemoryManager.cs @@ -0,0 +1,108 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; + +namespace System.IO.MemoryMappedFiles +{ + internal sealed unsafe class MemoryMappedFileMemoryManager : MemoryManager + { + private byte* _pointer; + private int _length; + private MemoryMappedFile _mappedFile; + private MemoryMappedViewAccessor _accessor; + + public MemoryMappedFileMemoryManager( + byte* pointer, + int length, + MemoryMappedFile mappedFile, + MemoryMappedViewAccessor accessor) + { + _pointer = pointer; + _length = length; + _mappedFile = mappedFile; + _accessor = accessor; + } + +#if DEBUG +#pragma warning disable CA2015 + ~MemoryMappedFileMemoryManager() +#pragma warning restore CA2015 + { + Environment.FailFast("MemoryMappedFileMemoryManager was finalized."); + } +#endif + + internal static MemoryMappedFileMemoryManager CreateFromFileClamped( + FileStream fileStream, + MemoryMappedFileAccess access = MemoryMappedFileAccess.Read, + HandleInheritability inheritability = HandleInheritability.None, + bool leaveOpen = false) + { + int length = (int)Math.Min(int.MaxValue, fileStream.Length); + MemoryMappedFile mapped = MemoryMappedFile.CreateFromFile(fileStream, null, 0, access, inheritability, leaveOpen); + MemoryMappedViewAccessor? accessor = null; + byte* pointer = null; + + try + { + accessor = mapped.CreateViewAccessor(0, length, access); + accessor.SafeMemoryMappedViewHandle.AcquirePointer(ref pointer); + + return new MemoryMappedFileMemoryManager(pointer, length, mapped, accessor); + } + catch (Exception) + { + if (pointer != null) + { + accessor!.SafeMemoryMappedViewHandle.ReleasePointer(); + } + + accessor?.Dispose(); + mapped.Dispose(); + throw; + } + } + + protected override void Dispose(bool disposing) + { + _pointer = null; + _length = -1; + _accessor?.SafeMemoryMappedViewHandle.ReleasePointer(); + _accessor?.Dispose(); + _mappedFile?.Dispose(); + _accessor = null!; + _mappedFile = null!; + } + + public override Span GetSpan() + { + ThrowIfDisposed(); + return new Span(_pointer, _length); + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + ThrowIfDisposed(); + return default; + } + + public override void Unpin() + { + ThrowIfDisposed(); + // nop + } + + private void ThrowIfDisposed() + { +#if NET + ObjectDisposedException.ThrowIf(_length < 0, this); +#else + if (_length < 0) + { + throw new ObjectDisposedException(GetType().FullName); + } +#endif + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs b/src/libraries/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs index 38e60abcebc64..67ee5593e6586 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs @@ -2,11 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.Formats.Asn1; -using System.Security.Cryptography.Asn1.Pkcs7; using System.Security.Cryptography.Pkcs; -using System.Security.Cryptography.X509Certificates; -using Internal.Cryptography; #if BUILDING_PKCS using Helpers = Internal.Cryptography.PkcsHelpers; @@ -16,10 +12,6 @@ namespace System.Security.Cryptography.Asn1.Pkcs12 { internal partial struct PfxAsn { - private const int MaxIterationWork = 300_000; - private static ReadOnlySpan EmptyPassword => ""; // don't use ReadOnlySpan.Empty because it will get confused with default. - private static ReadOnlySpan NullPassword => default; - internal bool VerifyMac( ReadOnlySpan macPassword, ReadOnlySpan authSafeContents) @@ -96,255 +88,5 @@ internal bool VerifyMac( MacData.Value.Mac.Digest.Span); } } - - internal ulong CountTotalIterations() - { - checked - { - ulong count = 0; - - // RFC 7292 section 4.1: - // the contentType field of authSafe shall be of type data - // or signedData. The content field of the authSafe shall, either - // directly (data case) or indirectly (signedData case), contain a BER- - // encoded value of type AuthenticatedSafe. - // We don't support authSafe that is signedData, so enforce that it's just data. - if (AuthSafe.ContentType != Oids.Pkcs7Data) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - ReadOnlyMemory authSafeContents = Helpers.DecodeOctetStringAsMemory(AuthSafe.Content); - AsnValueReader outerAuthSafe = new AsnValueReader(authSafeContents.Span, AsnEncodingRules.BER); // RFC 7292 PDU says BER - AsnValueReader authSafeReader = outerAuthSafe.ReadSequence(); - outerAuthSafe.ThrowIfNotEmpty(); - - bool hasSeenEncryptedInfo = false; - - while (authSafeReader.HasData) - { - ContentInfoAsn.Decode(ref authSafeReader, authSafeContents, out ContentInfoAsn contentInfo); - - ReadOnlyMemory contentData; - ArraySegment? rentedData = null; - - try - { - if (contentInfo.ContentType != Oids.Pkcs7Data) - { - if (contentInfo.ContentType == Oids.Pkcs7Encrypted) - { - if (hasSeenEncryptedInfo) - { - // We will process at most one encryptedData ContentInfo. This is the most typical scenario where - // certificates are stored in an encryptedData ContentInfo, and keys are shrouded in a data ContentInfo. - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - ArraySegment content = DecryptContentInfo(contentInfo, out uint iterations); - contentData = content; - rentedData = content; - hasSeenEncryptedInfo = true; - count += iterations; - } - else - { - // Not a common scenario. It's not data or encryptedData, so they need to go through the - // regular PKCS12 loader. - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - } - else - { - contentData = Helpers.DecodeOctetStringAsMemory(contentInfo.Content); - } - - AsnValueReader outerSafeBag = new AsnValueReader(contentData.Span, AsnEncodingRules.BER); - AsnValueReader safeBagReader = outerSafeBag.ReadSequence(); - outerSafeBag.ThrowIfNotEmpty(); - - while (safeBagReader.HasData) - { - SafeBagAsn.Decode(ref safeBagReader, contentData, out SafeBagAsn bag); - - // We only need to count iterations on PKCS8ShroudedKeyBag. - // * KeyBag is PKCS#8 PrivateKeyInfo and doesn't do iterations. - // * CertBag, either for x509Certificate or sdsiCertificate don't do iterations. - // * CRLBag doesn't do iterations. - // * SecretBag doesn't do iteations. - // * Nested SafeContents _can_ do iterations, but Windows ignores it. So we will ignore it too. - if (bag.BagId == Oids.Pkcs12ShroudedKeyBag) - { - AsnValueReader pkcs8ShroudedKeyReader = new AsnValueReader(bag.BagValue.Span, AsnEncodingRules.BER); - EncryptedPrivateKeyInfoAsn.Decode( - ref pkcs8ShroudedKeyReader, - bag.BagValue, - out EncryptedPrivateKeyInfoAsn epki); - - count += IterationsFromParameters(epki.EncryptionAlgorithm); - } - } - } - finally - { - if (rentedData.HasValue) - { - CryptoPool.Return(rentedData.Value); - } - } - } - - if (MacData.HasValue) - { - if (MacData.Value.IterationCount < 0) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - count += (uint)MacData.Value.IterationCount; - } - - return count; - } - } - - private static ArraySegment DecryptContentInfo(ContentInfoAsn contentInfo, out uint iterations) - { - EncryptedDataAsn encryptedData = EncryptedDataAsn.Decode(contentInfo.Content, AsnEncodingRules.BER); - - if (encryptedData.Version != 0 && encryptedData.Version != 2) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - // The encrypted contentInfo can only wrap a PKCS7 data. - if (encryptedData.EncryptedContentInfo.ContentType != Oids.Pkcs7Data) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - if (!encryptedData.EncryptedContentInfo.EncryptedContent.HasValue) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - iterations = IterationsFromParameters(encryptedData.EncryptedContentInfo.ContentEncryptionAlgorithm); - - // This encryptData is encrypted with more rounds than we are willing to process. Bail out of the whole thing. - if (iterations > MaxIterationWork) - { - throw new X509IterationCountExceededException(); - } - - int encryptedValueLength = encryptedData.EncryptedContentInfo.EncryptedContent.Value.Length; - byte[] destination = CryptoPool.Rent(encryptedValueLength); - int written = 0; - - try - { - try - { - written = PasswordBasedEncryption.Decrypt( - in encryptedData.EncryptedContentInfo.ContentEncryptionAlgorithm, - EmptyPassword, - default, - encryptedData.EncryptedContentInfo.EncryptedContent.Value.Span, - destination); - - // When padding happens to be as expected (false-positive), we can detect gibberish and prevent unexpected failures later - // This extra check makes it so it's very unlikely we'll end up with false positive. - AsnValueReader outerSafeBag = new AsnValueReader(destination.AsSpan(0, written), AsnEncodingRules.BER); - AsnValueReader safeBagReader = outerSafeBag.ReadSequence(); - outerSafeBag.ThrowIfNotEmpty(); - } - catch - { - // If empty password didn't work, try null password. - written = PasswordBasedEncryption.Decrypt( - in encryptedData.EncryptedContentInfo.ContentEncryptionAlgorithm, - NullPassword, - default, - encryptedData.EncryptedContentInfo.EncryptedContent.Value.Span, - destination); - - AsnValueReader outerSafeBag = new AsnValueReader(destination.AsSpan(0, written), AsnEncodingRules.BER); - AsnValueReader safeBagReader = outerSafeBag.ReadSequence(); - outerSafeBag.ThrowIfNotEmpty(); - } - } - finally - { - if (written == 0) - { - // This means the decryption operation failed and destination could contain - // partial data. Clear it to be hygienic. - CryptographicOperations.ZeroMemory(destination); - } - } - - return new ArraySegment(destination, 0, written); - } - - private static uint IterationsFromParameters(in AlgorithmIdentifierAsn algorithmIdentifier) - { - switch (algorithmIdentifier.Algorithm) - { - case Oids.PasswordBasedEncryptionScheme2: - if (!algorithmIdentifier.Parameters.HasValue) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - PBES2Params pbes2Params = PBES2Params.Decode(algorithmIdentifier.Parameters.Value, AsnEncodingRules.BER); - - // PBES2 only defines PKBDF2 for now. See RFC 8018 A.4 - if (pbes2Params.KeyDerivationFunc.Algorithm != Oids.Pbkdf2) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - if (!pbes2Params.KeyDerivationFunc.Parameters.HasValue) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - Pbkdf2Params pbkdf2Params = Pbkdf2Params.Decode(pbes2Params.KeyDerivationFunc.Parameters.Value, AsnEncodingRules.BER); - - if (pbkdf2Params.IterationCount < 0) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - return (uint)pbkdf2Params.IterationCount; - - // PBES1 - case Oids.PbeWithMD5AndDESCBC: - case Oids.PbeWithMD5AndRC2CBC: - case Oids.PbeWithSha1AndDESCBC: - case Oids.PbeWithSha1AndRC2CBC: - case Oids.Pkcs12PbeWithShaAnd3Key3Des: - case Oids.Pkcs12PbeWithShaAnd2Key3Des: - case Oids.Pkcs12PbeWithShaAnd128BitRC2: - case Oids.Pkcs12PbeWithShaAnd40BitRC2: - if (!algorithmIdentifier.Parameters.HasValue) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - PBEParameter pbeParameters = PBEParameter.Decode( - algorithmIdentifier.Parameters.Value, - AsnEncodingRules.BER); - - if (pbeParameters.IterationCount < 0) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - return (uint)pbeParameters.IterationCount; - - default: - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/DSASecurityTransforms.macOS.cs b/src/libraries/Common/src/System/Security/Cryptography/DSASecurityTransforms.macOS.cs index 47396768b09d9..05fd3676f7616 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/DSASecurityTransforms.macOS.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/DSASecurityTransforms.macOS.cs @@ -134,7 +134,7 @@ public override void ImportEncryptedPkcs8PrivateKey( base.ImportEncryptedPkcs8PrivateKey(password, source, out bytesRead); } - private static SafeSecKeyRefHandle ImportKey(DSAParameters parameters) + internal static SafeSecKeyRefHandle ImportKey(DSAParameters parameters) { AsnWriter keyWriter; bool hasPrivateKey; diff --git a/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs b/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs index 462e4d26a8f64..341931377ddd8 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Helpers.cs @@ -3,6 +3,8 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Security.Cryptography; @@ -10,6 +12,10 @@ namespace Internal.Cryptography { internal static partial class Helpers { +#if NETFRAMEWORK || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) + private static readonly RandomNumberGenerator s_rng = RandomNumberGenerator.Create(); +#endif + [UnsupportedOSPlatformGuard("browser")] internal static bool HasSymmetricEncryption { get; } = #if NET @@ -53,6 +59,30 @@ internal static partial class Helpers }; } + internal static bool ContainsNull(this ReadOnlySpan span) + { + return Unsafe.IsNullRef(ref MemoryMarshal.GetReference(span)); + } + +#if NETFRAMEWORK || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) + internal static void RngFill(byte[] destination) + { + s_rng.GetBytes(destination); + } +#endif + + internal static void RngFill(Span destination) + { +#if NET || NETSTANDARD2_1_OR_GREATER + RandomNumberGenerator.Fill(destination); +#else + byte[] temp = CryptoPool.Rent(destination.Length); + s_rng.GetBytes(temp, 0, destination.Length); + temp.AsSpan(0, destination.Length).CopyTo(destination); + CryptoPool.Return(temp, destination.Length); +#endif + } + internal static bool TryCopyToDestination(this ReadOnlySpan source, Span destination, out int bytesWritten) { if (source.TryCopyTo(destination)) diff --git a/src/libraries/Common/src/System/Security/Cryptography/IncrementalHash.netfx.cs b/src/libraries/Common/src/System/Security/Cryptography/IncrementalHash.netfx.cs index b60bac3f6ede6..7f78979af0735 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/IncrementalHash.netfx.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/IncrementalHash.netfx.cs @@ -123,6 +123,26 @@ public byte[] GetHashAndReset() return hashValue; } + internal bool TryGetHashAndReset( + Span destination, + out int bytesWritten) + { + if (destination.Length < _hash.HashSize / 8) + { + bytesWritten = 0; + return false; + } + + _hash.TransformFinalBlock(Array.Empty(), 0, 0); + byte[] actual = _hash.Hash; + _hash.Initialize(); + + Debug.Assert(actual.Length * 8 == _hash.HashSize); + actual.AsSpan().CopyTo(destination); + bytesWritten = actual.Length; + return true; + } + /// /// Release all resources used by the current instance of the /// class. diff --git a/src/libraries/Common/src/System/Security/Cryptography/KdfWorkLimiter.cs b/src/libraries/Common/src/System/Security/Cryptography/KdfWorkLimiter.cs deleted file mode 100644 index 7500212fe27d9..0000000000000 --- a/src/libraries/Common/src/System/Security/Cryptography/KdfWorkLimiter.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; - -namespace System.Security.Cryptography -{ - // Places KDF work limits on the current thread. - internal static class KdfWorkLimiter - { - [ThreadStatic] - private static State? t_state; - - // Entry point: sets the iteration limit to a new value. - internal static void SetIterationLimit(ulong workLimit) - { - Debug.Assert(t_state == null, "This method is not intended to be called recursively."); - State state = new State(); - state.RemainingAllowedWork = workLimit; - t_state = state; - } - - internal static bool WasWorkLimitExceeded() - { - Debug.Assert(t_state != null, "This method should only be called within a protected block."); - return t_state.WorkLimitWasExceeded; - } - - // Removes any iteration limit on the current thread. - internal static void ResetIterationLimit() - { - t_state = null; - } - - // Records that we're about to perform some amount of work. - // Overflows if the work count is exceeded. - internal static void RecordIterations(int workCount) - { - RecordIterations((long)workCount); - } - - // Records that we're about to perform some amount of work. - // Overflows if the work count is exceeded. - internal static void RecordIterations(long workCount) - { - State? state = t_state; - if (state == null) - { - return; - } - - bool success = false; - - if (workCount < 0) - { - throw new CryptographicException(); - } - - try - { - if (!state.WorkLimitWasExceeded) - { - state.RemainingAllowedWork = checked(state.RemainingAllowedWork - (ulong)workCount); - success = true; - } - } - finally - { - // If for any reason we failed, mark the thread as "no further work allowed" and - // normalize to CryptographicException. - if (!success) - { - state.RemainingAllowedWork = 0; - state.WorkLimitWasExceeded = true; - throw new CryptographicException(); - } - } - } - - private sealed class State - { - internal ulong RemainingAllowedWork; - internal bool WorkLimitWasExceeded; - } - } -} diff --git a/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.Encrypted.cs b/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.Encrypted.cs index 8e96369bca917..24b238e502b1f 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.Encrypted.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/KeyFormatHelper.Encrypted.cs @@ -6,6 +6,7 @@ using System.Formats.Asn1; using System.Runtime.InteropServices; using System.Security.Cryptography.Asn1; +using Internal.Cryptography; namespace System.Security.Cryptography { @@ -202,7 +203,7 @@ private static AsnWriter WriteEncryptedPkcs8( try { - RandomNumberGenerator.Fill(salt); + Helpers.RngFill(salt); int written = PasswordBasedEncryption.Encrypt( password, diff --git a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs index b7364108222ad..033c9c9db0d9f 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Oids.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Oids.cs @@ -41,6 +41,7 @@ internal static partial class Oids internal const string SigningCertificate = "1.2.840.113549.1.9.16.2.12"; internal const string SigningCertificateV2 = "1.2.840.113549.1.9.16.2.47"; internal const string DocumentName = "1.3.6.1.4.1.311.88.2.1"; + internal const string Pkcs9FriendlyName = "1.2.840.113549.1.9.20"; internal const string LocalKeyId = "1.2.840.113549.1.9.21"; internal const string EnrollCertTypeExtension = "1.3.6.1.4.1.311.20.2"; internal const string UserPrincipalName = "1.3.6.1.4.1.311.20.2.3"; @@ -50,6 +51,7 @@ internal static partial class Oids internal const string OcspEndpoint = "1.3.6.1.5.5.7.48.1"; internal const string CertificateAuthorityIssuers = "1.3.6.1.5.5.7.48.2"; internal const string Pkcs9ExtensionRequest = "1.2.840.113549.1.9.14"; + internal const string MsPkcs12KeyProviderName = "1.3.6.1.4.1.311.17.1"; // Key wrap algorithms internal const string CmsRc2Wrap = "1.2.840.113549.1.9.16.3.7"; diff --git a/src/libraries/Common/src/System/Security/Cryptography/PasswordBasedEncryption.cs b/src/libraries/Common/src/System/Security/Cryptography/PasswordBasedEncryption.cs index 4bf056ffd5f4f..0e931591200a7 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/PasswordBasedEncryption.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/PasswordBasedEncryption.cs @@ -393,12 +393,7 @@ internal static unsafe int Encrypt( Debug.Assert(pwdTmpBytes!.Length == 0); } - KdfWorkLimiter.RecordIterations(iterationCount); - using (var pbkdf2 = new Rfc2898DeriveBytes(pwdTmpBytes, salt.ToArray(), iterationCount, prf)) - { - derivedKey = pbkdf2.GetBytes(keySizeBytes); - } - + derivedKey = DeriveKey(pwdTmpBytes, salt, iterationCount, prf, keySizeBytes); iv.CopyTo(ivDest); } @@ -786,7 +781,7 @@ private static unsafe Rfc2898DeriveBytes OpenPbkdf2( { requestedKeyLength = pbkdf2Params.KeyLength; - return new Rfc2898DeriveBytes( + return OpenPbkdf2( tmpPassword, tmpSalt, iterationCount, @@ -1113,5 +1108,62 @@ private static RC2 CreateRC2() return RC2.Create(); } + + private static byte[] DeriveKey( + byte[] password, + ReadOnlySpan salt, + int iterationCount, + HashAlgorithmName prf, + int outputLength) + { +#if NET + return Rfc2898DeriveBytes.Pbkdf2(password, salt, iterationCount, prf, outputLength); +#else + using (Rfc2898DeriveBytes pbkdf2 = OpenPbkdf2(password, salt.ToArray(), iterationCount, prf)) + { + return pbkdf2.GetBytes(outputLength); + } +#endif + } + + private static Rfc2898DeriveBytes OpenPbkdf2( + byte[] password, + byte[] salt, + int iterationCount, + HashAlgorithmName prf) + { +#if NET || NETSTANDARD2_1_OR_GREATER || NET472_OR_GREATER +#pragma warning disable CA5379 + return new Rfc2898DeriveBytes( + password, + salt, + iterationCount, + prf); +#pragma warning restore CA5379 +#else + if (prf == HashAlgorithmName.SHA1) + { +#pragma warning disable CA5379 + return new Rfc2898DeriveBytes(password, salt, iterationCount); +#pragma warning restore CA5379 + } + + try + { + return (Rfc2898DeriveBytes)Activator.CreateInstance( + typeof(Rfc2898DeriveBytes), + password, + salt, + iterationCount, + prf); + } + catch (MissingMethodException e) + { + throw new CryptographicException( + SR.Format(SR.Cryptography_UnknownHashAlgorithm, prf.Name), + e); + } +#endif + } } } diff --git a/src/libraries/Common/src/System/Security/Cryptography/Pkcs12Kdf.cs b/src/libraries/Common/src/System/Security/Cryptography/Pkcs12Kdf.cs index 1fa1d0ee03391..8264a6586589e 100644 --- a/src/libraries/Common/src/System/Security/Cryptography/Pkcs12Kdf.cs +++ b/src/libraries/Common/src/System/Security/Cryptography/Pkcs12Kdf.cs @@ -155,7 +155,6 @@ private static void Derive( I = IRented.AsSpan(0, ILen); } - KdfWorkLimiter.RecordIterations(iterationCount); IncrementalHash hash = IncrementalHash.CreateHash(hashAlgorithm); try diff --git a/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/Pkcs12LoadLimitExceededException.cs b/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/Pkcs12LoadLimitExceededException.cs new file mode 100644 index 0000000000000..2edafaeb7db12 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/Pkcs12LoadLimitExceededException.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography.X509Certificates +{ + /// + /// The exception that is thrown when importing a PKCS#12/PFX has failed + /// due to violating a specified limit. + /// + public sealed class Pkcs12LoadLimitExceededException : CryptographicException + { + /// + /// Initializes a new instance of the + /// class. + /// + /// + /// The name of the property representing the limit that was exceeded. + /// + public Pkcs12LoadLimitExceededException(string propertyName) + : base(SR.Format(SR.Cryptography_X509_PKCS12_LimitExceeded, propertyName)) + { + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/Pkcs12LoaderLimits.cs b/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/Pkcs12LoaderLimits.cs new file mode 100644 index 0000000000000..3200771cee0ad --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/Pkcs12LoaderLimits.cs @@ -0,0 +1,392 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +namespace System.Security.Cryptography.X509Certificates +{ + /// + /// Represents a set of constraints to apply when loading PKCS#12/PFX contents. + /// + public sealed class Pkcs12LoaderLimits + { + private bool _isReadOnly; + private int? _macIterationLimit = 300_000; + private int? _individualKdfIterationLimit = 300_000; + private int? _totalKdfIterationLimit = 1_000_000; + private int? _maxKeys = 200; + private int? _maxCertificates = 200; + private bool _preserveStorageProvider; + private bool _preserveKeyName; + private bool _preserveCertificateAlias; + private bool _preserveUnknownAttributes; + private bool _ignorePrivateKeys; + private bool _ignoreEncryptedAuthSafes; + + /// + /// Gets a shared reference to the default loader limits. + /// + /// + /// The singleton instance returned from this property is equivalent to an + /// instance produced via the default constructor, except the properties + /// prohibit reassignment. As with the default constructor, the individual + /// property values may change over time. + /// + /// A shared reference to the default loader limits. + /// + public static Pkcs12LoaderLimits Defaults { get; } = MakeReadOnly(new Pkcs12LoaderLimits()); + + /// + /// Gets a shared reference to loader limits that indicate no + /// filtering or restrictions of the contents should be applied + /// before sending them to the underlying system loader. + /// + /// + /// A shared reference to loader limits that indicate no + /// filtering or restrictions of the contents should be applied + /// before sending them to the underlying system loader. + /// + /// + /// + /// The system loader may have its own limits where only part + /// of the contents are respected, or where the load is rejected. + /// Using this set of limits only affects the .NET layer of filtering. + /// + /// + /// The class checks for reference + /// equality to this property to determine if filtering should be bypassed. + /// Making a new Pkcs12LoaderLimits value that has all of the same property + /// values may give different results for certain inputs. + /// + /// + public static Pkcs12LoaderLimits DangerousNoLimits { get; } = + MakeReadOnly( + new Pkcs12LoaderLimits + { + MacIterationLimit = null, + IndividualKdfIterationLimit = null, + TotalKdfIterationLimit = null, + MaxKeys = null, + MaxCertificates = null, + PreserveStorageProvider = true, + PreserveKeyName = true, + PreserveCertificateAlias = true, + PreserveUnknownAttributes = true, + }); + + /// + /// Initializes a new instance of the class + /// with default values. + /// + /// + /// The default values for each property on a default instance of this class + /// are chosen as a compromise between maximizing compatibility and minimizing + /// "nuisance" work. The defaults for any given property may vary over time. + /// + public Pkcs12LoaderLimits() + { + } + + /// + /// Initializes a new instance of the class + /// by copying the values from another instance. + /// + /// The instance to copy the values from. + /// + /// is . + /// + public Pkcs12LoaderLimits(Pkcs12LoaderLimits copyFrom) + { +#if NET + ArgumentNullException.ThrowIfNull(copyFrom); +#else + if (copyFrom is null) + throw new ArgumentNullException(nameof(copyFrom)); +#endif + + // Do not copy _isReadOnly. + + _macIterationLimit = copyFrom._macIterationLimit; + _individualKdfIterationLimit = copyFrom._individualKdfIterationLimit; + _totalKdfIterationLimit = copyFrom._totalKdfIterationLimit; + _maxKeys = copyFrom._maxKeys; + _maxCertificates = copyFrom._maxCertificates; + _preserveStorageProvider = copyFrom._preserveStorageProvider; + _preserveKeyName = copyFrom._preserveKeyName; + _preserveCertificateAlias = copyFrom._preserveCertificateAlias; + _preserveUnknownAttributes = copyFrom._preserveUnknownAttributes; + _ignorePrivateKeys = copyFrom._ignorePrivateKeys; + _ignoreEncryptedAuthSafes = copyFrom._ignoreEncryptedAuthSafes; + } + + /// + /// Gets a value indicating whether the instance is read-only. + /// + /// + /// if the instance is read-only; otherwise, . + /// + public bool IsReadOnly => _isReadOnly; + + /// + /// Makes the instance read-only. + /// + public void MakeReadOnly() + { + _isReadOnly = true; + } + + private static Pkcs12LoaderLimits MakeReadOnly(Pkcs12LoaderLimits limits) + { + limits.MakeReadOnly(); + return limits; + } + + private void CheckReadOnly() + { + if (_isReadOnly) + { + throw new InvalidOperationException(SR.Cryptography_X509_PKCS12_LimitsReadOnly); + } + } + + /// + /// Gets or sets the iteration limit for the MAC calculation. + /// + /// The iteration limit for the MAC calculation, or for no limit. + public int? MacIterationLimit + { + get => _macIterationLimit; + set + { + CheckNonNegative(value); + CheckReadOnly(); + _macIterationLimit = value; + } + } + + /// + /// Gets or sets the iteration limit for the individual Key Derivation Function (KDF) calculations. + /// + /// + /// The iteration limit for the individual Key Derivation Function (KDF) calculations, + /// or for no limit. + /// + public int? IndividualKdfIterationLimit + { + get => _individualKdfIterationLimit; + set + { + CheckNonNegative(value); + CheckReadOnly(); + _individualKdfIterationLimit = value; + } + } + + /// + /// Gets or sets the total iteration limit for the Key Derivation Function (KDF) calculations. + /// + /// + /// The total iteration limit for the Key Derivation Function (KDF) calculations, + /// or for no limit. + /// + public int? TotalKdfIterationLimit + { + get => _totalKdfIterationLimit; + set + { + CheckNonNegative(value); + CheckReadOnly(); + _totalKdfIterationLimit = value; + } + } + + /// + /// Gets or sets the maximum number of keys permitted. + /// + /// + /// The maximum number of keys permitted, or for no maximum. + /// + public int? MaxKeys + { + get => _maxKeys; + set + { + CheckNonNegative(value); + CheckReadOnly(); + _maxKeys = value; + } + } + + /// + /// Gets or sets the maximum number of certificates permitted. + /// + /// + /// The maximum number of certificates permitted, or for no maximum. + /// + public int? MaxCertificates + { + get => _maxCertificates; + set + { + CheckNonNegative(value); + CheckReadOnly(); + _maxCertificates = value; + } + } + + /// + /// Gets or sets a value indicating whether to preserve the storage provider. + /// + /// + /// to respect the storage provider identifier for a + /// private key; to ignore the storage provider + /// information and use the system defaults. + /// The default is . + /// + /// + /// Storage Provider values from the PFX are only processed on the + /// Microsoft Windows family of operating systems. + /// This property has no effect on non-Windows systems. + /// + public bool PreserveStorageProvider + { + get => _preserveStorageProvider; + set + { + CheckReadOnly(); + _preserveStorageProvider = value; + } + } + + /// + /// Gets or sets a value indicating whether to preserve the key name. + /// + /// + /// to respect the key name identifier for a + /// private key; to ignore the key name + /// information and use a randomly generated identifier. + /// The default is . + /// + /// + /// Key name identifier values from the PFX are only processed on the + /// Microsoft Windows family of operating systems. + /// This property has no effect on non-Windows systems. + /// + public bool PreserveKeyName + { + get => _preserveKeyName; + set + { + CheckReadOnly(); + _preserveKeyName = value; + } + } + + /// + /// Gets or sets a value indicating whether to preserve the certificate alias, + /// also known as the friendly name. + /// + /// + /// to respect the alias for a + /// certificate; to ignore the alias + /// information. + /// The default is . + /// + /// + /// Certificate alias values from the PFX are only processed on the + /// Microsoft Windows family of operating systems. + /// This property has no effect on non-Windows systems. + /// + /// + public bool PreserveCertificateAlias + { + get => _preserveCertificateAlias; + set + { + CheckReadOnly(); + _preserveCertificateAlias = value; + } + } + + /// + /// Gets or sets a value indicating whether to preserve unknown attributes. + /// + /// + /// to keep any attributes of a certificate or + /// private key that are not described by another property on this type intact + /// when invoking the system PKCS#12/PFX loader; + /// to remove the unknown attributes prior to invoking + /// the system loader. + /// The default is . + /// + public bool PreserveUnknownAttributes + { + get => _preserveUnknownAttributes; + set + { + CheckReadOnly(); + _preserveUnknownAttributes = value; + } + } + + /// + /// Gets or sets a value indicating whether to ignore private keys. + /// + /// + /// to skip loading private keys; + /// to load both certificates and private keys. + /// The default is . + /// + public bool IgnorePrivateKeys + { + get => _ignorePrivateKeys; + set + { + CheckReadOnly(); + _ignorePrivateKeys = value; + } + } + + /// + /// Gets or sets a value indicating whether to ignore encrypted authentication safes. + /// + /// + /// to skip over encrypted PFX AuthSafe values; + /// to decrypt encrypted PFX AuthSafe values to process their + /// contents. + /// The default is . + /// + public bool IgnoreEncryptedAuthSafes + { + get => _ignoreEncryptedAuthSafes; + set + { + CheckReadOnly(); + _ignoreEncryptedAuthSafes = value; + } + } + + private static void CheckNonNegative( + int? value, + [CallerArgumentExpression(nameof(value))] string? paramName = null) + { + // Null turns to 0, 0 is non-negative, so null is non-negative. + CheckNonNegative(value.GetValueOrDefault(), paramName); + } + + private static void CheckNonNegative( + int value, + [CallerArgumentExpression(nameof(value))] string? paramName = null) + { +#if NET + ArgumentOutOfRangeException.ThrowIfNegative(value, paramName); +#else + if (value < 0) + { + throw new ArgumentOutOfRangeException(paramName, SR.ArgumentOutOfRange_NeedNonNegNum); + } +#endif + } + + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Pkcs12.cs b/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Pkcs12.cs new file mode 100644 index 0000000000000..ccea2de41fc00 --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Pkcs12.cs @@ -0,0 +1,944 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Formats.Asn1; +using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs7; +using System.Security.Cryptography.Asn1.Pkcs12; +using System.Security.Cryptography.Pkcs; +using Internal.Cryptography; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + private const int CRYPT_E_BAD_DECODE = unchecked((int)0x80092002); + private const int ERROR_INVALID_PASSWORD = unchecked((int)0x80070056); + +#if NET + private const int NTE_FAIL = unchecked((int)0x80090020); +#endif + + static partial void LoadPkcs12NoLimits( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + ref Pkcs12Return earlyReturn); + + static partial void LoadPkcs12NoLimits( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + ref X509Certificate2Collection? earlyReturn); + + private static partial Pkcs12Return LoadPkcs12( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags); + + private static partial X509Certificate2Collection LoadPkcs12Collection( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags); + + private static Pkcs12Return LoadPkcs12( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + if (ReferenceEquals(loaderLimits, Pkcs12LoaderLimits.DangerousNoLimits)) + { + Pkcs12Return earlyReturn = default; + LoadPkcs12NoLimits(data, password, keyStorageFlags, ref earlyReturn); + + if (earlyReturn.HasValue()) + { + return earlyReturn; + } + } + + BagState bagState = default; + + try + { + ReadCertsAndKeys(ref bagState, data, ref password, loaderLimits); + + if (bagState.CertCount == 0) + { + throw new CryptographicException(SR.Cryptography_Pfx_NoCertificates); + } + + bagState.UnshroudKeys(ref password); + + return LoadPkcs12(ref bagState, password, keyStorageFlags); + } + finally + { + bagState.Dispose(); + } + } + + private static X509Certificate2Collection LoadPkcs12Collection( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + if (ReferenceEquals(loaderLimits, Pkcs12LoaderLimits.DangerousNoLimits)) + { + X509Certificate2Collection? earlyReturn = null; + LoadPkcs12NoLimits(data, password, keyStorageFlags, ref earlyReturn); + + if (earlyReturn is not null) + { + return earlyReturn; + } + } + + BagState bagState = default; + + try + { + ReadCertsAndKeys(ref bagState, data, ref password, loaderLimits); + + if (bagState.CertCount == 0) + { + return new X509Certificate2Collection(); + } + + bagState.UnshroudKeys(ref password); + + return LoadPkcs12Collection(ref bagState, password, keyStorageFlags); + } + finally + { + bagState.Dispose(); + } + } + + private static void ReadCertsAndKeys( + ref BagState bagState, + ReadOnlyMemory data, + ref ReadOnlySpan password, + Pkcs12LoaderLimits loaderLimits) + { + try + { + AsnDecoder.ReadSequence(data.Span, AsnEncodingRules.BER, out _, out _, out int trimLength); + data = data.Slice(0, trimLength); + + PfxAsn pfxAsn = PfxAsn.Decode(data, AsnEncodingRules.BER); + + if (pfxAsn.AuthSafe.ContentType != Oids.Pkcs7Data) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ReadOnlyMemory authSafeMemory = + Helpers.DecodeOctetStringAsMemory(pfxAsn.AuthSafe.Content); + ReadOnlySpan authSafeContents = authSafeMemory.Span; + + if (!password.IsEmpty) + { + bagState.LockPassword(); + } + + if (pfxAsn.MacData.HasValue) + { + if (pfxAsn.MacData.Value.IterationCount > loaderLimits.MacIterationLimit) + { + throw new Pkcs12LoadLimitExceededException(nameof(Pkcs12LoaderLimits.MacIterationLimit)); + } + + bool verified = false; + + if (!bagState.LockedPassword) + { + if (!pfxAsn.VerifyMac(password, authSafeContents)) + { + password = password.ContainsNull() ? "".AsSpan() : default; + } + else + { + verified = true; + } + } + + if (!verified && !pfxAsn.VerifyMac(password, authSafeContents)) + { + ThrowWithHResult(SR.Cryptography_Pfx_BadPassword, ERROR_INVALID_PASSWORD); + } + + bagState.ConfirmPassword(); + } + + AsnValueReader outer = new AsnValueReader(authSafeContents, AsnEncodingRules.BER); + AsnValueReader reader = outer.ReadSequence(); + outer.ThrowIfNotEmpty(); + + ReadOnlyMemory rebind = pfxAsn.AuthSafe.Content; + bagState.Init(loaderLimits); + + int? workRemaining = loaderLimits.TotalKdfIterationLimit; + + while (reader.HasData) + { + ContentInfoAsn.Decode(ref reader, rebind, out ContentInfoAsn safeContentsAsn); + + ReadOnlyMemory contentData; + + if (safeContentsAsn.ContentType == Oids.Pkcs7Data) + { + contentData = Helpers.DecodeOctetStringAsMemory(safeContentsAsn.Content); + } + else if (safeContentsAsn.ContentType == Oids.Pkcs7Encrypted) + { + if (loaderLimits.IgnoreEncryptedAuthSafes) + { + continue; + } + + bagState.PrepareDecryptBuffer(authSafeContents.Length); + + if (!bagState.LockedPassword) + { + bagState.LockPassword(); + int? workRemainingSave = workRemaining; + + try + { + contentData = DecryptSafeContents( + safeContentsAsn, + loaderLimits, + password, + ref bagState, + ref workRemaining); + } + catch (CryptographicException) + { + password = password.ContainsNull() ? "".AsSpan() : default; + workRemaining = workRemainingSave; + + contentData = DecryptSafeContents( + safeContentsAsn, + loaderLimits, + password, + ref bagState, + ref workRemaining); + } + } + else + { + contentData = DecryptSafeContents( + safeContentsAsn, + loaderLimits, + password, + ref bagState, + ref workRemaining); + } + } + else + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ProcessSafeContents( + contentData, + loaderLimits, + ref workRemaining, + ref bagState); + } + } + catch (AsnContentException e) + { + ThrowWithHResult(SR.Cryptography_Der_Invalid_Encoding, CRYPT_E_BAD_DECODE, e); + } + } + + private static void ProcessSafeContents( + ReadOnlyMemory contentData, + Pkcs12LoaderLimits loaderLimits, + ref int? workRemaining, + ref BagState bagState) + { + AsnValueReader outer = new AsnValueReader(contentData.Span, AsnEncodingRules.BER); + AsnValueReader reader = outer.ReadSequence(); + outer.ThrowIfNotEmpty(); + + while (reader.HasData) + { + SafeBagAsn.Decode(ref reader, contentData, out SafeBagAsn bag); + + if (bag.BagId == Oids.Pkcs12CertBag) + { + CertBagAsn certBag = CertBagAsn.Decode(bag.BagValue, AsnEncodingRules.BER); + + if (certBag.CertId == Oids.Pkcs12X509CertBagType) + { + if (bagState.CertCount >= loaderLimits.MaxCertificates) + { + throw new Pkcs12LoadLimitExceededException(nameof(Pkcs12LoaderLimits.MaxCertificates)); + } + + if (bag.BagAttributes is not null) + { + FilterAttributes( + loaderLimits, + ref bag, + static (limits, oid) => + oid switch + { + Oids.LocalKeyId => true, + Oids.Pkcs9FriendlyName => limits.PreserveCertificateAlias, + _ => limits.PreserveUnknownAttributes, + }); + } + + bagState.AddCert(bag); + } + } + else if (bag.BagId is Oids.Pkcs12KeyBag or Oids.Pkcs12ShroudedKeyBag) + { + if (loaderLimits.IgnorePrivateKeys) + { + continue; + } + + if (bagState.KeyCount >= loaderLimits.MaxKeys) + { + throw new Pkcs12LoadLimitExceededException(nameof(Pkcs12LoaderLimits.MaxKeys)); + } + + if (bag.BagId == Oids.Pkcs12ShroudedKeyBag) + { + EncryptedPrivateKeyInfoAsn epki = EncryptedPrivateKeyInfoAsn.Decode( + bag.BagValue, + AsnEncodingRules.BER); + + int kdfCount = GetKdfCount(epki.EncryptionAlgorithm); + + if (kdfCount > loaderLimits.IndividualKdfIterationLimit || kdfCount > workRemaining) + { + string propertyName = kdfCount > loaderLimits.IndividualKdfIterationLimit ? + nameof(Pkcs12LoaderLimits.IndividualKdfIterationLimit) : + nameof(Pkcs12LoaderLimits.TotalKdfIterationLimit); + + throw new Pkcs12LoadLimitExceededException(propertyName); + } + + if (workRemaining.HasValue) + { + workRemaining = checked(workRemaining - kdfCount); + } + } + + if (bag.BagAttributes is not null) + { + FilterAttributes( + loaderLimits, + ref bag, + static (limits, attrType) => + attrType switch + { + Oids.LocalKeyId => true, + Oids.Pkcs9FriendlyName => limits.PreserveKeyName, + Oids.MsPkcs12KeyProviderName => limits.PreserveStorageProvider, + _ => limits.PreserveUnknownAttributes, + }); + } + + bagState.AddKey(bag); + } + } + } + + private static void FilterAttributes( + Pkcs12LoaderLimits loaderLimits, + ref SafeBagAsn bag, + Func filter) + { + if (bag.BagAttributes is not null) + { + // Should this dedup/fail-on-dup? + int attrIdx = -1; + + for (int i = bag.BagAttributes.Length - 1; i > attrIdx; i--) + { + string attrType = bag.BagAttributes[i].AttrType; + + if (filter(loaderLimits, attrType)) + { + attrIdx++; + + if (i > attrIdx) + { + AttributeAsn attr = bag.BagAttributes[i]; + bag.BagAttributes[i] = bag.BagAttributes[attrIdx]; + bag.BagAttributes[attrIdx] = attr; + + // After swapping, back up one position to check if the attribute + // swapped into this position should also be preserved. + i++; + } + } + } + + attrIdx++; + + if (attrIdx < bag.BagAttributes.Length) + { + if (attrIdx == 0) + { + bag.BagAttributes = null; + } + else + { + Array.Resize(ref bag.BagAttributes, attrIdx); + } + } + } + } + + private static ReadOnlyMemory DecryptSafeContents( + ContentInfoAsn safeContentsAsn, + Pkcs12LoaderLimits loaderLimits, + ReadOnlySpan passwordSpan, + ref BagState bagState, + ref int? workRemaining) + { + EncryptedDataAsn encryptedData = + EncryptedDataAsn.Decode(safeContentsAsn.Content, AsnEncodingRules.BER); + + // https://tools.ietf.org/html/rfc5652#section-8 + if (encryptedData.Version != 0 && encryptedData.Version != 2) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // Since the contents are supposed to be the BER-encoding of an instance of + // SafeContents (https://tools.ietf.org/html/rfc7292#section-4.1) that implies the + // content type is simply "data", and that content is present. + if (encryptedData.EncryptedContentInfo.ContentType != Oids.Pkcs7Data) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (!encryptedData.EncryptedContentInfo.EncryptedContent.HasValue) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + ReadOnlyMemory encryptedContent = + encryptedData.EncryptedContentInfo.EncryptedContent.Value; + + int kdfCount = GetKdfCount(encryptedData.EncryptedContentInfo.ContentEncryptionAlgorithm); + + if (kdfCount > loaderLimits.IndividualKdfIterationLimit || kdfCount > workRemaining) + { + throw new Pkcs12LoadLimitExceededException( + kdfCount > loaderLimits.IndividualKdfIterationLimit ? + nameof(Pkcs12LoaderLimits.IndividualKdfIterationLimit) : + nameof(Pkcs12LoaderLimits.TotalKdfIterationLimit)); + } + + if (workRemaining.HasValue) + { + workRemaining = checked(workRemaining - kdfCount); + } + + return bagState.DecryptSafeContents( + encryptedData.EncryptedContentInfo.ContentEncryptionAlgorithm, + passwordSpan, + encryptedContent.Span); + } + + private static int GetKdfCount(in AlgorithmIdentifierAsn algorithmIdentifier) + { + int rawCount = GetRawKdfCount(in algorithmIdentifier); + + if (rawCount < 0) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return rawCount; + + static int GetRawKdfCount(in AlgorithmIdentifierAsn algorithmIdentifier) + { + if (!algorithmIdentifier.Parameters.HasValue) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + switch (algorithmIdentifier.Algorithm) + { + case Oids.PbeWithMD5AndDESCBC: + case Oids.PbeWithMD5AndRC2CBC: + case Oids.PbeWithSha1AndDESCBC: + case Oids.PbeWithSha1AndRC2CBC: + case Oids.Pkcs12PbeWithShaAnd3Key3Des: + case Oids.Pkcs12PbeWithShaAnd2Key3Des: + case Oids.Pkcs12PbeWithShaAnd128BitRC2: + case Oids.Pkcs12PbeWithShaAnd40BitRC2: + PBEParameter pbeParameter = PBEParameter.Decode( + algorithmIdentifier.Parameters.Value, + AsnEncodingRules.BER); + + return pbeParameter.IterationCount; + case Oids.PasswordBasedEncryptionScheme2: + PBES2Params pbes2Params = PBES2Params.Decode( + algorithmIdentifier.Parameters.Value, + AsnEncodingRules.BER); + + if (pbes2Params.KeyDerivationFunc.Algorithm != Oids.Pbkdf2) + { + throw new CryptographicException( + SR.Format( + SR.Cryptography_UnknownAlgorithmIdentifier, + pbes2Params.EncryptionScheme.Algorithm)); + } + + if (!pbes2Params.KeyDerivationFunc.Parameters.HasValue) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + Pbkdf2Params pbkdf2Params = Pbkdf2Params.Decode( + pbes2Params.KeyDerivationFunc.Parameters.Value, + AsnEncodingRules.BER); + + return pbkdf2Params.IterationCount; + default: + throw new CryptographicException( + SR.Format( + SR.Cryptography_UnknownAlgorithmIdentifier, + algorithmIdentifier.Algorithm)); + } + } + } + + private readonly partial struct Pkcs12Return + { + internal partial bool HasValue(); + internal partial X509Certificate2 ToCertificate(); + } + + private partial struct BagState + { + private SafeBagAsn[]? _certBags; + private SafeBagAsn[]? _keyBags; + private byte[]? _decryptBuffer; + private byte[]? _keyDecryptBuffer; + private int _certCount; + private int _keyCount; + private int _decryptBufferOffset; + private int _keyDecryptBufferOffset; + private byte _passwordState; + + internal void Init(Pkcs12LoaderLimits loaderLimits) + { + _certBags = ArrayPool.Shared.Rent(loaderLimits.MaxCertificates.GetValueOrDefault(10)); + _keyBags = ArrayPool.Shared.Rent(loaderLimits.MaxKeys.GetValueOrDefault(10)); + _certCount = 0; + _keyCount = 0; + _decryptBufferOffset = 0; + } + + public void Dispose() + { + if (_certBags is not null) + { + ArrayPool.Shared.Return(_certBags, clearArray: true); + } + + if (_keyBags is not null) + { + ArrayPool.Shared.Return(_keyBags, clearArray: true); + } + + if (_decryptBuffer is not null) + { + CryptoPool.Return(_decryptBuffer, _decryptBufferOffset); + } + + if (_keyDecryptBuffer is not null) + { + CryptoPool.Return(_keyDecryptBuffer, _keyDecryptBufferOffset); + } + + this = default; + } + + public readonly int CertCount => _certCount; + + public readonly int KeyCount => _keyCount; + + internal bool LockedPassword => (_passwordState & 1) != 0; + + internal void LockPassword() + { + _passwordState |= 1; + } + + internal bool ConfirmedPassword => (_passwordState & 2) != 0; + + internal void ConfirmPassword() + { + // Confirming it (verifying it was correct), also locks it. + _passwordState |= 3; + } + + internal void PrepareDecryptBuffer(int upperBound) + { + if (_decryptBuffer is null) + { + _decryptBuffer = CryptoPool.Rent(upperBound); + } + else + { + Debug.Assert(_decryptBuffer.Length >= upperBound); + } + } + + internal ReadOnlyMemory DecryptSafeContents( + in AlgorithmIdentifierAsn algorithmIdentifier, + ReadOnlySpan passwordSpan, + ReadOnlySpan encryptedContent) + { + Debug.Assert(_decryptBuffer is not null); + Debug.Assert(_decryptBuffer.Length - _decryptBufferOffset >= encryptedContent.Length); + + // In case anything goes wrong decrypting, clear the whole buffer in the cleanup + int saveOffset = _decryptBufferOffset; + _decryptBufferOffset = _decryptBuffer.Length; + + try + { + int written = PasswordBasedEncryption.Decrypt( + algorithmIdentifier, + passwordSpan, + default, + encryptedContent, + _decryptBuffer.AsSpan(saveOffset)); + + _decryptBufferOffset = saveOffset + written; + + try + { + AsnValueReader reader = new AsnValueReader( + _decryptBuffer.AsSpan(saveOffset, written), + AsnEncodingRules.BER); + + reader.ReadSequence(); + reader.ThrowIfNotEmpty(); + } + catch (AsnContentException) + { + ThrowWithHResult(SR.Cryptography_Der_Invalid_Encoding, CRYPT_E_BAD_DECODE); + } + + ConfirmPassword(); + + return new ReadOnlyMemory( + _decryptBuffer, + saveOffset, + written); + } + catch (CryptographicException e) + { + CryptographicOperations.ZeroMemory( + _decryptBuffer.AsSpan(saveOffset, _decryptBufferOffset - saveOffset)); + + _decryptBufferOffset = saveOffset; + +#if NET + if (e.HResult != CRYPT_E_BAD_DECODE) + { + e.HResult = ConfirmedPassword ? NTE_FAIL : ERROR_INVALID_PASSWORD; + } +#else + Debug.Assert(e.HResult != 0); +#endif + + throw; + } + } + + internal void AddCert(SafeBagAsn bag) + { + Debug.Assert(_certBags is not null); + + GrowIfNeeded(ref _certBags, _certCount); + _certBags[_certCount] = bag; + _certCount++; + } + + internal void AddKey(SafeBagAsn bag) + { + Debug.Assert(_keyBags is not null); + + GrowIfNeeded(ref _keyBags, _keyCount); + _keyBags[_keyCount] = bag; + _keyCount++; + } + + private static void GrowIfNeeded(ref SafeBagAsn[] array, int index) + { + if (array.Length <= index) + { + SafeBagAsn[] next = ArrayPool.Shared.Rent(checked(index + 1)); + array.AsSpan().CopyTo(next); + ArrayPool.Shared.Return(array, clearArray: true); + array = next; + } + } + + internal void UnshroudKeys(ref ReadOnlySpan password) + { + Debug.Assert(_keyBags is not null); + + int spaceRequired = 0; + + for (int i = 0; i < _keyCount; i++) + { + SafeBagAsn bag = _keyBags[i]; + + if (bag.BagId == Oids.Pkcs12ShroudedKeyBag) + { + spaceRequired += bag.BagValue.Length; + } + } + + _keyDecryptBuffer = CryptoPool.Rent(spaceRequired); + + for (int i = 0; i < _keyCount; i++) + { + ref SafeBagAsn bag = ref _keyBags[i]; + + if (bag.BagId == Oids.Pkcs12ShroudedKeyBag) + { + ArraySegment decrypted = default; + int contentRead = 0; + + if (!LockedPassword) + { + try + { + decrypted = KeyFormatHelper.DecryptPkcs8( + password, + bag.BagValue, + out contentRead); + + try + { + AsnValueReader reader = new AsnValueReader(decrypted, AsnEncodingRules.BER); + reader.ReadSequence(); + reader.ThrowIfNotEmpty(); + } + catch (AsnContentException) + { + CryptoPool.Return(decrypted); + decrypted = default; + throw new CryptographicException(); + } + } + catch (CryptographicException) + { + password = password.ContainsNull() ? "".AsSpan() : default; + } + } + + if (decrypted.Array is null) + { + try + { + decrypted = KeyFormatHelper.DecryptPkcs8( + password, + bag.BagValue, + out contentRead); + + try + { + AsnValueReader reader = new AsnValueReader(decrypted, AsnEncodingRules.BER); + reader.ReadSequence(); + reader.ThrowIfNotEmpty(); + } + catch (AsnContentException) + { + CryptoPool.Return(decrypted); + decrypted = default; + throw new CryptographicException(); + } + } + catch (CryptographicException) + { + // Windows 10 compatibility: + // If anything goes wrong loading this key, just ignore it. + // If no one ended up needing it, no harm/no foul. + // If this has a LocalKeyId and something references it, then it'll fail. + + continue; + } + } + + ConfirmPassword(); + + Debug.Assert(decrypted.Array is not null); + Debug.Assert(_keyDecryptBuffer.Length - _keyDecryptBufferOffset >= decrypted.Count); + decrypted.AsSpan().CopyTo(_keyDecryptBuffer.AsSpan(_keyDecryptBufferOffset)); + + ReadOnlyMemory newBagValue = new( + _keyDecryptBuffer, + _keyDecryptBufferOffset, + decrypted.Count); + + CryptoPool.Return(decrypted); + _keyDecryptBufferOffset += newBagValue.Length; + + if (contentRead != bag.BagValue.Length) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + bag.BagValue = newBagValue; + bag.BagId = Oids.Pkcs12KeyBag; + } + } + } + + internal +#if !NET + unsafe +#endif + ArraySegment ToPfx(ReadOnlySpan password) + { + Debug.Assert(_certBags is not null); + Debug.Assert(_keyBags is not null); + + ContentInfoAsn safeContents = new ContentInfoAsn + { + ContentType = Oids.Pkcs7Data, + }; + + AsnWriter writer = new AsnWriter(AsnEncodingRules.BER); + + using (writer.PushOctetString()) + using (writer.PushSequence()) + { + for (int i = 0; i < _certCount; i++) + { + SafeBagAsn bag = _certBags[i]; + bag.Encode(writer); + } + + for (int i = 0; i < _keyCount; i++) + { + SafeBagAsn bag = _keyBags[i]; + bag.Encode(writer); + } + } + + safeContents.Content = writer.Encode(); + writer.Reset(); + + using (writer.PushSequence()) + { + safeContents.Encode(writer); + } + + byte[] authSafe = writer.Encode(); + writer.Reset(); + + const int Sha1MacSize = 20; + HashAlgorithmName hashAlgorithm = HashAlgorithmName.SHA1; + Span salt = stackalloc byte[Sha1MacSize]; + Helpers.RngFill(salt); + +#if NET + Span macKey = stackalloc byte[Sha1MacSize]; +#else + byte[] macKey = new byte[Sha1MacSize]; +#endif + + // Pin macKey (if it's on the heap), derive the key into it, then overwrite it with the MAC output. +#if !NET + fixed (byte* macKeyPin = macKey) +#endif + { + Pkcs12Kdf.DeriveMacKey( + password, + hashAlgorithm, + 1, + salt, + macKey); + + using (IncrementalHash mac = IncrementalHash.CreateHMAC(hashAlgorithm, macKey)) + { + mac.AppendData(authSafe); + + if (!mac.TryGetHashAndReset(macKey, out int bytesWritten) || bytesWritten != macKey.Length) + { + Debug.Fail($"TryGetHashAndReset wrote {bytesWritten} of {macKey.Length} bytes"); + throw new CryptographicException(); + } + } + } + + // https://tools.ietf.org/html/rfc7292#section-4 + // + // PFX ::= SEQUENCE { + // version INTEGER {v3(3)}(v3,...), + // authSafe ContentInfo, + // macData MacData OPTIONAL + // } + using (writer.PushSequence()) + { + writer.WriteInteger(3); + + using (writer.PushSequence()) + { + writer.WriteObjectIdentifierForCrypto(Oids.Pkcs7Data); + + Asn1Tag contextSpecific0 = new Asn1Tag(TagClass.ContextSpecific, 0); + + using (writer.PushSequence(contextSpecific0)) + { + writer.WriteOctetString(authSafe); + } + } + + // https://tools.ietf.org/html/rfc7292#section-4 + // + // MacData ::= SEQUENCE { + // mac DigestInfo, + // macSalt OCTET STRING, + // iterations INTEGER DEFAULT 1 + // -- Note: The default is for historical reasons and its use is + // -- deprecated. + // } + using (writer.PushSequence()) + { + using (writer.PushSequence()) + { + using (writer.PushSequence()) + { + writer.WriteObjectIdentifierForCrypto(Oids.Sha1); + } + + writer.WriteOctetString(macKey); + } + + writer.WriteOctetString(salt); + } + } + + byte[] ret = CryptoPool.Rent(writer.GetEncodedLength()); + int written = writer.Encode(ret); + return new ArraySegment(ret, 0, written); + } + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.cs b/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.cs new file mode 100644 index 0000000000000..a4ef6480b0eea --- /dev/null +++ b/src/libraries/Common/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.cs @@ -0,0 +1,741 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Formats.Asn1; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Runtime.CompilerServices; +using System.Runtime.Versioning; + +namespace System.Security.Cryptography.X509Certificates +{ + [UnsupportedOSPlatform("browser")] + public static partial class X509CertificateLoader + { + private const int MemoryMappedFileCutoff = 1_048_576; + + /// + /// Loads a single X.509 certificate from , in either the PEM + /// or DER encoding. + /// + /// The data to load. + /// + /// The certificate loaded from . + /// + /// + /// The data did not load as a valid X.509 certificate. + /// + /// + /// This method only loads plain certificates, which are identified as + /// by + /// + /// + public static partial X509Certificate2 LoadCertificate(ReadOnlySpan data); + + /// + /// Loads a single X.509 certificate from , in either the PEM + /// or DER encoding. + /// + /// The data to load. + /// + /// The certificate loaded from . + /// + /// + /// is . + /// + /// + /// The data did not load as a valid X.509 certificate. + /// + /// + /// This method only loads plain certificates, which are identified as + /// by + /// + /// + public static partial X509Certificate2 LoadCertificate(byte[] data); + + /// + /// Loads a single X.509 certificate (in either the PEM or DER encoding) + /// from the specified file. + /// + /// The path of the file to open. + /// + /// The loaded certificate. + /// + /// + /// is . + /// + /// + /// The data did not load as a valid X.509 certificate. + /// + /// + /// An error occurred while loading the specified file. + /// + /// + /// This method only loads plain certificates, which are identified as + /// by + /// + /// + public static partial X509Certificate2 LoadCertificateFromFile(string path); + + /// + /// Loads the provided data as a PKCS#12 PFX and extracts a certificate. + /// + /// The data to load. + /// The password to decrypt the contents of the PFX. + /// + /// A bitwise combination of the enumeration values that control where and how to + /// import the private key associated with the returned certificate. + /// + /// + /// Limits to apply when loading the PFX. A value, the default, + /// is equivalent to . + /// + /// The loaded certificate. + /// + /// is . + /// + /// + /// contains a value, or combination of values, + /// that is not valid. + /// + /// + /// contains a value that is not valid for the + /// current platform. + /// + /// + /// The PKCS#12/PFX violated one or more constraints of . + /// + /// + /// An error occurred while loading the PKCS#12/PFX. + /// + /// + /// A PKCS#12/PFX can contain multiple certificates. + /// Using the ordering that the certificates appear in the results of + /// , + /// this method returns the first + /// certificate where is + /// . + /// If no certificates have associated private keys, then the first + /// certificate is returned. + /// If the PKCS#12/PFX contains no certificates, a + /// is thrown. + /// + public static X509Certificate2 LoadPkcs12( + byte[] data, + string? password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet, + Pkcs12LoaderLimits? loaderLimits = null) + { + ThrowIfNull(data); + ValidateKeyStorageFlagsCore(keyStorageFlags); + + return LoadPkcs12( + new ReadOnlyMemory(data), + password.AsSpan(), + keyStorageFlags, + loaderLimits ?? Pkcs12LoaderLimits.Defaults).ToCertificate(); + } + + /// + /// Loads the provided data as a PKCS#12 PFX and extracts a certificate. + /// + /// The data to load. + /// The password to decrypt the contents of the PFX. + /// + /// A bitwise combination of the enumeration values that control where and how to + /// import the private key associated with the returned certificate. + /// + /// + /// Limits to apply when loading the PFX. A value, the default, + /// is equivalent to . + /// + /// The loaded certificate. + /// + /// is . + /// + /// + /// contains a value, or combination of values, + /// that is not valid. + /// + /// + /// contains a value that is not valid for the + /// current platform. + /// + /// + /// The PKCS#12/PFX violated one or more constraints of . + /// + /// + /// An error occurred while loading the PKCS#12/PFX. + /// + /// + /// A PKCS#12/PFX can contain multiple certificates. + /// Using the ordering that the certificates appear in the results of + /// , + /// this method returns the first + /// certificate where is + /// . + /// If no certificates have associated private keys, then the first + /// certificate is returned. + /// If the PKCS#12/PFX contains no certificates, a + /// is thrown. + /// + public static X509Certificate2 LoadPkcs12( + ReadOnlySpan data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet, + Pkcs12LoaderLimits? loaderLimits = null) + { + unsafe + { + fixed (byte* pinned = data) + { + using (PointerMemoryManager manager = new(pinned, data.Length)) + { + return LoadPkcs12( + manager.Memory, + password, + keyStorageFlags, + loaderLimits ?? Pkcs12LoaderLimits.Defaults).ToCertificate(); + } + } + } + } + + /// + /// Opens the specified file, reads the contents as a PKCS#12 PFX and extracts a certificate. + /// + /// The path of the file to open. + /// + /// The loaded certificate. + /// + /// The password to decrypt the contents of the PFX. + /// + /// A bitwise combination of the enumeration values that control where and how to + /// import the private key associated with the returned certificate. + /// + /// + /// Limits to apply when loading the PFX. A value, the default, + /// is equivalent to . + /// + /// The loaded certificate. + /// + /// is . + /// + /// + /// contains a value, or combination of values, + /// that is not valid. + /// + /// + /// contains a value that is not valid for the + /// current platform. + /// + /// + /// The PKCS#12/PFX violated one or more constraints of . + /// + /// + /// An error occurred while loading the PKCS#12/PFX. + /// + /// + /// An error occurred while loading the specified file. + /// + /// + /// A PKCS#12/PFX can contain multiple certificates. + /// Using the ordering that the certificates appear in the results of + /// , + /// this method returns the first + /// certificate where is + /// . + /// If no certificates have associated private keys, then the first + /// certificate is returned. + /// If the PKCS#12/PFX contains no certificates, a + /// is thrown. + /// + public static X509Certificate2 LoadPkcs12FromFile( + string path, + string? password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet, + Pkcs12LoaderLimits? loaderLimits = null) + { + return LoadPkcs12FromFile( + path, + password.AsSpan(), + keyStorageFlags, + loaderLimits); + } + + /// + /// Opens the specified file, reads the contents as a PKCS#12 PFX and extracts a certificate. + /// + /// The path of the file to open. + /// + /// The loaded certificate. + /// + /// The password to decrypt the contents of the PFX. + /// + /// A bitwise combination of the enumeration values that control where and how to + /// import the private key associated with the returned certificate. + /// + /// + /// Limits to apply when loading the PFX. A value, the default, + /// is equivalent to . + /// + /// The loaded certificate. + /// + /// is . + /// + /// + /// contains a value, or combination of values, + /// that is not valid. + /// + /// + /// contains a value that is not valid for the + /// current platform. + /// + /// + /// The PKCS#12/PFX violated one or more constraints of . + /// + /// + /// An error occurred while loading the PKCS#12/PFX. + /// + /// + /// An error occurred while loading the specified file. + /// + /// + /// A PKCS#12/PFX can contain multiple certificates. + /// Using the ordering that the certificates appear in the results of + /// , + /// this method returns the first + /// certificate where is + /// . + /// If no certificates have associated private keys, then the first + /// certificate is returned. + /// If the PKCS#12/PFX contains no certificates, a + /// is thrown. + /// + public static X509Certificate2 LoadPkcs12FromFile( + string path, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet, + Pkcs12LoaderLimits? loaderLimits = null) + { + ThrowIfNullOrEmpty(path); + ValidateKeyStorageFlagsCore(keyStorageFlags); + + return LoadFromFile( + path, + password, + keyStorageFlags, + loaderLimits ?? Pkcs12LoaderLimits.Defaults, + LoadPkcs12).ToCertificate(); + } + + /// + /// Loads the provided data as a PKCS#12 PFX and returns a collection of + /// all of the certificates therein. + /// + /// The data to load. + /// The password to decrypt the contents of the PFX. + /// + /// A bitwise combination of the enumeration values that control where and how to + /// import the private key associated with the returned certificate. + /// + /// + /// Limits to apply when loading the PFX. A value, the default, + /// is equivalent to . + /// + /// A collection of the certificates loaded from the input. + /// + /// is . + /// + /// + /// contains a value, or combination of values, + /// that is not valid. + /// + /// + /// contains a value that is not valid for the + /// current platform. + /// + /// + /// The PKCS#12/PFX violated one or more constraints of . + /// + /// + /// An error occurred while loading the PKCS#12/PFX. + /// + public static X509Certificate2Collection LoadPkcs12Collection( + byte[] data, + string? password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet, + Pkcs12LoaderLimits? loaderLimits = null) + { + ThrowIfNull(data); + ValidateKeyStorageFlagsCore(keyStorageFlags); + + return LoadPkcs12Collection( + new ReadOnlyMemory(data), + password.AsSpan(), + keyStorageFlags, + loaderLimits ?? Pkcs12LoaderLimits.Defaults); + } + + /// + /// Loads the provided data as a PKCS#12 PFX and returns a collection of + /// all of the certificates therein. + /// + /// The data to load. + /// The password to decrypt the contents of the PFX. + /// + /// A bitwise combination of the enumeration values that control where and how to + /// import the private key associated with the returned certificate. + /// + /// + /// Limits to apply when loading the PFX. A value, the default, + /// is equivalent to . + /// + /// A collection of the certificates loaded from the input. + /// + /// is . + /// + /// + /// contains a value, or combination of values, + /// that is not valid. + /// + /// + /// contains a value that is not valid for the + /// current platform. + /// + /// + /// The PKCS#12/PFX violated one or more constraints of . + /// + /// + /// An error occurred while loading the PKCS#12/PFX. + /// + public static X509Certificate2Collection LoadPkcs12Collection( + ReadOnlySpan data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet, + Pkcs12LoaderLimits? loaderLimits = null) + { + ValidateKeyStorageFlagsCore(keyStorageFlags); + + unsafe + { + fixed (byte* pinned = data) + { + using (PointerMemoryManager manager = new(pinned, data.Length)) + { + return LoadPkcs12Collection( + manager.Memory, + password, + keyStorageFlags, + loaderLimits ?? Pkcs12LoaderLimits.Defaults); + } + } + } + } + + /// + /// Opens the specified file, reads the contents as a PKCS#12 PFX and extracts a certificate. + /// Loads the provided data as a PKCS#12 PFX and returns a collection of + /// all of the certificates therein. + /// + /// The path of the file to open. + /// + /// The loaded certificate. + /// + /// The password to decrypt the contents of the PFX. + /// + /// A bitwise combination of the enumeration values that control where and how to + /// import the private key associated with the returned certificate. + /// + /// + /// Limits to apply when loading the PFX. A value, the default, + /// is equivalent to . + /// + /// The loaded certificate. + /// + /// is . + /// + /// + /// contains a value, or combination of values, + /// that is not valid. + /// + /// + /// contains a value that is not valid for the + /// current platform. + /// + /// + /// The PKCS#12/PFX violated one or more constraints of . + /// + /// + /// An error occurred while loading the PKCS#12/PFX. + /// + /// + /// An error occurred while loading the specified file. + /// + public static X509Certificate2Collection LoadPkcs12CollectionFromFile( + string path, + string? password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet, + Pkcs12LoaderLimits? loaderLimits = null) + { + return LoadPkcs12CollectionFromFile( + path, + password.AsSpan(), + keyStorageFlags, + loaderLimits); + } + + /// + /// Opens the specified file, reads the contents as a PKCS#12 PFX and extracts a certificate. + /// Loads the provided data as a PKCS#12 PFX and returns a collection of + /// all of the certificates therein. + /// + /// The path of the file to open. + /// + /// The loaded certificate. + /// + /// The password to decrypt the contents of the PFX. + /// + /// A bitwise combination of the enumeration values that control where and how to + /// import the private key associated with the returned certificate. + /// + /// + /// Limits to apply when loading the PFX. A value, the default, + /// is equivalent to . + /// + /// The loaded certificate. + /// + /// is . + /// + /// + /// contains a value, or combination of values, + /// that is not valid. + /// + /// + /// contains a value that is not valid for the + /// current platform. + /// + /// + /// The PKCS#12/PFX violated one or more constraints of . + /// + /// + /// An error occurred while loading the PKCS#12/PFX. + /// + /// + /// An error occurred while loading the specified file. + /// + public static X509Certificate2Collection LoadPkcs12CollectionFromFile( + string path, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags = X509KeyStorageFlags.DefaultKeySet, + Pkcs12LoaderLimits? loaderLimits = null) + { + ThrowIfNullOrEmpty(path); + ValidateKeyStorageFlagsCore(keyStorageFlags); + + return LoadFromFile( + path, + password, + keyStorageFlags, + loaderLimits ?? Pkcs12LoaderLimits.Defaults, + LoadPkcs12Collection); + } + + private delegate T LoadFromFileFunc( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits); + + private static T LoadFromFile( + string path, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits, + LoadFromFileFunc loader) + { + (byte[]? rented, int length, MemoryManager? mapped) = ReadAllBytesIfBerSequence(path); + + try + { + Debug.Assert(rented is null != mapped is null); + ReadOnlyMemory memory = mapped?.Memory ?? new ReadOnlyMemory(rented, 0, length); + + return loader(memory, password, keyStorageFlags, loaderLimits); + } + finally + { + (mapped as IDisposable)?.Dispose(); + + if (rented is not null) + { + CryptoPool.Return(rented, length); + } + } + } + + private static (byte[]?, int, MemoryManager?) ReadAllBytesIfBerSequence(string path) + { + // The expected header in a PFX is 30 82 XX XX, but since it's BER-encoded + // it could be up to 30 FE 00 00 00 .. XX YY ZZ AA and still be within the + // bounds of what we can load into an array. 30 FE would be followed by 0x7E bytes, + // so we need 0x81 total bytes for a tag and length using a maximal BER encoding. + Span earlyBuf = stackalloc byte[0x81]; + + try + { + using (FileStream stream = File.OpenRead(path)) + { + int read = stream.ReadAtLeast(earlyBuf, 2); + + if (earlyBuf[0] != 0x30 || earlyBuf[1] is 0 or 1) + { + ThrowWithHResult(SR.Cryptography_Der_Invalid_Encoding, CRYPT_E_BAD_DECODE); + } + + int totalLength; + + if (earlyBuf[1] < 0x80) + { + // The two bytes we already read, plus the interpreted length + totalLength = earlyBuf[1] + 2; + } + else if (earlyBuf[1] == 0x80) + { + // indeterminate length + long streamLength = stream.Length; + + if (streamLength < MemoryMappedFileCutoff) + { + totalLength = (int)streamLength; + } + else + { + totalLength = -1; + } + } + else + { + int lengthLength = earlyBuf[1] - 0x80; + int toRead = lengthLength - read; + + if (toRead > 0) + { + int localRead = stream.ReadAtLeast(earlyBuf.Slice(read), toRead); + read += localRead; + } + + ReadOnlySpan lengthPart = earlyBuf.Slice(1, read - 1); + + if (!AsnDecoder.TryDecodeLength(lengthPart, AsnEncodingRules.BER, out int? decoded, out int decodedLength)) + { + ThrowWithHResult(SR.Cryptography_Der_Invalid_Encoding, CRYPT_E_BAD_DECODE); + } + + Debug.Assert(decoded.HasValue); + + // The interpreted value, the bytes involved in the length (which includes earlyBuf[1]), + // and the tag (earlyBuf[0]) + totalLength = decoded.GetValueOrDefault() + decodedLength + 1; + } + + if (totalLength >= 0) + { + byte[] rented = CryptoPool.Rent(totalLength); + earlyBuf.Slice(0, read).CopyTo(rented); + + stream.ReadExactly(rented.AsSpan(read, totalLength - read)); + return (rented, totalLength, null); + } + + return (null, 0, MemoryMappedFileMemoryManager.CreateFromFileClamped(stream)); + } + } + catch (IOException e) + { + throw new CryptographicException(SR.Arg_CryptographyException, e); + } + catch (UnauthorizedAccessException e) + { + throw new CryptographicException(SR.Arg_CryptographyException, e); + } + } + + [DoesNotReturn] + private static void ThrowWithHResult(string message, int hResult) + { +#if NET + throw new CryptographicException(message) + { + HResult = hResult, + }; +#else +#if NETSTANDARD + if (!Runtime.InteropServices.RuntimeInformation.IsOSPlatform(Runtime.InteropServices.OSPlatform.Windows)) + { + throw new CryptographicException(message); + } +#endif + throw new CryptographicException(hResult); +#endif + } + + static partial void ValidateKeyStorageFlagsCore(X509KeyStorageFlags keyStorageFlags); + + [DoesNotReturn] + private static void ThrowWithHResult(string message, int hResult, Exception innerException) + { +#if NET + throw new CryptographicException(message, innerException) + { + HResult = hResult, + }; +#else +#if NETSTANDARD + if (!Runtime.InteropServices.RuntimeInformation.IsOSPlatform(Runtime.InteropServices.OSPlatform.Windows)) + { + throw new CryptographicException(message, innerException); + } +#endif + + throw new CryptographicException(hResult); +#endif + } + + private static void ThrowIfNull( + [NotNull] object? argument, + [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (argument is null) + { + ThrowNull(paramName); + } + } + + private static void ThrowIfNullOrEmpty( + [NotNull] string? argument, + [CallerArgumentExpression(nameof(argument))] string? paramName = null) + { + if (string.IsNullOrEmpty(argument)) + { + ThrowNullOrEmpty(argument, paramName); + } + } + + [DoesNotReturn] + private static void ThrowNull(string? paramName) + { + throw new ArgumentNullException(paramName); + } + + [DoesNotReturn] + private static void ThrowNullOrEmpty(string? argument, string? paramName) + { + ThrowIfNull(argument, paramName); + throw new ArgumentException(SR.Argument_EmptyString, paramName); + } + } +} diff --git a/src/libraries/Common/src/System/Security/Cryptography/X509IterationCountExceededException.cs b/src/libraries/Common/src/System/Security/Cryptography/X509IterationCountExceededException.cs deleted file mode 100644 index 60ee5d47a2af6..0000000000000 --- a/src/libraries/Common/src/System/Security/Cryptography/X509IterationCountExceededException.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Security.Cryptography.X509Certificates -{ - internal sealed class X509IterationCountExceededException : Exception - { - } -} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/ByteUtils.cs b/src/libraries/Common/tests/System/Security/Cryptography/ByteUtils.cs index 8a1a95bb0984b..569ef966f13cb 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/ByteUtils.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/ByteUtils.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; +using System.Security.Cryptography; using System.Text; namespace Test.Cryptography @@ -72,5 +73,15 @@ internal static byte[] RepeatByte(byte b, int count) return value; } + + internal static string PemEncode(string label, byte[] data) + { +#if NET + return PemEncoding.WriteString(label, data); +#else + return + $"-----BEGIN {label}-----\n{Convert.ToBase64String(data, Base64FormattingOptions.InsertLineBreaks)}\n-----END {label}-----"; +#endif + } } } diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/TempFileHolder.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/TempFileHolder.cs similarity index 87% rename from src/libraries/System.Security.Cryptography/tests/X509Certificates/TempFileHolder.cs rename to src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/TempFileHolder.cs index a6efa3a1f1917..23b844cd65549 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/TempFileHolder.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/TempFileHolder.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.IO; namespace System.Security.Cryptography.X509Certificates.Tests @@ -16,7 +15,13 @@ public TempFileHolder(ReadOnlySpan content) using (StreamWriter writer = new StreamWriter(FilePath, append: false)) { - writer.Write(content); + writer.Write( +#if NET + content +#else + content.ToArray() +#endif + ); } } diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/TestData.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/TestData.cs similarity index 94% rename from src/libraries/System.Security.Cryptography/tests/X509Certificates/TestData.cs rename to src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/TestData.cs index 0e0295ecf7f6e..9233360c94142 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/TestData.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/TestData.cs @@ -7,6 +7,8 @@ namespace System.Security.Cryptography.X509Certificates.Tests { internal static class TestData { + internal const string PlaceholderPw = "Placeholder"; + public static byte[] MsCertificate = ( "308204ec308203d4a003020102021333000000b011af0a8bd03b9fdd00010000" + "00b0300d06092a864886f70d01010505003079310b3009060355040613025553" + @@ -4295,5 +4297,257 @@ internal static DSAParameters GetDSA1024Params() "0D6DB9939E88B998662A27F092634BBF21F58EEAAA").HexToByteArray(); internal static readonly byte[] EmptyPkcs7 = "300B06092A864886F70D010702".HexToByteArray(); + + // This is an RSA-1024 certificate, "CN=outer" which has an extension with + // the unregistered OID 0.0.1 that is a PEM-encoded ECDSA-secp521r1 + // certificate ("CN=inner"). + internal static readonly byte[] NestedCertificates = ( + "3082041930820382A00302010202084062D86F6A371DD7300D06092A864886F7" + + "0D01010B05003010310E300C060355040313056F75746572301E170D32343031" + + "32353031333630315A170D3234303132353031343630315A3010310E300C0603" + + "55040313056F7574657230819F300D06092A864886F70D010101050003818D00" + + "30818902818100AC24F75F85C4C38E6DBF57DE889AED7598DD01202FACC00EA7" + + "4EC449DD420E7CB49992E1BCCC69B3EAB8B2AEECA5B2BCFC1295DC82B83EDB71" + + "C1764191EA0F73D1BAB26D03B95F322EF8299B1DADAECCAADEBA052BD3BC2549" + + "D83AD1C2FB2DC556370306AC3CBCE09F3669448EDEF8FCA12164D793D5B456A7" + + "418393899E00590203010001A382027A3082027630820272060200010482026A" + + "0D0A2D2D2D2D2D424547494E2043455254494649434154452D2D2D2D2D0A4D49" + + "49426D6A43422F61414441674543416767312B7233664B4775457054414B4267" + + "6771686B6A4F50515144416A41514D5134774441594456515144457756700A62" + + "6D356C636A4165467730794E4441784D6A55774D544D324D444661467730794E" + + "4441784D6A55774D5451324D4446614D424178446A414D42674E5642414D540A" + + "42576C75626D56794D4947624D42414742797147534D34394167454742537542" + + "4241416A4134474741415142513143437A4448737A724E6839445456697A6E75" + + "0A4B38516F496B5649324138494E676B4B6F6B3649704158484C533058767333" + + "4E43745152396973486C5A376C707844366C6B72376449793363396756427063" + + "630A7131734242796B73534854745A4766337A2F6E526E5A3961684463755444" + + "2F486E71302B326A33476461423557594B6733336E65337730423258385A7377" + + "65390A46774F2B335A6954734D6D647038312B6B4D6C76463775453236597743" + + "6759494B6F5A497A6A304541774944675973414D494748416B4942454B6C4654" + + "4978560A434274324839636A30595042493632457734752F665A4B4A53657272" + + "7848707244476C67546B36635075585345723569395A397731505A3872654A51" + + "7A7367690A6C736268363653584B4F6C67466945435153587972457736633565" + + "734B587A6269484F7762564E4756305A4430424A6761316533667144507A4170" + + "75527734510A75656A636749684E723577504E7A4E696A464D57697944716D79" + + "6E586D4B6E627933457050396D370A2D2D2D2D2D454E44204345525449464943" + + "4154452D2D2D2D2D0D0A300D06092A864886F70D01010B05000381810032833C" + + "38E5F66D64880C191D247CE8B819899B20F284DD59CBDE1731A4D0FE09A95A52" + + "D5A7D049CA108BEB9673FD207C842B324DFD8086C9E1CDAB931D7403730E0521" + + "69943C58ECC3DA6E11C6ED4F16455152D6FF4D104C88976F9BA88120B0889563" + + "1378357F297A6B3E444296C06C636A589973250F2A096C39C1EDE5C1C9").HexToByteArray(); + + // This is a PFX that uses mixed algorithms and iteration counts. + // PFX { + // SC[0], encrypted, PBES2: AES128CBC, SHA256, 2000 iterations { + // cert, keyid=1 + // }, + // SC[1], plaintext { + // shrouded_key, PBES2: AES192CBC, SHA384, 2001 iterations, keyid=1 + // }, + // mac: SHA384, 39 iterations. + // } + // "Placeholder" + internal static readonly byte[] MixedIterationsPfx = ( + "30820A320201033082099206092A864886F70D010701A08209830482097F3082" + + "097B308203FA06092A864886F70D010706A08203EB308203E7020100308203E0" + + "06092A864886F70D010701305F06092A864886F70D01050D3052303106092A86" + + "4886F70D01050C302404101215FB90500DC30882E7D84625F97923020207D030" + + "0C06082A864886F70D020A0500301D06096086480165030401160410558E3FAE" + + "FE92ADEC4CF1405E7DE7C5118082037095F887CE32E748EC0D647062286EA9D0" + + "C3C04213ED9431FFE0F6028105D94D088684A55C997630D16EF7CE7D49729C30" + + "1F750D8864B988ABBAF551A8819ED7D17CBB810CA3DC39EBD804B3C388C73BA9" + + "320783DF3AABEBAD9F44E4ABFF931AAA95AE874E348B49F35749614D9EE94F5E" + + "EC7A5C597FE8CB6CCDB7A0721EE5836C8F839D83E1577A753F8B2B7E0C2C53BD" + + "D365322E211A62DE061FAC3ECF770CEBCF37AC2604A1CF318C4DB1BA83C85779" + + "3BE57C898EA80D52E0F83B17F877210C1A5E819F5C35DE6DB5EBACE1BFB3101C" + + "C2FE9FF4983BAB7B93CA5D466B855D973FB5D72D8DD109B61FBA238320C886C0" + + "8FC141168C43B946AAC28C114B20A667D189E473E9779432EF64C965459AE9C4" + + "6430B94E19BCEB4733D3038D9F65B85D79B7A1DA954887BAE1DB0A999135A09A" + + "25C38616DBADB9BE50F729925F23C107F899341F76B4B2161CCAF8C499DF7646" + + "F7DB16A21EFD90097D75EEF893B8B8115F6A8816B2D9628278F8ADAFD0E4AC7F" + + "14BED13FFB6DD7FEDE1EA1990CACE772DBFDACC2A4547F910D3F2B238FE86A4F" + + "1BAEE8AC282F918D3F82B8149DD3A99B78D433AB0F5592E7B7B25FBC4A726520" + + "A49CF2FDA60D747F19031B34F911C442E20BD91600A32F9A4FCC1E3DF330171A" + + "4E0721CD01A93CC20C2B770990029C30306945AFC7E3ABD9516D8C6F359D904B" + + "5F32F3BAEC8770A8BE15C89E4B836B1F20D29A7D3D3BB7491EFC9B88A161B598" + + "02819D2EC695000C2F83B1A363217CC2B0A70A4A226993BD32BE302DCD157EEE" + + "3F9DBFBDE0375A03DEA2761941F49D42A8F5029DC3B65E661B61DFBE5F2527D6" + + "C59927B5A0857FE7B9836A63D202895C1FA6FDE40E70107659CE3E4C7DDDBC9E" + + "04B85A46104EFDF6692BF898C565480F5A5163231167DB69F873D1341CDF8DF0" + + "7BD4473F58162A3EB653C6567C2E3120B0034B9E35D22838BBF02968651FC1F8" + + "873C405237B7F29297ED47CADEABF248386CE94D215C8D5731A503623C9CA916" + + "C5896B8F713A044ABBECA764D36D074DF523D989228B89589463DD3615A7E844" + + "E7E9E91BE0BFAD79218CD7B52A313458BBA8FC660B5DF8513464FE749EF3C201" + + "5C15B2EF15F63D5FDA726ED8E04B3B5E7DB8610367AB08EDF120DDEEA7382AC5" + + "1C778B8069E09F2EB68D19154DD10129A1E0DA0717AE1E108B2496C69904D851" + + "18B37E06FA95E1F5E75371D8C7EC3BC80C817F931744428BF03C1F51A0164C0A" + + "3082057906092A864886F70D010701A082056A04820566308205623082055E06" + + "0B2A864886F70D010C0A0102A082053930820535305F06092A864886F70D0105" + + "0D3052303106092A864886F70D01050C30240410B5D9DF064271CEFFDBC3C067" + + "F6DFEB74020207D1300C06082A864886F70D02090500301D0609608648016503" + + "0401020410DEC794EDB555B89CAFB3671FAB4A52E1048204D050B2A2E9E3D3C0" + + "84D5BFCAB1C7C27951C0B72FB45B52089605DBF5E60550521DEAB0A66EF2C182" + + "3AE2F4EC21A1B8BF65D240DF20759C66AB0C6311CABB30F9DE1290B81CF69AAE" + + "7B6F42A4E9BE7BB6F09058C7D0D6FBE6F24EC2E3457680907F000C5B457B1E7A" + + "E394A7A70E6289C3B009F522399D5CC41014F12A336926F3AB8E2A3AF5496BC3" + + "27B073206F20F131AFCB627A1E67B9B457C44DB6A6C10EEBB8856E0BAAF99D8A" + + "9D24D7C90AC5EA9EB14C66315E77F158948988BB729D8E0796741CAE29894DC9" + + "6614C2B06911013C168C7A4E6C46F09D1AB0D7933729FC88BA47A41BFAB0AB53" + + "C60FEAF6A93688E67039184B598BDF1CC95C3967F9ECC649745E265974713102" + + "E271ECFA6A067F10751C2A4A70A94FA39F37E944996C843809D82990A9D44C18" + + "9EE7B9DC8534F9B821A5C56A61DFAAF470444883200B3FFFAA23DCE84375937D" + + "D0149869B2683773F650B70003EB025A04E170DFD97D4F86D2913E91F757BC01" + + "5F1F85F497B1400052E0868ED6844E2390F71B036B524E824B3ED6381BCE2600" + + "71A5F6EB12E9C1C44BBE0664217C4A44EA07E440A33010A0CA46647E7FFA4F58" + + "74F1EFB9DD330DA2CD50AC01C489639520A1A27B603F44831EF235F0540817F2" + + "196AF45D3B9957D272B5549BEC5507F6BA443A37F54FD6D6AEBEBBB7AAE15F7F" + + "8C1C46D68CEEC2E95426D177A390E7C50B0122BDDA9EFC104294840F6D5374AD" + + "90952842C0EDF796AF48C6FDD4AA02C81AFCD606CD0E94F3DF2B06B649A47D9D" + + "7C8AE23E1A776E1D149CA25E4929238778B19E33DE05DE577B763305B4884B8A" + + "7DE31BB9712477C9CDC138B52FB2D59CFE42A1CDAEB0E205C70B38CC4B88E0A6" + + "ABCC41D29D11947FF5B03633A5E164ED22F10536AAD07DB2EB52A2C696EBB135" + + "B5220839D1E5A7DB6C2B8DEB09C883129BD5253F68169F9D5F5A2F3AB17C4921" + + "10636978B8573A5B4E1FFF5A4C3E75E2F03AD71AF874C544474E1B41969416D6" + + "D8FFFD595428EC7928BD17C652F67D9B6B150C6D4A9352C405BED162492A5FD5" + + "7EB6BA5F77AAC2BD7E4EEB6B0BD2E4329E2CA8A425F88F4743B25F259E292C04" + + "483472CA79FF52271A830AA6A27A52C3531E2B2503592C017A7CC00F91F63F73" + + "4E3E56746475B8B338440B7D5FAC87A90831EE78A2DE4FD6F60F1C66B31A520C" + + "44B73B09D5419451C4A32E8E1A5BB17E44B9FAABFB07021747093DBFE248BACB" + + "E2BB6C7F145DE7397A2B2AEAA083EE57F46C8DF85FA2DFF4582C2E3CE3CB2E91" + + "706395A63BA96343B0567E41A33FC964BC8C03CEDE5E3E1D7A8B285F745EEAF5" + + "CD1382C86DA82922DA1772158F8BCCFF70DA87A64602033560566F33A3793A4E" + + "5C404D2C69274EEE9E82B5B97B0760FD66067888711C572E84DB382491792CB2" + + "C7BFA472E6B0D70D529701C2A0B730F5E1E0A980EAD56EA323E0008C70D62F53" + + "5B9E0533F9A4B7CFA22319274E68C8B4737E5DAB5B1956C235E7EA548E24E23F" + + "F9FCA61D11DAFC6B90E0BE8E96A66B6973D5F025C0619D283CC92C3224000FEE" + + "F9BD002E7EFAD4C737C4CFEB42858DBDFBC489B1131AABDD1868C58EDCAD35C2" + + "AE1A42BBCF0A2A90E0557A7A5F79F2D92D19E39D505994163CC94F5EA56009F0" + + "5E9ABCAF24807130F90FCA606D5448C103489BB53090EC603394705B472132E4" + + "FAD50491069F44040A0D66F7D3D5C86593D61C9D37CDE3BBE5651EA2E104B324" + + "3272E665CDBB139E063112301006092A864886F70D0109153103040101308196" + + "304F300B060960864801650304020304405EDC86442FD573401BD2DF0F95356A" + + "1C1454F401231B7F772179626ABCB220C8096AC0ED6C27CACED7D94615768B61" + + "8BDDCF4B8A0996E019BD418423F79F173404406B32D0D889B85234D716C87F3D" + + "EEBD62B5DC14984FDB9EA9FC765B340F54D3E5203C6A9F4F23913B22605A32BD" + + "3D8D120CFFFE4ACB83BA4D488C67271E38CD40020127").HexToByteArray(); + + // This is a PFX that mixes encrypted and unencrypted certificates. + // PFX { + // SC[0], plaintext { + // cert, keyid=2 + // }, + // SC[1], encrypted, PBES2: AES128CBC, SHA384, 2000 iterations { + // cert, keyid=1 + // }, + // SC[2], plaintext { + // shrouded_key, PBES2: AES128CBC, SHA256, 2001 iterations, keyid=1, + // shrouded_key, PBES2: AES128CBC, SHA256, 27 iterations, keyid=2 + // }, + // mac: SHA384, 39 iterations. + // } + // "Placeholder" + internal static readonly byte[] TwoCertsPfx_OneEncrypted = ( + "30820CAB02010330820C0B06092A864886F70D010701A0820BFC04820BF83082" + + "0BF43082029406092A864886F70D010701A0820285048202813082027D308202" + + "79060B2A864886F70D010C0A0103A082025430820250060A2A864886F70D0109" + + "1601A08202400482023C30820238308201A1A003020102020900BD698EB46606" + + "F1E4300D06092A864886F70D01010B0500305E311E301C060355040A13154D69" + + "63726F736F667420436F72706F726174696F6E31173015060355040B130E2E4E" + + "4554204C6962726172696573312330210603550403131A506C61696E74657874" + + "2054657374204365727469666963617465301E170D3234303531303138303033" + + "375A170D3234303531303138313033375A305E311E301C060355040A13154D69" + + "63726F736F667420436F72706F726174696F6E31173015060355040B130E2E4E" + + "4554204C6962726172696573312330210603550403131A506C61696E74657874" + + "205465737420436572746966696361746530819F300D06092A864886F70D0101" + + "01050003818D0030818902818100A6638EFEFA9A1B364574D9A7BA4A85017E73" + + "61831606B717DCF8FC0F5B982583D5460BCD216E99FBC15ABF62B5C30FDC7CF7" + + "D13CDCF9E0A0A25C26AF0AC14D116569FAA6496CB87ECB0A3B87AAF624010630" + + "4E7F0DDB63FF7EC95396F2CF4E57A1F0356414C7D1032433831C327AC33DD264" + + "FC7B1BA567FBE360AF35446B63110203010001300D06092A864886F70D01010B" + + "0500038181001E2C2AC3FC484C42FBFC3A0D027E438BDBF6FDD4609BF1E11BA9" + + "CDCF50D121BAEDFA5361619B469D48F1CBA203F361A2E7D662A88239A500D056" + + "CB38D215197A3F5FFACA5AADDAC875D380C4435C0E8633243174623BF59836F0" + + "8DBA917C5B4A2270579574566FFA29A7A5FF0415D34E8CCFFB9EAD4BA8EC90BF" + + "B3A2F7041F6B3112301006092A864886F70D01091531030401023082031A0609" + + "2A864886F70D010706A082030B308203070201003082030006092A864886F70D" + + "010701305F06092A864886F70D01050D3052303106092A864886F70D01050C30" + + "24041078790CAC0DA9B3E9B0EE236CA8073CF3020207D0300C06082A864886F7" + + "0D020A0500301D06096086480165030401160410256DE9ED9807FD7D30597DB5" + + "51A5815080820290D8156A7DDCD344FE96B8D8BBA4303A373108D725BFC30071" + + "DBF716A7C1FD1B598BAE1FBC9A77CCBB7319646E83B747B59330760B6EBC29B0" + + "91E591FF0030ECB28626BA1594FEF6F0A01B60F2548AE1577FD05E7CAC5BCFA6" + + "2422E71F551FC2A8ACE488E871C03E1E02A9DB4ADA9DB335466EE1A6E7AE6B9C" + + "AE118AF4008879690C446F0D4A03740E31E4879B163E58FFA46394DF57CD98AE" + + "FA3C44E94BDD36D31A53EB2C9A74EA9F45718370106F205A81837A05952E7C17" + + "3460A2400195D658671BE62E76AAD565D848109BE41B6F06CA87A7B7676B9A5A" + + "525B6ADDE80A68A87FE82C8547D611F6DEC5C2AD617D354216CC7E724DA2F7A7" + + "BC67C336A68F7A8ED4F555E53330DDA1318332E170458D10E7A1CA60D87F61C5" + + "89B8BC4CDCAC26BA341B96BD20096B89977750B1E2BACDEE23130825B827F5CA" + + "E73373AD2258201B4DED998F003E1084BE969F5B1EE33B8443BE01C509927876" + + "1D69A1E48662EE91DF5211176DA5495AD54CB50EB2A2A3E077E93946958CAFE9" + + "5F2FB3450B6269F3541BB21B6F129288115E177594D6EC0DD516B8882100C9CA" + + "DB9874064B3A1F051FBB0B43257F0644A05C6D416C0652696E89DB6E43CE5E92" + + "94D05711CDBF7B304E076D73FEDA97A7E2DFF398761DE6425730CB576D26F49A" + + "2CCD93BDD0E410F70D4EF8DB6BCF221BD3E53C472CEA9C40E09A284923992BD2" + + "12B32E36BBB06EC3000261E8EC9D9E389F663DBAAFEBE0CF671E771A246D033E" + + "AFFDF400B94D8926E23FD660E0A8304C01D490EF54F868506EE8AA255F95E00A" + + "7E1AA1D18998B9AE8E0AA0472CD152A674536F5D2882502247D2B056137BFF97" + + "08CD8EC61A3339CCB1DF43DC60A0A75F9260905BB3D339580EA5B5B7BAA92374" + + "4A8850389A2E68B98FE6EF78D78207ACC858A97DEC55C0D03082063A06092A86" + + "4886F70D010701A082062B04820627308206233082030E060B2A864886F70D01" + + "0C0A0102A08202E9308202E5305F06092A864886F70D01050D3052303106092A" + + "864886F70D01050C3024041077CF0CA72DCB1B541E6F481D3A51470A020207D1" + + "300C06082A864886F70D02090500301D06096086480165030401020410503141" + + "7A0A42803706E84B8C7470CD2D048202807E3108B51EA4D80D2E7CF358E08C6E" + + "A30087AE0E9F80CF1B022A0DE0BBDA715733141C6877276864DE98D439D61650" + + "63ED517FBF206450AE6B29835F89914003B8D77F0F15D6FB073EF27F90E9D851" + + "ABE569D3DE6968E8F61EA184B484B4284C2FD3803D9AA4DE083F18A64B2F9320" + + "156004E49C4A179CF8800799451563200CD998CBA779032302CEA4227A78CD10" + + "8FC8BFA6C114F005A2937948F14DDDF05AEB543F4D2FB3C56FA7C1F14B7ACA57" + + "0CA8554A6EA4AC1C0FD356E3D1DD568F97D6EDBFD3B09C87BC2013A88C442993" + + "4C73F442A5D5DA3563FCB85EBEC41085843D14EE88E582151E6855741106CA1D" + + "D775A5793E8A0B4E036427E6D2FC9F0AA394085997E42875E4EF5742099141CD" + + "70B4AA887C40E3EDDB407C39F3C474F95D394D98DAA68BD1005F1223B6E58B52" + + "AA552FD25652AA871F03A53A58922953DB120BAFB44C29DCF9C9C2F96DEE3BEE" + + "7E0F7FF8F04C98ED631F6A845B8B89F30EFB956846BA87E9B3F6FCCB49FE0F21" + + "10139E855389C8EC5120983A09D5F4D655E64058ADD1E8C44F3B6A4936E6788B" + + "E939061221424D71666765D99FE6D883C9E8079992D585F73B0CD57947D38A54" + + "FE7CBE93BE117675A3E52F708B2A6BA6C390BD3BE01100243E7B7B5CB8A9F501" + + "CCCCD082F9867B4DA0A9FF3DC5BB0F0974D4EDEF9FF89F6EB355BCE07B00A32D" + + "AEE443D46EF19A96E6EB3EA4C0B141606A1FDB200D1BADBC11954951383E8935" + + "4603C10E241353FED1502D8111E42DA45198D1586CB0938947F3856CCC855F84" + + "231F131D636DFBB46394A3EBEDAC9A4DAD0DCB32163C57A9B5DD21BC56CA969A" + + "F574E38787DFA6EC2BA063E60375585ED5B9E8D5B6D1BC162D63036C52FF4473" + + "E0A4FD96AA055ED4014C3B0E4F7A098F553112301006092A864886F70D010915" + + "31030401013082030D060B2A864886F70D010C0A0102A08202E8308202E4305E" + + "06092A864886F70D01050D3051303006092A864886F70D01050C30230410DE7A" + + "16E6BCE889C8A5F823E7670FB9E302011B300C06082A864886F70D0209050030" + + "1D06096086480165030401020410683B6EE3E1287242ED73707ECE1F271F0482" + + "028018435588B8BEA17EB47E7947D4FE82D66F7C26850B840C0F06964DC99C56" + + "8391707178494F177EFB65E17D20D3C39E80A00342783B33164F371902D76658" + + "78A79802FE7D9EB31D21137B56692D1B7E2B38F54B00CEE506867968420CB4FA" + + "9EE135BA960C0D40EC22714F5D8576BAF03269D19384C246A60ED4B9F0F4268E" + + "2E135F7BE41536D844718B7339EE14A1CE261B8A27C2AA2AAA90C98F9437372B" + + "0448579A0C392B347C7E95A1A7617A47218F63D0C52C88A3F93AC47BB9493E2C" + + "67DE061F28E90FED463E392C6743C8E6F60524A2455C5E46218E357AFB31553D" + + "0AAD18A2784719D281B12560CB904527D912BA2B40790D9068661C01AA4B03E0" + + "B5316D6FEBBC38087B4DD46CF2CC0C98B6F488AA9940C6FA16BC8BB1853CD0B0" + + "E41D85F382F5BE36E5A821C54EBB8C34DD7CD7B970315FF65B657AED6CE2598A" + + "6DF4E96370815660B3711FDE0A2CA0AB5C2B234C95B699922CFC4E95B781F783" + + "F20373E8E23EB6D518C797A21B401A797DDAF8A823602ED38135F3FF3C555C7E" + + "7FA7B800F4535B781B4F6B21BF2147D8C20388CDD48DB3C17413DC4FBD347368" + + "809762C4851C28CAD52B4F916CAF53E2D5E05ACAC3E85BA1F63525F57F456059" + + "56B7830AFBC50D0CE517C282AE5DB5638635155073C567A82A7ACDEE6860D18A" + + "1CAACE341845A41470F47E7A440F21A3DA96CF986181E1F044B28FB3EEFA0E41" + + "82B93AA2FD50A23B045DABCD774519879CB824B9A58A1859ED553578508B5CF7" + + "85BE3D2A67F0FC508B94A738162455CD11A4BF0C3736914D8C5A99F9D5ACA959" + + "B1C4D5872BA852DD3F8B2B8C231017AEF373B3B5E7B65B6DF7115DB99D5EB11F" + + "E80E8CB3E1D3CE6F50DD7B255F103DC290B7287D6AD7AB12D5242E13092DB9E1" + + "ADE03112301006092A864886F70D0109153103040102308196304F300B060960" + + "8648016503040203044044A762FCCD979F096F1C0DE0C4C584E42C1848DB1138" + + "389DAC33048D6B81DBBCA7E031E8620B37C14F85257DA4A6F57FE58E939493C5" + + "928343622B80009D8C5B0440E74D33A5F7DA48801A4EF4FD65D0F442F26C845F" + + "A418D3E0D78FD0285A92A74B433661F516C6955EC40FE46DAB813B6AE940C2DE" + + "FE8F8F5E32E6B491C999D598020127").HexToByteArray(); } } diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12CollectionTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12CollectionTests.cs new file mode 100644 index 0000000000000..84d57f6e7e74e --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12CollectionTests.cs @@ -0,0 +1,763 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.IO; +using System.IO.MemoryMappedFiles; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderPkcs12CollectionTests_FromByteArray : X509CertificateLoaderPkcs12CollectionTests + { + protected override void NullInputAssert(Action action) => + AssertExtensions.Throws("data", action); + + protected override void EmptyInputAssert(Action action) => + Assert.Throws(action); + + protected override X509Certificate2Collection LoadPfxCore( + byte[] bytes, + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12Collection(bytes, password, keyStorageFlags, loaderLimits); + } + + protected override X509Certificate2Collection LoadPfxFileOnlyCore( + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12Collection( + File.ReadAllBytes(path), + password, + keyStorageFlags, + loaderLimits); + } + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + if (bytes is null) + { + contentType = X509ContentType.Unknown; + return false; + } + + contentType = X509Certificate2.GetCertContentType(bytes); + return true; + } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderPkcs12CollectionTests_FromByteSpan : X509CertificateLoaderPkcs12CollectionTests + { + protected override void NullInputAssert(Action action) => + Assert.ThrowsAny(action); + + protected override void EmptyInputAssert(Action action) => + Assert.ThrowsAny(action); + + protected override X509Certificate2Collection LoadPfxCore( + byte[] bytes, + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12Collection( + new ReadOnlySpan(bytes), + password.AsSpan(), + keyStorageFlags, + loaderLimits); + } + + protected override X509Certificate2Collection LoadPfxAtOffsetCore( + byte[] bytes, + int offset, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12Collection( + bytes.AsSpan(offset), + password.AsSpan(), + keyStorageFlags, + loaderLimits); + } + + protected override X509Certificate2Collection LoadPfxFileOnlyCore( + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + // Use a strategy other than File.ReadAllBytes. + + using (FileStream stream = File.OpenRead(path)) + using (MemoryManager manager = MemoryMappedFileMemoryManager.CreateFromFileClamped(stream)) + { + return X509CertificateLoader.LoadPkcs12Collection( + manager.Memory.Span, + password.AsSpan(), + keyStorageFlags, + loaderLimits); + } + } + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + contentType = X509ContentType.Unknown; + return false; + } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderPkcs12CollectionTests_FromFile : X509CertificateLoaderPkcs12CollectionTests + { + protected override void NullInputAssert(Action action) => + AssertExtensions.Throws("path", action); + + protected override void EmptyInputAssert(Action action) => + AssertExtensions.Throws("path", action); + + protected override X509Certificate2Collection LoadPfxCore( + byte[] bytes, + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12CollectionFromFile(path, password, keyStorageFlags, loaderLimits); + } + + protected override X509Certificate2Collection LoadPfxFileOnlyCore( + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12CollectionFromFile(path, password, keyStorageFlags, loaderLimits); + } + + protected override X509Certificate2Collection LoadPfxNoFileCore( + byte[] bytes, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + using (TempFileHolder holder = new TempFileHolder(bytes)) + { + return LoadPfx(bytes, holder.FilePath, password, keyStorageFlags, loaderLimits); + } + } + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + if (path is null) + { + contentType = X509ContentType.Unknown; + return false; + } + + contentType = X509Certificate2.GetCertContentType(path); + return true; + } + } + + public abstract partial class X509CertificateLoaderPkcs12CollectionTests + { + private const int ERROR_INVALID_PASSWORD = -2147024810; + + protected static readonly X509KeyStorageFlags EphemeralIfPossible = +#if NETFRAMEWORK + X509KeyStorageFlags.DefaultKeySet; +#else + PlatformDetection.UsesAppleCrypto ? + X509KeyStorageFlags.DefaultKeySet : + X509KeyStorageFlags.EphemeralKeySet; +#endif + + protected abstract void NullInputAssert(Action action); + protected abstract void EmptyInputAssert(Action action); + + protected X509Certificate2Collection LoadPfx( + byte[] bytes, + string path, + string password = "", + X509KeyStorageFlags? keyStorageFlags = null, + Pkcs12LoaderLimits loaderLimits = null) + { + return LoadPfxCore( + bytes, + path, + password, + keyStorageFlags.GetValueOrDefault(EphemeralIfPossible), + loaderLimits); + } + + protected abstract X509Certificate2Collection LoadPfxCore( + byte[] bytes, + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits); + + protected X509Certificate2Collection LoadPfxFileOnly( + string path, + string password = "", + X509KeyStorageFlags? keyStorageFlags = null, + Pkcs12LoaderLimits loaderLimits = null) + { + return LoadPfxFileOnlyCore( + path, + password, + keyStorageFlags.GetValueOrDefault(EphemeralIfPossible), + loaderLimits); + } + + protected abstract X509Certificate2Collection LoadPfxFileOnlyCore( + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits); + + protected X509Certificate2Collection LoadPfxNoFile( + byte[] bytes, + string password = "", + X509KeyStorageFlags? keyStorageFlags = null, + Pkcs12LoaderLimits loaderLimits = null) + { + return LoadPfxNoFileCore( + bytes, + password, + keyStorageFlags.GetValueOrDefault(EphemeralIfPossible), + loaderLimits); + } + + protected virtual X509Certificate2Collection LoadPfxNoFileCore( + byte[] bytes, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return LoadPfx(bytes, null, password, keyStorageFlags, loaderLimits); + } + + protected X509Certificate2Collection LoadPfxAtOffset( + byte[] bytes, + int offset, + string password = "", + X509KeyStorageFlags? keyStorageFlags = null, + Pkcs12LoaderLimits loaderLimits = null) + { + return LoadPfxAtOffsetCore( + bytes, + offset, + password, + keyStorageFlags.GetValueOrDefault(EphemeralIfPossible), + loaderLimits); + } + + protected virtual X509Certificate2Collection LoadPfxAtOffsetCore( + byte[] bytes, + int offset, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return LoadPfxNoFile( + bytes.AsSpan(offset).ToArray(), + password, + keyStorageFlags, + loaderLimits); + } + + protected abstract bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType); + + [Fact] + public void LoadNull() + { + NullInputAssert(() => LoadPfx(null, null, null)); + } + + [Fact] + public void LoadEmpty() + { + EmptyInputAssert(() => LoadPfx(Array.Empty(), string.Empty)); + } + + private void LoadKnownFormat_Fails(byte[] data, string path, X509ContentType contentType) + { + if (PlatformDetection.IsWindows || !X509CertificateLoaderTests.IsWindowsOnlyContentType(contentType)) + { + if (TryGetContentType(data, path, out X509ContentType actualType)) + { + Assert.Equal(contentType, actualType); + } + } + + if (path is null) + { + Assert.ThrowsAny(() => LoadPfxNoFile(data)); + } + else if (data is null) + { + Assert.ThrowsAny(() => LoadPfxFileOnly(path)); + } + else + { + Assert.ThrowsAny(() => LoadPfx(data, path)); + } + } + + [Fact] + public void LoadCertificate_DER_Fails() + { + LoadKnownFormat_Fails(TestData.MsCertificate, TestFiles.MsCertificateDerFile, X509ContentType.Cert); + } + + [Fact] + public void LoadCertificate_PEM_Fails() + { + LoadKnownFormat_Fails(TestData.MsCertificatePemBytes, TestFiles.MsCertificatePemFile, X509ContentType.Cert); + } + + [Fact] + public void LoadPkcs7_BER_Fails() + { + LoadKnownFormat_Fails(TestData.Pkcs7ChainDerBytes, TestFiles.Pkcs7ChainDerFile, X509ContentType.Pkcs7); + } + + [Fact] + public void LoadPkcs7_PEM_Fails() + { + LoadKnownFormat_Fails(TestData.Pkcs7ChainPemBytes, TestFiles.Pkcs7ChainPemFile, X509ContentType.Pkcs7); + } + + [Fact] + public void LoadSerializedCert_Fails() + { + LoadKnownFormat_Fails(TestData.StoreSavedAsSerializedCerData, null, X509ContentType.SerializedCert); + } + + [Fact] + public void LoadSerializedStore_Fails() + { + LoadKnownFormat_Fails(TestData.StoreSavedAsSerializedStoreData, null, X509ContentType.SerializedStore); + } + + [Fact] + public void LoadSignedFile_Fails() + { + LoadKnownFormat_Fails(null, TestFiles.SignedMsuFile, X509ContentType.Authenticode); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_Single_WithPassword(bool ignorePrivateKeys) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnorePrivateKeys = ignorePrivateKeys, + }; + + X509Certificate2Collection coll = LoadPfx( + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + EphemeralIfPossible, + loaderLimits); + + using (new CollectionDisposer(coll)) + { + Assert.Equal(1, coll.Count); + + X509Certificate2 cert = coll[0]; + Assert.Equal("CN=MyName", cert.Subject); + Assert.NotEqual(ignorePrivateKeys, cert.HasPrivateKey); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void LoadPfx_Single_NoPassword(bool ignorePrivateKeys, bool useNull) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnorePrivateKeys = ignorePrivateKeys, + }; + + string password = useNull ? null : ""; + + X509Certificate2Collection coll = LoadPfxNoFile( + TestData.PfxWithNoPassword, + password, + EphemeralIfPossible, + loaderLimits); + + using (new CollectionDisposer(coll)) + { + Assert.Equal(1, coll.Count); + + X509Certificate2 cert = coll[0]; + Assert.Equal("CN=MyName", cert.Subject); + Assert.NotEqual(ignorePrivateKeys, cert.HasPrivateKey); + } + } + + [ConditionalTheory(typeof(PlatformSupport), nameof(PlatformSupport.IsRC2Supported))] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void LoadPfx_Single_NoPassword_AmbiguousDecrypt(bool ignorePrivateKeys, bool useNull) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnorePrivateKeys = ignorePrivateKeys, + }; + + string password = useNull ? null : ""; + + X509Certificate2Collection coll = LoadPfxNoFile( + TestData.MsCertificateExportedToPfx_NullPassword, + password, + EphemeralIfPossible, + loaderLimits); + + using (new CollectionDisposer(coll)) + { + Assert.Equal(1, coll.Count); + + X509Certificate2 cert = coll[0]; + X509CertificateLoaderTests.AssertRawDataEquals(TestData.MsCertificate, cert); + Assert.False(cert.HasPrivateKey, "cert.HasPrivateKey"); + } + } + + [Fact] + public void LoadPfx_Single_WrongPassword() + { + CryptographicException ex = Assert.Throws( + () => LoadPfx(TestData.PfxData, TestFiles.PfxFile, "asdf")); + + Assert.Contains("password", ex.Message); + Assert.Equal(ERROR_INVALID_PASSWORD, ex.HResult); + } + + [Fact] + public void LoadPfx_Single_EmptyPassword_WithWrongPassword() + { + CryptographicException ex = Assert.Throws( + () => LoadPfxNoFile(TestData.PfxWithNoPassword, "asdf")); + + Assert.Contains("password", ex.Message); + Assert.Equal(ERROR_INVALID_PASSWORD, ex.HResult); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_Single_EmptyPassword_NoMac(bool useEmpty) + { + string password = useEmpty ? "" : null; + + X509Certificate2Collection coll = LoadPfxNoFile( + TestData.Pkcs12OpenSslOneCertDefaultNoMac, + password, + EphemeralIfPossible); + + using (new CollectionDisposer(coll)) + { + Assert.Equal(1, coll.Count); + + X509Certificate2 cert = coll[0]; + Assert.Equal("CN=test", cert.Subject); + } + } + + [Fact] + public void LoadPfx_WithTrailingData() + { + byte[] data = TestData.PfxWithNoPassword; + Array.Resize(ref data, data.Length + 10); + + X509Certificate2Collection coll = LoadPfxNoFile(data); + + using (new CollectionDisposer(coll)) + { + Assert.Equal(1, coll.Count); + + X509Certificate2 cert = coll[0]; + Assert.Equal("CN=MyName", cert.Subject); + } + } + + [Fact] + public void LoadPfx_Empty() + { + X509Certificate2Collection coll = LoadPfxNoFile(TestData.EmptyPfx); + + using (new CollectionDisposer(coll)) + { + Assert.Equal(0, coll.Count); + } + } + + private void LoadPfx_VerifyLimit( + string propertyTested, + bool fail, + byte[] bytes, + string path, + string password, + Pkcs12LoaderLimits loaderLimits) + { + Func test; + + if (bytes is null) + { + test = () => LoadPfxFileOnly(path, password, EphemeralIfPossible, loaderLimits); + } + else if (path is null) + { + test = () => LoadPfxNoFile(bytes, password, EphemeralIfPossible, loaderLimits); + } + else + { + test = () => LoadPfx(bytes, path, password, EphemeralIfPossible, loaderLimits); + } + + if (fail) + { + Pkcs12LoadLimitExceededException ex = + AssertExtensions.Throws(() => test()); + + Assert.Contains(propertyTested, ex.Message); + } + else + { + // Assert.NoThrow + (new CollectionDisposer(test())).Dispose(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_VerifyMacIterationLimit(bool failLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + MacIterationLimit = failLimit ? 1999 : 2000, + }; + + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.MacIterationLimit), + failLimit, + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + loaderLimits); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_VerifyKdfIterationLimit(bool failLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IndividualKdfIterationLimit = failLimit ? 1999 : 2000, + }; + + // Both 1999 and 2000 will fail, because the key uses 2001. + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.IndividualKdfIterationLimit), + fail: true, + TestData.MixedIterationsPfx, + null, + TestData.PlaceholderPw, + loaderLimits); + + loaderLimits.IgnorePrivateKeys = true; + + // Now that we're ignoring the key, 1999 will fail, 2000 will pass. + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.IndividualKdfIterationLimit), + failLimit, + TestData.MixedIterationsPfx, + null, + TestData.PlaceholderPw, + loaderLimits); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_VerifyTotalKdfIterationLimit(bool failLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + TotalKdfIterationLimit = failLimit ? 3999 : 4000, + }; + + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.TotalKdfIterationLimit), + failLimit, + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + loaderLimits); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + public void LoadPfx_VerifyCertificateLimit(int? certLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + MaxCertificates = certLimit, + }; + + bool expectFailure = certLimit.GetValueOrDefault(int.MaxValue) < 3; + + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.MaxCertificates), + expectFailure, + TestData.ChainPfxBytes, + TestFiles.ChainPfxFile, + TestData.ChainPfxPassword, + loaderLimits); + } + + [Theory] + [InlineData(null)] + [InlineData(0)] + [InlineData(1)] + [InlineData(4)] + public void LoadPfx_VerifyKeysLimit(int? keysLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + MaxKeys = keysLimit, + }; + + bool expectFailure = keysLimit.GetValueOrDefault(int.MaxValue) < 1; + + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.MaxKeys), + expectFailure, + TestData.ChainPfxBytes, + TestFiles.ChainPfxFile, + TestData.ChainPfxPassword, + loaderLimits); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void LoadPfx_VerifyIgnoreEncryptedSafes(bool ignoreEncrypted) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnoreEncryptedAuthSafes = ignoreEncrypted, + }; + + const string PlaintextSubject = + "CN=Plaintext Test Certificate, OU=.NET Libraries, O=Microsoft Corporation"; + const string EncryptedSubject = + "CN=Encrypted Test Certificate, OU=.NET Libraries, O=Microsoft Corporation"; + + X509Certificate2Collection coll = LoadPfxNoFile( + TestData.TwoCertsPfx_OneEncrypted, + TestData.PlaceholderPw, + default, + loaderLimits); + + using (new CollectionDisposer(coll)) + { + if (ignoreEncrypted) + { + Assert.Equal(1, coll.Count); + + X509Certificate2 cert = coll[0]; + Assert.Equal(PlaintextSubject, cert.Subject); + } + else + { + Assert.Equal(2, coll.Count); + + X509Certificate2 cert = coll[0]; + Assert.Equal(EncryptedSubject, cert.Subject); + + cert = coll[1]; + Assert.Equal(PlaintextSubject, cert.Subject); + } + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void LoadPfx_VerifyIgnoreEncryptedSafes_EmptyIfIgnored(bool ignoreEncrypted) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnoreEncryptedAuthSafes = ignoreEncrypted, + }; + + X509Certificate2Collection coll = LoadPfx( + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + default, + loaderLimits); + + using (new CollectionDisposer(coll)) + { + if (ignoreEncrypted) + { + Assert.Equal(0, coll.Count); + } + else + { + Assert.Equal(1, coll.Count); + + X509Certificate2 cert = coll[0]; + Assert.Equal("CN=MyName", cert.Subject); + } + } + } + + private sealed class CollectionDisposer : IDisposable + { + private readonly X509Certificate2Collection _coll; + + internal CollectionDisposer(X509Certificate2Collection coll) + { + _coll = coll; + } + + public void Dispose() + { + foreach (X509Certificate2 cert in _coll) + { + cert.Dispose(); + } + } + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12Tests.WindowsAttributes.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12Tests.WindowsAttributes.cs new file mode 100644 index 0000000000000..444094c15b0ca --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12Tests.WindowsAttributes.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public abstract partial class X509CertificateLoaderPkcs12Tests + { + [Theory] + [InlineData(true)] + [InlineData(false)] + public void VerifyPreserveKeyName(bool preserveName) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + PreserveKeyName = preserveName, + }; + + string keyName = Guid.NewGuid().ToString("D"); + byte[] pfx = MakeAttributeTest(keyName: keyName, friendlyName: "Non-preserved"); + + X509Certificate2 cert = LoadPfxNoFile( + pfx, + keyStorageFlags: X509KeyStorageFlags.DefaultKeySet, + loaderLimits: loaderLimits); + + using (cert) + { + using (RSA key = cert.GetRSAPrivateKey()) + { + CngKey cngKey = Assert.IsType(key).Key; + + if (preserveName) + { + Assert.Equal(keyName, cngKey.KeyName); + } + else + { + Assert.NotEqual(keyName, cngKey.KeyName); + } + } + + // Alias was not preserved + Assert.Empty(cert.FriendlyName); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void VerifyPreserveAlias(bool preserveAlias) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + PreserveCertificateAlias = preserveAlias, + }; + + string keyName = Guid.NewGuid().ToString("D"); + string alias = Guid.NewGuid().ToString("D"); + byte[] pfx = MakeAttributeTest(keyName: keyName, friendlyName: alias); + + X509Certificate2 cert = LoadPfxNoFile( + pfx, + keyStorageFlags: X509KeyStorageFlags.DefaultKeySet, + loaderLimits: loaderLimits); + + using (cert) + { + if (preserveAlias) + { + Assert.Equal(alias, cert.FriendlyName); + } + else + { + Assert.Empty(cert.FriendlyName); + } + + using (RSA key = cert.GetRSAPrivateKey()) + { + CngKey cngKey = Assert.IsType(key).Key; + + // Key name was not preserved + Assert.NotEqual(keyName, cngKey.KeyName); + } + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void VerifyPreservePreserveProvider(bool preserveProvider, bool preserveName) + { + // This test forces a key creation with CAPI, and verifies that + // PreserveStorageProvider keeps the key in CAPI. Additionally, + // it shows that PreserveKeyName and PreserveStorageProvider are independent. + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + PreserveKeyName = preserveName, + PreserveStorageProvider = preserveProvider, + }; + + string keyName = Guid.NewGuid().ToString("D"); + string alias = Guid.NewGuid().ToString("D"); + byte[] pfx = MakeAttributeTest(keyName: keyName, friendlyName: alias, useCapi: true); + + X509Certificate2 cert = LoadPfxNoFile( + pfx, + keyStorageFlags: X509KeyStorageFlags.DefaultKeySet, + loaderLimits: loaderLimits); + + using (cert) + { + using (RSA key = cert.GetRSAPrivateKey()) + { + CngKey cngKey = Assert.IsType(key).Key; + + if (preserveName) + { + Assert.Equal(keyName, cngKey.KeyName); + } + else + { + Assert.NotEqual(keyName, cngKey.KeyName); + } + + const string CapiProvider = "Microsoft Enhanced RSA and AES Cryptographic Provider"; + + if (preserveProvider) + { + Assert.Equal(CapiProvider, cngKey.Provider.Provider); + } + else + { + Assert.NotEqual(CapiProvider, cngKey.Provider.Provider); + } + } + + // Alias is not preserved + Assert.Empty(cert.FriendlyName); + } + } + + private static byte[] MakeAttributeTest( + string? keyName = null, + string? friendlyName = null, + bool useCapi = false, + [CallerMemberName] string testName = null) + { + CngKey cngKey = null; + RSACryptoServiceProvider rsaCsp = null; + + try + { + RSA key; + + if (keyName is not null) + { + if (useCapi) + { + CspParameters cspParameters = new CspParameters(24) + { + KeyContainerName = keyName, + }; + + rsaCsp = new RSACryptoServiceProvider(2048, cspParameters); + key = rsaCsp; + } + else + { + CngKeyCreationParameters cngParams = new CngKeyCreationParameters + { + ExportPolicy = CngExportPolicies.AllowPlaintextExport, + }; + + cngKey = CngKey.Create(CngAlgorithm.Rsa, keyName, cngParams); + key = new RSACng(cngKey); + } + } + else + { + key = RSA.Create(2048); + } + + CertificateRequest req = new CertificateRequest( + $"CN={testName}-{keyName}-{friendlyName}", + key, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + DateTimeOffset now = DateTimeOffset.UtcNow; + + using (X509Certificate2 cert = req.CreateSelfSigned(now.AddMinutes(-5), now.AddMinutes(5))) + { + if (friendlyName is not null) + { + cert.FriendlyName = friendlyName; + } + + return cert.Export(X509ContentType.Pfx); + } + } + finally + { + cngKey?.Delete(); + + if (rsaCsp is not null) + { + rsaCsp.PersistKeyInCsp = false; + rsaCsp.Dispose(); + } + } + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12Tests.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12Tests.cs new file mode 100644 index 0000000000000..6568be9756eb2 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderPkcs12Tests.cs @@ -0,0 +1,739 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.IO; +using System.IO.MemoryMappedFiles; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderPkcs12Tests_FromByteArray : X509CertificateLoaderPkcs12Tests + { + protected override void NullInputAssert(Action action) => + AssertExtensions.Throws("data", action); + + protected override void EmptyInputAssert(Action action) => + Assert.Throws(action); + + protected override X509Certificate2 LoadPfxCore( + byte[] bytes, + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12(bytes, password, keyStorageFlags, loaderLimits); + } + + protected override X509Certificate2 LoadPfxFileOnlyCore( + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12( + File.ReadAllBytes(path), + password, + keyStorageFlags, + loaderLimits); + } + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + if (bytes is null) + { + contentType = X509ContentType.Unknown; + return false; + } + + contentType = X509Certificate2.GetCertContentType(bytes); + return true; + } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderPkcs12Tests_FromByteSpan : X509CertificateLoaderPkcs12Tests + { + protected override void NullInputAssert(Action action) => + Assert.ThrowsAny(action); + + protected override void EmptyInputAssert(Action action) => + Assert.ThrowsAny(action); + + protected override X509Certificate2 LoadPfxCore( + byte[] bytes, + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12( + new ReadOnlySpan(bytes), + password.AsSpan(), + keyStorageFlags, + loaderLimits); + } + + protected override X509Certificate2 LoadPfxAtOffsetCore( + byte[] bytes, + int offset, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12( + bytes.AsSpan(offset), + password.AsSpan(), + keyStorageFlags, + loaderLimits); + } + + protected override X509Certificate2 LoadPfxFileOnlyCore( + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + // Use a strategy other than File.ReadAllBytes. + + using (FileStream stream = File.OpenRead(path)) + using (MemoryManager manager = MemoryMappedFileMemoryManager.CreateFromFileClamped(stream)) + { + return X509CertificateLoader.LoadPkcs12( + manager.Memory.Span, + password.AsSpan(), + keyStorageFlags, + loaderLimits); + } + } + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + contentType = X509ContentType.Unknown; + return false; + } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderPkcs12Tests_FromFile : X509CertificateLoaderPkcs12Tests + { + protected override void NullInputAssert(Action action) => + AssertExtensions.Throws("path", action); + + protected override void EmptyInputAssert(Action action) => + AssertExtensions.Throws("path", action); + + protected override X509Certificate2 LoadPfxCore( + byte[] bytes, + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadPkcs12FromFile(path, password, keyStorageFlags, loaderLimits); + } + + protected override X509Certificate2 LoadPfxFileOnlyCore( + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return X509CertificateLoader.LoadCertificateFromFile(path); + } + + protected override X509Certificate2 LoadPfxNoFileCore( + byte[] bytes, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + using (TempFileHolder holder = new TempFileHolder(bytes)) + { + return LoadPfx(bytes, holder.FilePath, password, keyStorageFlags, loaderLimits); + } + } + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + if (path is null) + { + contentType = X509ContentType.Unknown; + return false; + } + + contentType = X509Certificate2.GetCertContentType(path); + return true; + } + } + + public abstract partial class X509CertificateLoaderPkcs12Tests + { + private const int ERROR_INVALID_PASSWORD = -2147024810; + + protected static readonly X509KeyStorageFlags EphemeralIfPossible = +#if NETFRAMEWORK + X509KeyStorageFlags.DefaultKeySet; +#else + PlatformDetection.UsesAppleCrypto ? + X509KeyStorageFlags.DefaultKeySet : + X509KeyStorageFlags.EphemeralKeySet; +#endif + + protected abstract void NullInputAssert(Action action); + protected abstract void EmptyInputAssert(Action action); + + protected X509Certificate2 LoadPfx( + byte[] bytes, + string path, + string password = "", + X509KeyStorageFlags? keyStorageFlags = null, + Pkcs12LoaderLimits loaderLimits = null) + { + return LoadPfxCore( + bytes, + path, + password, + keyStorageFlags.GetValueOrDefault(EphemeralIfPossible), + loaderLimits); + } + + protected abstract X509Certificate2 LoadPfxCore( + byte[] bytes, + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits); + + protected X509Certificate2 LoadPfxFileOnly( + string path, + string password = "", + X509KeyStorageFlags? keyStorageFlags = null, + Pkcs12LoaderLimits loaderLimits = null) + { + return LoadPfxFileOnlyCore( + path, + password, + keyStorageFlags.GetValueOrDefault(EphemeralIfPossible), + loaderLimits); + } + + protected abstract X509Certificate2 LoadPfxFileOnlyCore( + string path, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits); + + protected X509Certificate2 LoadPfxNoFile( + byte[] bytes, + string password = "", + X509KeyStorageFlags? keyStorageFlags = null, + Pkcs12LoaderLimits loaderLimits = null) + { + return LoadPfxNoFileCore( + bytes, + password, + keyStorageFlags.GetValueOrDefault(EphemeralIfPossible), + loaderLimits); + } + + protected virtual X509Certificate2 LoadPfxNoFileCore( + byte[] bytes, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return LoadPfx(bytes, null, password, keyStorageFlags, loaderLimits); + } + + protected X509Certificate2 LoadPfxAtOffset( + byte[] bytes, + int offset, + string password = "", + X509KeyStorageFlags? keyStorageFlags = null, + Pkcs12LoaderLimits loaderLimits = null) + { + return LoadPfxAtOffsetCore( + bytes, + offset, + password, + keyStorageFlags.GetValueOrDefault(EphemeralIfPossible), + loaderLimits); + } + + protected virtual X509Certificate2 LoadPfxAtOffsetCore( + byte[] bytes, + int offset, + string password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + return LoadPfxNoFile( + bytes.AsSpan(offset).ToArray(), + password, + keyStorageFlags, + loaderLimits); + } + + protected abstract bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType); + + [Fact] + public void LoadNull() + { + NullInputAssert(() => LoadPfx(null, null, null)); + } + + [Fact] + public void LoadEmpty() + { + EmptyInputAssert(() => LoadPfx(Array.Empty(), string.Empty)); + } + + private void LoadKnownFormat_Fails(byte[] data, string path, X509ContentType contentType) + { + if (PlatformDetection.IsWindows || !X509CertificateLoaderTests.IsWindowsOnlyContentType(contentType)) + { + if (TryGetContentType(data, path, out X509ContentType actualType)) + { + Assert.Equal(contentType, actualType); + } + } + + if (path is null) + { + Assert.ThrowsAny(() => LoadPfxNoFile(data)); + } + else if (data is null) + { + Assert.ThrowsAny(() => LoadPfxFileOnly(path)); + } + else + { + Assert.ThrowsAny(() => LoadPfx(data, path)); + } + } + + [Fact] + public void LoadCertificate_DER_Fails() + { + LoadKnownFormat_Fails(TestData.MsCertificate, TestFiles.MsCertificateDerFile, X509ContentType.Cert); + } + + [Fact] + public void LoadCertificate_PEM_Fails() + { + LoadKnownFormat_Fails(TestData.MsCertificatePemBytes, TestFiles.MsCertificatePemFile, X509ContentType.Cert); + } + + [Fact] + public void LoadPkcs7_BER_Fails() + { + LoadKnownFormat_Fails(TestData.Pkcs7ChainDerBytes, TestFiles.Pkcs7ChainDerFile, X509ContentType.Pkcs7); + } + + [Fact] + public void LoadPkcs7_PEM_Fails() + { + LoadKnownFormat_Fails(TestData.Pkcs7ChainPemBytes, TestFiles.Pkcs7ChainPemFile, X509ContentType.Pkcs7); + } + + [Fact] + public void LoadSerializedCert_Fails() + { + LoadKnownFormat_Fails(TestData.StoreSavedAsSerializedCerData, null, X509ContentType.SerializedCert); + } + + [Fact] + public void LoadSerializedStore_Fails() + { + LoadKnownFormat_Fails(TestData.StoreSavedAsSerializedStoreData, null, X509ContentType.SerializedStore); + } + + [Fact] + public void LoadSignedFile_Fails() + { + LoadKnownFormat_Fails(null, TestFiles.SignedMsuFile, X509ContentType.Authenticode); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_Single_WithPassword(bool ignorePrivateKeys) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnorePrivateKeys = ignorePrivateKeys, + }; + + X509Certificate2 cert = LoadPfx( + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + EphemeralIfPossible, + loaderLimits); + + using (cert) + { + Assert.Equal("CN=MyName", cert.Subject); + Assert.NotEqual(ignorePrivateKeys, cert.HasPrivateKey); + } + } + + [Theory] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void LoadPfx_Single_NoPassword(bool ignorePrivateKeys, bool useNull) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnorePrivateKeys = ignorePrivateKeys, + }; + + string password = useNull ? null : ""; + + X509Certificate2 cert = LoadPfxNoFile( + TestData.PfxWithNoPassword, + password, + EphemeralIfPossible, + loaderLimits); + + using (cert) + { + Assert.Equal("CN=MyName", cert.Subject); + Assert.NotEqual(ignorePrivateKeys, cert.HasPrivateKey); + } + } + + [ConditionalTheory(typeof(PlatformSupport), nameof(PlatformSupport.IsRC2Supported))] + [InlineData(true, true)] + [InlineData(true, false)] + [InlineData(false, true)] + [InlineData(false, false)] + public void LoadPfx_Single_NoPassword_AmbiguousDecrypt(bool ignorePrivateKeys, bool useNull) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnorePrivateKeys = ignorePrivateKeys, + }; + + string password = useNull ? null : ""; + + X509Certificate2 cert = LoadPfxNoFile( + TestData.MsCertificateExportedToPfx_NullPassword, + password, + EphemeralIfPossible, + loaderLimits); + + using (cert) + { + X509CertificateLoaderTests.AssertRawDataEquals(TestData.MsCertificate, cert); + Assert.False(cert.HasPrivateKey, "cert.HasPrivateKey"); + } + } + + [Fact] + public void LoadPfx_Single_WrongPassword() + { + CryptographicException ex = Assert.Throws( + () => LoadPfx(TestData.PfxData, TestFiles.PfxFile, "asdf")); + + Assert.Contains("password", ex.Message); + Assert.Equal(ERROR_INVALID_PASSWORD, ex.HResult); + } + + [Fact] + public void LoadPfx_Single_EmptyPassword_WithWrongPassword() + { + CryptographicException ex = Assert.Throws( + () => LoadPfxNoFile(TestData.PfxWithNoPassword, "asdf")); + + Assert.Contains("password", ex.Message); + Assert.Equal(ERROR_INVALID_PASSWORD, ex.HResult); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_Single_EmptyPassword_NoMac(bool useEmpty) + { + string password = useEmpty ? "" : null; + + X509Certificate2 cert = LoadPfxNoFile( + TestData.Pkcs12OpenSslOneCertDefaultNoMac, + password, + EphemeralIfPossible); + + using (cert) + { + Assert.Equal("CN=test", cert.Subject); + } + } + + [Fact] + public void LoadPfx_WithTrailingData() + { + byte[] data = TestData.PfxWithNoPassword; + Array.Resize(ref data, data.Length + 10); + + using (X509Certificate2 cert = LoadPfxNoFile(data)) + { + Assert.Equal("CN=MyName", cert.Subject); + } + } + + [Fact] + public void LoadPfx_Empty() + { + AssertExtensions.Throws( + () => LoadPfxNoFile(TestData.EmptyPfx), + "The provided PFX data contains no certificates."); + } + + private void LoadPfx_VerifyLimit( + string propertyTested, + bool fail, + byte[] bytes, + string path, + string password, + Pkcs12LoaderLimits loaderLimits) + { + Func test; + + if (bytes is null) + { + test = () => LoadPfxFileOnly(path, password, EphemeralIfPossible, loaderLimits); + } + else if (path is null) + { + test = () => LoadPfxNoFile(bytes, password, EphemeralIfPossible, loaderLimits); + } + else + { + test = () => LoadPfx(bytes, path, password, EphemeralIfPossible, loaderLimits); + } + + if (fail) + { + Pkcs12LoadLimitExceededException ex = + AssertExtensions.Throws(() => test()); + + Assert.Contains(propertyTested, ex.Message); + } + else + { + // Assert.NoThrow + test().Dispose(); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_VerifyMacIterationLimit(bool failLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + MacIterationLimit = failLimit ? 1999 : 2000, + }; + + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.MacIterationLimit), + failLimit, + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + loaderLimits); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_VerifyKdfIterationLimit(bool failLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IndividualKdfIterationLimit = failLimit ? 1999 : 2000, + }; + + // Both 1999 and 2000 will fail, because the key uses 2001. + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.IndividualKdfIterationLimit), + fail: true, + TestData.MixedIterationsPfx, + null, + TestData.PlaceholderPw, + loaderLimits); + + loaderLimits.IgnorePrivateKeys = true; + + // Now that we're ignoring the key, 1999 will fail, 2000 will pass. + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.IndividualKdfIterationLimit), + failLimit, + TestData.MixedIterationsPfx, + null, + TestData.PlaceholderPw, + loaderLimits); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void LoadPfx_VerifyTotalKdfIterationLimit(bool failLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + TotalKdfIterationLimit = failLimit ? 3999 : 4000, + }; + + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.TotalKdfIterationLimit), + failLimit, + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + loaderLimits); + } + + [Theory] + [InlineData(null)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + public void LoadPfx_VerifyCertificateLimit(int? certLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + MaxCertificates = certLimit, + }; + + bool expectFailure = certLimit.GetValueOrDefault(int.MaxValue) < 3; + + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.MaxCertificates), + expectFailure, + TestData.ChainPfxBytes, + TestFiles.ChainPfxFile, + TestData.ChainPfxPassword, + loaderLimits); + } + + [Theory] + [InlineData(null)] + [InlineData(0)] + [InlineData(1)] + [InlineData(4)] + public void LoadPfx_VerifyKeysLimit(int? keysLimit) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + MaxKeys = keysLimit, + }; + + bool expectFailure = keysLimit.GetValueOrDefault(int.MaxValue) < 1; + + LoadPfx_VerifyLimit( + nameof(Pkcs12LoaderLimits.MaxKeys), + expectFailure, + TestData.ChainPfxBytes, + TestFiles.ChainPfxFile, + TestData.ChainPfxPassword, + loaderLimits); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void LoadPfx_VerifyIgnoreEncryptedSafes(bool ignoreEncrypted) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnoreEncryptedAuthSafes = ignoreEncrypted, + }; + + string expectedSubject = ignoreEncrypted ? + "CN=Plaintext Test Certificate, OU=.NET Libraries, O=Microsoft Corporation" : + "CN=Encrypted Test Certificate, OU=.NET Libraries, O=Microsoft Corporation"; + + X509Certificate2 cert = LoadPfxNoFile( + TestData.TwoCertsPfx_OneEncrypted, + TestData.PlaceholderPw, + default, + loaderLimits); + + using (cert) + { + Assert.Equal(expectedSubject, cert.Subject); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void LoadPfx_VerifyIgnoreEncryptedSafes_EmptyIfIgnored(bool ignoreEncrypted) + { + Pkcs12LoaderLimits loaderLimits = new Pkcs12LoaderLimits + { + IgnoreEncryptedAuthSafes = ignoreEncrypted, + }; + + if (ignoreEncrypted) + { + AssertExtensions.Throws( + () => LoadPfx( + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + default, + loaderLimits), + "The provided PFX data contains no certificates."); + } + else + { + X509Certificate2 cert = LoadPfx( + TestData.PfxData, + TestFiles.PfxFile, + TestData.PfxDataPassword, + default, + loaderLimits); + + using (cert) + { + Assert.Equal("CN=MyName", cert.Subject); + } + } + } + + [Fact] + public void VerifyIndependentLifetime() + { + X509Certificate2 c1 = LoadPfx(TestData.PfxData, TestFiles.PfxFile, TestData.PfxDataPassword); + + using (X509Certificate2 c2 = LoadPfx(TestData.PfxData, TestFiles.PfxFile, TestData.PfxDataPassword)) + { + RSA rsa = c1.GetRSAPrivateKey(); + byte[] hash = new byte[256 >> 3]; + byte[] sig = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + Assert.Equal(TestData.PfxSha256Empty_ExpectedSig, sig); + + c1.Dispose(); + rsa.Dispose(); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + // The c1 objects being disposed have no bearing on c2 + using (rsa = c2.GetRSAPrivateKey()) + { + sig = rsa.SignHash(hash, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + Assert.Equal(TestData.PfxSha256Empty_ExpectedSig, sig); + } + } + } + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderTests.cs new file mode 100644 index 0000000000000..f29eaf7c0e023 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/X509CertificateLoaderTests.cs @@ -0,0 +1,356 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Buffers.Binary; +using System.Diagnostics; +using System.IO; +using System.IO.MemoryMappedFiles; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderTests_FromByteArray : X509CertificateLoaderTests + { + protected override void NullInputAssert(Action action) => + AssertExtensions.Throws("data", action); + + protected override void EmptyInputAssert(Action action) => + Assert.ThrowsAny(action); + + protected override X509Certificate2 LoadCertificate(byte[] bytes, string path) => + X509CertificateLoader.LoadCertificate(bytes); + + protected override X509Certificate2 LoadCertificateFileOnly(string path) => + X509CertificateLoader.LoadCertificate(File.ReadAllBytes(path)); + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + // If the test data only provides data from a file, don't check the content type + // (it will be checked by the file variant). + if (bytes is null) + { + contentType = X509ContentType.Unknown; + return false; + } + + contentType = X509Certificate2.GetCertContentType(bytes); + return true; + } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderTests_FromByteSpan : X509CertificateLoaderTests + { + protected override void NullInputAssert(Action action) => + Assert.ThrowsAny(action); + + protected override void EmptyInputAssert(Action action) => + Assert.ThrowsAny(action); + + protected override X509Certificate2 LoadCertificate(byte[] bytes, string path) => + X509CertificateLoader.LoadCertificate(new ReadOnlySpan(bytes)); + + protected override X509Certificate2 LoadCertificateAtOffset(byte[] bytes, int offset) => + X509CertificateLoader.LoadCertificate(bytes.AsSpan(offset)); + + protected override X509Certificate2 LoadCertificateFileOnly(string path) + { + // Use a strategy other than File.ReadAllBytes. + using (FileStream stream = File.OpenRead(path)) + using (MemoryManager manager = MemoryMappedFileMemoryManager.CreateFromFileClamped(stream)) + { + return X509CertificateLoader.LoadCertificate(manager.Memory.Span); + } + } + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + // All test data is either from a byte[] or a file. + // If it comes from a byte[], it'll get verified by _FromByteArray; + // likewise with a file and _FromFile. + // + // Since there are no uniquely span inputs, and not all the applicable TFMs have + // a GetContentType(ReadOnlySpan), just always return false and skip the file + // format sanity test in the _FromByteSpan variant. + contentType = X509ContentType.Unknown; + return false; + } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Browser doesn't support X.509 certificates")] + public class X509CertificateLoaderTests_FromFile : X509CertificateLoaderTests + { + protected override void NullInputAssert(Action action) => + AssertExtensions.Throws("path", action); + + protected override void EmptyInputAssert(Action action) => + AssertExtensions.Throws("path", action); + + protected override X509Certificate2 LoadCertificate(byte[] bytes, string path) => + X509CertificateLoader.LoadCertificateFromFile(path); + + protected override X509Certificate2 LoadCertificateFileOnly(string path) => + X509CertificateLoader.LoadCertificateFromFile(path); + + protected override X509Certificate2 LoadCertificateNoFile(byte[] bytes) + { + using (TempFileHolder holder = new TempFileHolder(bytes)) + { + return LoadCertificate(bytes, holder.FilePath); + } + } + + protected override bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType) + { + // If the test data only provides data from a byte[], don't check the content type + // (it will be checked by the byte array variant). + if (path is null) + { + contentType = X509ContentType.Unknown; + return false; + } + + contentType = X509Certificate2.GetCertContentType(path); + return true; + } + } + + public abstract class X509CertificateLoaderTests + { + protected abstract void NullInputAssert(Action action); + protected abstract void EmptyInputAssert(Action action); + protected abstract X509Certificate2 LoadCertificate(byte[] bytes, string path); + protected abstract X509Certificate2 LoadCertificateFileOnly(string path); + + protected virtual X509Certificate2 LoadCertificateNoFile(byte[] bytes) => + LoadCertificate(bytes, null); + + protected virtual X509Certificate2 LoadCertificateAtOffset(byte[] bytes, int offset) => + LoadCertificateNoFile(bytes.AsSpan(offset).ToArray()); + + protected abstract bool TryGetContentType(byte[] bytes, string path, out X509ContentType contentType); + + [Fact] + public void LoadNull() + { + NullInputAssert(() => LoadCertificate(null, null)); + } + + [Fact] + public void LoadEmpty() + { + EmptyInputAssert(() => LoadCertificate(Array.Empty(), string.Empty)); + } + + private void LoadKnownFormat_Fails(byte[] data, string path, X509ContentType contentType) + { + if (PlatformDetection.IsWindows || !IsWindowsOnlyContentType(contentType)) + { + if (TryGetContentType(data, path, out X509ContentType actualType)) + { + Assert.Equal(contentType, actualType); + } + } + + if (path is null) + { + Assert.ThrowsAny(() => LoadCertificateNoFile(data)); + } + else if (data is null) + { + Assert.ThrowsAny(() => LoadCertificateFileOnly(path)); + } + else + { + Assert.ThrowsAny(() => LoadCertificate(data, path)); + } + } + + [Fact] + public void LoadCertificate_DER() + { + using (X509Certificate2 cert = LoadCertificate(TestData.MsCertificate, TestFiles.MsCertificateDerFile)) + { + Assert.NotNull(cert); + AssertRawDataEquals(TestData.MsCertificate, cert); + } + } + + [Fact] + public void LoadCertificate_PEM() + { + using (X509Certificate2 cert = LoadCertificate(TestData.MsCertificatePemBytes, TestFiles.MsCertificatePemFile)) + { + Assert.NotNull(cert); + AssertRawDataEquals(TestData.MsCertificate, cert); + } + } + + [Fact] + public void LoadPkcs7_BER_Fails() + { + LoadKnownFormat_Fails(TestData.Pkcs7ChainDerBytes, TestFiles.Pkcs7ChainDerFile, X509ContentType.Pkcs7); + } + + [Fact] + public void LoadPkcs7_PEM_Fails() + { + LoadKnownFormat_Fails(TestData.Pkcs7ChainPemBytes, TestFiles.Pkcs7ChainPemFile, X509ContentType.Pkcs7); + } + + [Fact] + public void LoadPfx_NeedsPassword_Fails() + { + LoadKnownFormat_Fails(TestData.PfxData, TestFiles.PfxFile, X509ContentType.Pfx); + } + + [Fact] + public void LoadPfx_NoPasswordNeeded_Fails() + { + LoadKnownFormat_Fails(TestData.PfxWithNoPassword, null, X509ContentType.Pfx); + } + + [Fact] + public void LoadSignedFile_Fails() + { + LoadKnownFormat_Fails(null, TestFiles.SignedMsuFile, X509ContentType.Authenticode); + } + + [Fact] + public void LoadSerializedCert_Fails() + { + LoadKnownFormat_Fails(TestData.StoreSavedAsSerializedCerData, null, X509ContentType.SerializedCert); + } + + [Fact] + public void LoadSerializedStore_Fails() + { + LoadKnownFormat_Fails(TestData.StoreSavedAsSerializedStoreData, null, X509ContentType.SerializedStore); + } + + [Fact] + public void LoadNestedCertificates() + { + using (X509Certificate2 cert = LoadCertificateNoFile(TestData.NestedCertificates)) + { + Assert.Equal("CN=outer", cert.Subject); + + X509Extension ext = cert.Extensions["0.0.1"]; + + using (X509Certificate2 inner = LoadCertificateNoFile(ext.RawData)) + { + Assert.Equal("CN=inner", inner.Subject); + } + } + } + + [Fact] + public void LoadCertificate_WithTrailingData() + { + // Find the PEM-encoded certificate embedded within NestedCertificates, and + // load only that portion of the data. + byte[] data = TestData.NestedCertificates; + + // The offset could be hard-coded, but it's not expensive to do the find and saves on test maintenance. + Span needle = stackalloc byte[] { 0x2D, 0x2D, 0x2D, 0x2D, 0x2D }; + int offset = data.AsSpan().IndexOf(needle); + +#if NET + // The macOS PEM loader seems to be rejecting the trailing data. + if (OperatingSystem.IsMacOS()) + { + Assert.ThrowsAny(() => LoadCertificateAtOffset(data, offset)); + return; + } +#endif + + using (X509Certificate2 cert = LoadCertificateAtOffset(data, offset)) + { + Assert.Equal("CN=inner", cert.Subject); + } + } + + [Fact] + public void LoadCertificate_DER_WithTrailingData() + { + byte[] data = TestData.MsCertificate; + Array.Resize(ref data, data.Length + 21); + + using (X509Certificate2 cert = LoadCertificateNoFile(data)) + { + AssertRawDataEquals(TestData.MsCertificate, cert); + } + } + + [Fact] + public void LoadWrappingCertificate_PEM() + { + byte[] data = System.Text.Encoding.ASCII.GetBytes( + ByteUtils.PemEncode("CERTIFICATE", TestData.NestedCertificates)); + + using (X509Certificate2 cert = LoadCertificateNoFile(data)) + { + AssertRawDataEquals(TestData.NestedCertificates, cert); + } + } + + [Fact] + public void LoadWrappingCertificate_PEM_WithTrailingData() + { + byte[] source = TestData.NestedCertificates; + Array.Resize(ref source, source.Length + 4); + + BinaryPrimitives.WriteInt32LittleEndian( + source.AsSpan(TestData.NestedCertificates.Length), + Process.GetCurrentProcess().Id); + + byte[] data = System.Text.Encoding.ASCII.GetBytes( + ByteUtils.PemEncode("CERTIFICATE", source)); + +#if NET + // OpenSSL is being more strict here than other platforms. + if (OperatingSystem.IsLinux()) + { + Assert.Throws(() => LoadCertificateNoFile(data)); + return; + } +#endif + + using (X509Certificate2 cert = LoadCertificateNoFile(data)) + { + AssertRawDataEquals(TestData.NestedCertificates, cert); + } + } + + [Fact] + public void LoadWrappingCertificate_PEM_WithSurroundingText() + { + string pem = ByteUtils.PemEncode("CERTIFICATE", TestData.NestedCertificates); + + byte[] data = System.Text.Encoding.ASCII.GetBytes( + "Four score and seven years ago ...\n" + pem + "\n... perish from this Earth."); + + using (X509Certificate2 cert = LoadCertificateNoFile(data)) + { + AssertRawDataEquals(TestData.NestedCertificates, cert); + } + } + + internal static void AssertRawDataEquals(byte[] expected, X509Certificate2 cert) + { +#if NET + AssertExtensions.SequenceEqual(expected, cert.RawDataMemory.Span); +#else + AssertExtensions.SequenceEqual(expected, cert.RawData); +#endif + } + + internal static bool IsWindowsOnlyContentType(X509ContentType contentType) + { + return contentType is X509ContentType.Authenticode or X509ContentType.SerializedStore or X509ContentType.SerializedCert; + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.Forwards.cs b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.Forwards.cs index 5f6e0fbf89576..b7cb4dae61d56 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.Forwards.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.Forwards.cs @@ -1,4 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#if NET8_0_OR_GREATER [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.SP800108HmacCounterKdf))] +#endif +#if NET9_0_OR_GREATER +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.X509Certificates.Pkcs12LoadLimitExceededException))] +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Security.Cryptography.X509Certificates.X509CertificateLoader))] +#endif diff --git a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.cs b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.cs index 83dc7a9f3fa30..e13db0f1f5e4e 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.cs @@ -4,6 +4,7 @@ // Changes to this file must follow the https://aka.ms/api-review process. // ------------------------------------------------------------------------------ +#if NETFRAMEWORK || NETSTANDARD namespace System.Security.Cryptography { public sealed partial class SP800108HmacCounterKdf : System.IDisposable @@ -25,3 +26,50 @@ public void DeriveKey(System.ReadOnlySpan label, System.ReadOnlySpan public void Dispose() { } } } +#endif +#if NETFRAMEWORK || NETSTANDARD || NET8_0 +namespace System.Security.Cryptography.X509Certificates +{ + public sealed partial class Pkcs12LoaderLimits + { + public Pkcs12LoaderLimits() { } + public Pkcs12LoaderLimits(System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits copyFrom) { } + public static System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits DangerousNoLimits { get { throw null; } } + public static System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits Defaults { get { throw null; } } + public bool IgnoreEncryptedAuthSafes { get { throw null; } set { } } + public bool IgnorePrivateKeys { get { throw null; } set { } } + public int? IndividualKdfIterationLimit { get { throw null; } set { } } + public bool IsReadOnly { get { throw null; } } + public int? MacIterationLimit { get { throw null; } set { } } + public int? MaxCertificates { get { throw null; } set { } } + public int? MaxKeys { get { throw null; } set { } } + public bool PreserveCertificateAlias { get { throw null; } set { } } + public bool PreserveKeyName { get { throw null; } set { } } + public bool PreserveStorageProvider { get { throw null; } set { } } + public bool PreserveUnknownAttributes { get { throw null; } set { } } + public int? TotalKdfIterationLimit { get { throw null; } set { } } + public void MakeReadOnly() { } + } + public sealed partial class Pkcs12LoadLimitExceededException : System.Security.Cryptography.CryptographicException + { + public Pkcs12LoadLimitExceededException(string propertyName) { } + } +#if NET + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] +#endif + public static partial class X509CertificateLoader + { + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadCertificate(byte[] data) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadCertificate(System.ReadOnlySpan data) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadCertificateFromFile(string path) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadPkcs12(byte[] data, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadPkcs12(System.ReadOnlySpan data, System.ReadOnlySpan password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2Collection LoadPkcs12Collection(byte[] data, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2Collection LoadPkcs12Collection(System.ReadOnlySpan data, System.ReadOnlySpan password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2Collection LoadPkcs12CollectionFromFile(string path, System.ReadOnlySpan password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2Collection LoadPkcs12CollectionFromFile(string path, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadPkcs12FromFile(string path, System.ReadOnlySpan password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadPkcs12FromFile(string path, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + } +} +#endif diff --git a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.csproj index ef8ae599f15b4..d6c0033452364 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/ref/Microsoft.Bcl.Cryptography.csproj @@ -1,14 +1,11 @@ - netstandard2.0;$(NetFrameworkMinimum);$(NetCoreAppMinimum) + netstandard2.0;$(NetFrameworkMinimum);$(NetCoreAppMinimum);$(NetCoreAppCurrent) - + - - - diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj index 2c71f52147943..88c60b8f42ede 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft.Bcl.Cryptography.csproj @@ -1,7 +1,7 @@ - netstandard2.0;$(NetFrameworkMinimum);$(NetCoreAppMinimum) + netstandard2.0;$(NetFrameworkMinimum);$(NetCoreAppMinimum);$(NetCoreAppCurrent) true false true @@ -14,8 +14,20 @@ System.Security.Cryptography.SP800108HmacCounterKdf true - true + true + + true + false + true + + + + + + + Link="Common\Interop\Windows\BCrypt\Interop.Blobs.cs" /> - + + + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml + + + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml.cs + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml + + + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.manual.cs + Common\System\Security\Cryptography\Asn1\AlgorithmIdentifierAsn.xml + + + Common\System\Security\Cryptography\Asn1\AttributeAsn.xml + + + Common\System\Security\Cryptography\Asn1\AttributeAsn.xml.cs + Common\System\Security\Cryptography\Asn1\AttributeAsn.xml + + + Common\System\Security\Cryptography\Asn1\AttributeAsn.manual.cs + Common\System\Security\Cryptography\Asn1\AttributeAsn.xml + + + Common\System\Security\Cryptography\Asn1\DigestInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\DigestInfoAsn.xml.cs + Common\System\Security\Cryptography\Asn1\DigestInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml.cs + Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\PBEParameter.xml + + + Common\System\Security\Cryptography\Asn1\PBEParameter.xml.cs + Common\System\Security\Cryptography\Asn1\PBEParameter.xml + + + Common\System\Security\Cryptography\Asn1\PBES2Params.xml + + + Common\System\Security\Cryptography\Asn1\PBES2Params.xml.cs + Common\System\Security\Cryptography\Asn1\PBES2Params.xml + + + Common\System\Security\Cryptography\Asn1\Pbkdf2Params.xml + + + Common\System\Security\Cryptography\Asn1\Pbkdf2Params.xml.cs + Common\System\Security\Cryptography\Asn1\Pbkdf2Params.xml + + + Common\System\Security\Cryptography\Asn1\Pbkdf2SaltChoice.xml + + + Common\System\Security\Cryptography\Asn1\Pbkdf2SaltChoice.xml.cs + Common\System\Security\Cryptography\Asn1\Pbkdf2SaltChoice.xml + + + Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml.cs + Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\Rc2CbcParameters.xml + + + Common\System\Security\Cryptography\Asn1\Rc2CbcParameters.xml.cs + Common\System\Security\Cryptography\Asn1\Rc2CbcParameters.xml + + + Common\System\Security\Cryptography\Asn1\Rc2CbcParameters.manual.cs + Common\System\Security\Cryptography\Asn1\Rc2CbcParameters.xml + + + Common\System\Security\Cryptography\Asn1\SubjectPublicKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\SubjectPublicKeyInfoAsn.xml.cs + Common\System\Security\Cryptography\Asn1\SubjectPublicKeyInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\CertBagAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\CertBagAsn.xml.cs + Common\System\Security\Cryptography\Asn1\Pkcs12\CertBagAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\MacData.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\MacData.xml.cs + Common\System\Security\Cryptography\Asn1\Pkcs12\MacData.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\PfxAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\PfxAsn.manual.cs + Common\System\Security\Cryptography\Asn1\Pkcs12\PfxAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\PfxAsn.xml.cs + Common\System\Security\Cryptography\Asn1\Pkcs12\PfxAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\SafeBagAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs12\SafeBagAsn.xml.cs + Common\System\Security\Cryptography\Asn1\Pkcs12\SafeBagAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs7\ContentInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs7\ContentInfoAsn.xml.cs + Common\System\Security\Cryptography\Asn1\Pkcs7\ContentInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedContentInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedContentInfoAsn.xml.cs + Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedContentInfoAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedDataAsn.xml + + + Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedDataAsn.xml.cs + Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedDataAsn.xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.cs new file mode 100644 index 0000000000000..918c1cbd4ea50 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Security; +using Internal.Cryptography; + +namespace Microsoft.Win32.SafeHandles +{ + /// + /// Wrap a string- or SecureString-based object. A null value indicates IntPtr.Zero should be used. + /// + internal sealed partial class SafePasswordHandle : SafeHandleZeroOrMinusOneIsInvalid + { + internal int Length { get; private set; } + + /// + /// This is used to track if a password was explicitly provided. + /// A null/empty password is a valid password. + /// + internal bool PasswordProvided { get; } + + public SafePasswordHandle(string? password, bool passwordProvided) + : base(ownsHandle: true) + { + if (password != null) + { + handle = Marshal.StringToHGlobalUni(password); + Length = password.Length; + } + + PasswordProvided = passwordProvided; + } + + public SafePasswordHandle(ReadOnlySpan password, bool passwordProvided) + : base(ownsHandle: true) + { + // "".AsSpan() is not default, so this is compat for "null tries NULL first". + if (!password.ContainsNull()) + { + int spanLen; + + checked + { + spanLen = password.Length + 1; + handle = Marshal.AllocHGlobal(spanLen * sizeof(char)); + } + + unsafe + { + Span dest = new Span((void*)handle, spanLen); + password.CopyTo(dest); + dest[password.Length] = '\0'; + } + + Length = password.Length; + } + + PasswordProvided = passwordProvided; + } + + public SafePasswordHandle(SecureString? password, bool passwordProvided) + : base(ownsHandle: true) + { + if (password != null) + { + handle = Marshal.SecureStringToGlobalAllocUnicode(password); + Length = password.Length; + } + + PasswordProvided = passwordProvided; + } + + protected override bool ReleaseHandle() + { + Marshal.ZeroFreeGlobalAllocUnicode(handle); + SetHandle((IntPtr)(-1)); + Length = 0; + return true; + } + + internal ReadOnlySpan DangerousGetSpan() + { + if (IsInvalid) + { + return default; + } + + unsafe + { + return new ReadOnlySpan((char*)handle, Length); + } + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx index 85b7fab4f321a..02e47cb969f04 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/Resources/Strings.resx @@ -57,6 +57,18 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Error occurred during a cryptographic operation. + + + Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection. + + + Value was invalid. + + + {0} ('{1}') must be a non-negative and non-zero value. + Non-negative number required. @@ -66,7 +78,40 @@ The value cannot be an empty string. + + The KDF for algorithm '{0}' requires a char-based password input. + + + Algorithm '{0}' is not supported on this platform. + + + ASN1 corrupted data. + + + The hash algorithm name cannot be null or empty. + + + Key is not a valid public or private key. + + + The certificate data cannot be read with the provided password, the password may be incorrect. + + + The provided PFX data contains no certificates. + + + The EncryptedPrivateKeyInfo structure was decoded but was not successfully interpreted, the password may be incorrect. + + + The algorithm identified by '{0}' is unknown, not valid for the requested usage, or was not handled. + '{0}' is not a known hash algorithm. + + The PKCS#12/PFX violated the '{0}' limit. + + + This Pkcs12LoaderLimits object has been made read-only and can no longer be modified. + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/Helpers.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/Helpers.cs new file mode 100644 index 0000000000000..0d2af61b8d634 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/Helpers.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Diagnostics; +using System.Formats.Asn1; +using System.Security.Cryptography; + +namespace Internal.Cryptography +{ + internal static partial class Helpers + { + internal static ReadOnlyMemory DecodeOctetStringAsMemory(ReadOnlyMemory encodedOctetString) + { + try + { + ReadOnlySpan input = encodedOctetString.Span; + + if (AsnDecoder.TryReadPrimitiveOctetString( + input, + AsnEncodingRules.BER, + out ReadOnlySpan primitive, + out int consumed)) + { + if (consumed != input.Length) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (input.Overlaps(primitive, out int offset)) + { + return encodedOctetString.Slice(offset, primitive.Length); + } + + Debug.Fail("input.Overlaps(primitive) failed after TryReadPrimitiveOctetString succeeded"); + } + + byte[] ret = AsnDecoder.ReadOctetString(input, AsnEncodingRules.BER, out consumed); + + if (consumed != input.Length) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return ret; + } + catch (AsnContentException e) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); + } + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/NetStandardShims.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/NetStandardShims.cs index 4dc6e7b867d7e..1e81d7a01b023 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/NetStandardShims.cs +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/NetStandardShims.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using System.Runtime.CompilerServices; using System.Text; @@ -8,6 +9,19 @@ namespace System.Security.Cryptography { internal static class NetStandardShims { + internal static unsafe int GetByteCount(this Encoding encoding, ReadOnlySpan str) + { + if (str.IsEmpty) + { + return 0; + } + + fixed (char* pStr = str) + { + return encoding.GetByteCount(pStr, str.Length); + } + } + internal static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan str, Span destination) { if (str.IsEmpty) @@ -21,6 +35,94 @@ internal static unsafe int GetBytes(this Encoding encoding, ReadOnlySpan s return encoding.GetBytes(pStr, str.Length, pDestination, destination.Length); } } + + internal static void ReadExactly(this System.IO.Stream stream, Span buffer) => + ReadAtLeast(stream, buffer, buffer.Length, throwOnEndOfStream: true); + + internal static int ReadAtLeast( + this System.IO.Stream stream, + Span buffer, + int minimumBytes, + bool throwOnEndOfStream = true) + { + if (minimumBytes > buffer.Length) + throw new ArgumentOutOfRangeException(nameof(minimumBytes)); + + byte[] rented = CryptoPool.Rent(Math.Min(minimumBytes, 32768)); + int max = 0; + int spaceRemaining = buffer.Length; + int totalRead = 0; + + while (totalRead < minimumBytes) + { + int read = stream.Read(rented, 0, Math.Min(spaceRemaining, rented.Length)); + max = Math.Max(read, max); + + if (read == 0) + { + CryptoPool.Return(rented, max); + + if (throwOnEndOfStream) + { + throw new System.IO.EndOfStreamException(); + } + + return totalRead; + } + + spaceRemaining -= read; + totalRead += read; + rented.AsSpan(0, read).CopyTo(buffer); + buffer = buffer.Slice(read); + } + + CryptoPool.Return(rented, max); + return totalRead; + } + + internal static void AppendData(this IncrementalHash hash, ReadOnlySpan data) + { + byte[] rented = CryptoPool.Rent(data.Length); + + try + { + data.CopyTo(rented); + hash.AppendData(rented, 0, data.Length); + } + finally + { + CryptoPool.Return(rented, data.Length); + } + } + + internal static bool TryGetHashAndReset( + this IncrementalHash hash, + Span destination, + out int bytesWritten) + { + int hashSize = hash.AlgorithmName.Name switch + { + nameof(HashAlgorithmName.MD5) => 128 >> 3, + nameof(HashAlgorithmName.SHA1) => 160 >> 3, + nameof(HashAlgorithmName.SHA256) => 256 >> 3, + nameof(HashAlgorithmName.SHA384) => 384 >> 3, + nameof(HashAlgorithmName.SHA512) => 512 >> 3, + _ => throw new CryptographicException(), + }; + + if (destination.Length < hashSize) + { + bytesWritten = 0; + return false; + } + + byte[] actual = hash.GetHashAndReset(); + Debug.Assert(actual.Length == hashSize); + + actual.AsSpan().CopyTo(destination); + bytesWritten = actual.Length; + return true; + } } internal static class CryptographicOperations @@ -30,5 +132,43 @@ internal static void ZeroMemory(Span buffer) { buffer.Clear(); } + + [MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)] + internal static bool FixedTimeEquals(ReadOnlySpan left, ReadOnlySpan right) + { + // NoOptimization because we want this method to be exactly as non-short-circuiting + // as written. + // + // NoInlining because the NoOptimization would get lost if the method got inlined. + + if (left.Length != right.Length) + { + return false; + } + + int length = left.Length; + int accum = 0; + + for (int i = 0; i < length; i++) + { + accum |= left[i] - right[i]; + } + + return accum == 0; + } + } +} + +namespace System.Runtime.CompilerServices +{ + [AttributeUsage(AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] + internal sealed class CallerArgumentExpressionAttribute : Attribute + { + public CallerArgumentExpressionAttribute(string parameterName) + { + ParameterName = parameterName; + } + + public string ParameterName { get; } } } diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PbeEncryptionAlgorithm.netstandard.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PbeEncryptionAlgorithm.netstandard.cs new file mode 100644 index 0000000000000..f73df6b4e081a --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PbeEncryptionAlgorithm.netstandard.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ +#if !NET + internal enum PbeEncryptionAlgorithm + { + Unknown = 0, + Aes128Cbc = 1, + Aes192Cbc = 2, + Aes256Cbc = 3, + TripleDes3KeyPkcs12 = 4, + } +#endif +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PbeParameters.netstandard.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PbeParameters.netstandard.cs new file mode 100644 index 0000000000000..890f5b32c4934 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/PbeParameters.netstandard.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ +#if !NET + internal sealed class PbeParameters + { + public PbeEncryptionAlgorithm EncryptionAlgorithm { get; } + public HashAlgorithmName HashAlgorithm { get; } + public int IterationCount { get; } + + public PbeParameters( + PbeEncryptionAlgorithm encryptionAlgorithm, + HashAlgorithmName hashAlgorithm, + int iterationCount) + { + if (iterationCount <= 0) + { + throw new ArgumentOutOfRangeException( + nameof(iterationCount), + iterationCount, + SR.Format( + SR.ArgumentOutOfRange_Generic_MustBeNonNegativeNonZero, + nameof(iterationCount), + iterationCount)); + } + + EncryptionAlgorithm = encryptionAlgorithm; + HashAlgorithm = hashAlgorithm; + IterationCount = iterationCount; + } + } +#endif +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.ProcessedPkcs12.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.ProcessedPkcs12.cs new file mode 100644 index 0000000000000..25a778d09a194 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.ProcessedPkcs12.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + static partial void LoadPkcs12NoLimits( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + ref Pkcs12Return earlyReturn) + { + string hydrated = password.ToString(); + + if (MemoryMarshal.TryGetArray(data, out ArraySegment segment) && segment.Offset == 0) + { + Debug.Assert(segment.Array is not null); + earlyReturn = new X509Certificate2(segment.Array, hydrated, keyStorageFlags); + } + else + { + byte[] rented = CryptoPool.Rent(data.Length); + data.Span.CopyTo(rented); + + try + { + earlyReturn = new X509Certificate2(rented, hydrated, keyStorageFlags); + } + finally + { + CryptoPool.Return(rented, data.Length); + } + } + } + + static partial void LoadPkcs12NoLimits( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + ref X509Certificate2Collection? earlyReturn) + { + string hydrated = password.ToString(); + X509Certificate2Collection coll = new X509Certificate2Collection(); + + if (MemoryMarshal.TryGetArray(data, out ArraySegment segment) && segment.Offset == 0) + { + Debug.Assert(segment.Array is not null); + coll.Import(segment.Array, hydrated, keyStorageFlags); + } + else + { + byte[] rented = CryptoPool.Rent(data.Length); + data.Span.CopyTo(rented); + + try + { + coll.Import(rented, hydrated, keyStorageFlags); + } + finally + { + CryptoPool.Return(rented, data.Length); + } + } + + earlyReturn = coll; + } + + private static partial Pkcs12Return LoadPkcs12( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + ArraySegment reassembled = bagState.ToPfx(password); + + try + { + Debug.Assert(reassembled.Array is not null); + Debug.Assert(reassembled.Offset == 0); + + return new X509Certificate2(reassembled.Array, password.ToString(), keyStorageFlags); + } + finally + { + CryptoPool.Return(reassembled); + } + } + + private static partial X509Certificate2Collection LoadPkcs12Collection( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + ArraySegment reassembled = bagState.ToPfx(password); + X509Certificate2Collection coll = new X509Certificate2Collection(); + + try + { + Debug.Assert(reassembled.Array is not null); + Debug.Assert(reassembled.Offset == 0); + + coll.Import(reassembled.Array, password.ToString(), keyStorageFlags); + return coll; + } + finally + { + CryptoPool.Return(reassembled); + } + } + + private readonly partial struct Pkcs12Return + { + private readonly X509Certificate2 _cert; + + internal Pkcs12Return(X509Certificate2 cert) + { + _cert = cert; + } + + internal partial bool HasValue() => _cert is not null; + internal partial X509Certificate2 ToCertificate() => _cert; + + public static implicit operator Pkcs12Return(X509Certificate2 cert) + { + return new Pkcs12Return(cert); + } + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netfx.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netfx.cs new file mode 100644 index 0000000000000..649979d0186be --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netfx.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using Internal.Cryptography; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + public static partial X509Certificate2 LoadCertificate(byte[] data) + { + ThrowIfNull(data); + + return LoadCertificate(new ReadOnlySpan(data)); + } + + public static partial X509Certificate2 LoadCertificate(ReadOnlySpan data) + { + unsafe + { + fixed (byte* dataPtr = data) + { + Interop.Crypt32.DATA_BLOB blob = new Interop.Crypt32.DATA_BLOB( + (IntPtr)dataPtr, + (uint)data.Length); + + return LoadCertificate( + Interop.Crypt32.CertQueryObjectType.CERT_QUERY_OBJECT_BLOB, + &blob); + } + } + } + + public static partial X509Certificate2 LoadCertificateFromFile(string path) + { + ThrowIfNullOrEmpty(path); + + unsafe + { + fixed (char* pathPtr = path) + { + return LoadCertificate( + Interop.Crypt32.CertQueryObjectType.CERT_QUERY_OBJECT_FILE, + pathPtr); + } + } + } + + private static unsafe X509Certificate2 LoadCertificate( + Interop.Crypt32.CertQueryObjectType objectType, + void* pvObject) + { + Debug.Assert(objectType != 0); + Debug.Assert(pvObject != (void*)0); + + const Interop.Crypt32.ContentType ContentType = + Interop.Crypt32.ContentType.CERT_QUERY_CONTENT_CERT; + const Interop.Crypt32.ExpectedContentTypeFlags ExpectedContentType = + Interop.Crypt32.ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_CERT; + + IntPtr certHandle = IntPtr.Zero; + + try + { + bool loaded = Interop.Crypt32.CryptQueryObject( + objectType, + pvObject, + ExpectedContentType, + Interop.Crypt32.ExpectedFormatTypeFlags.CERT_QUERY_FORMAT_FLAG_ALL, + dwFlags: 0, + pdwMsgAndCertEncodingType: IntPtr.Zero, + out Interop.Crypt32.ContentType actualType, + pdwFormatType: IntPtr.Zero, + phCertStore: IntPtr.Zero, + phMsg: IntPtr.Zero, + out certHandle); + + if (!loaded) + { + throw Marshal.GetHRForLastWin32Error().ToCryptographicException(); + } + + // Since contentType is an input filter, actualType should not be possible to disagree. + // + // Since contentType is only CERT, singleContext should either be valid, or the + // function should have returned false. + if (actualType != ContentType) + { + throw new CryptographicException(); + } + + return new X509Certificate2(certHandle); + } + finally + { + if (certHandle != IntPtr.Zero) + { + Interop.Crypt32.CertFreeCertificateContext(certHandle); + } + } + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netstandard.cs b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netstandard.cs new file mode 100644 index 0000000000000..c867617e66b15 --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netstandard.cs @@ -0,0 +1,53 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + public static partial X509Certificate2 LoadCertificate(byte[] data) + { + X509ContentType contentType = X509Certificate2.GetCertContentType(data); + + if (contentType != X509ContentType.Cert) + { + ThrowWithHResult(SR.Cryptography_Der_Invalid_Encoding, CRYPT_E_BAD_DECODE); + } + + return new X509Certificate2(data); + } + + public static partial X509Certificate2 LoadCertificate(ReadOnlySpan data) + { + if (data.IsEmpty) + { + ThrowWithHResult(SR.Cryptography_Der_Invalid_Encoding, CRYPT_E_BAD_DECODE); + } + + byte[] rented = CryptoPool.Rent(data.Length); + + try + { + data.CopyTo(rented); + + return LoadCertificate(rented); + } + finally + { + CryptoPool.Return(rented, data.Length); + } + } + + public static partial X509Certificate2 LoadCertificateFromFile(string path) + { + X509ContentType contentType = X509Certificate2.GetCertContentType(path); + + if (contentType != X509ContentType.Cert) + { + ThrowWithHResult(SR.Cryptography_Der_Invalid_Encoding, CRYPT_E_BAD_DECODE); + } + + return new X509Certificate2(path); + } + } +} diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj index 02ee9aae4af71..309d9b2e1c16b 100644 --- a/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/Microsoft.Bcl.Cryptography.Tests.csproj @@ -4,6 +4,14 @@ $(NetCoreAppCurrent);$(NetFrameworkMinimum) + + + + + + + + + + @@ -19,4 +38,7 @@ + + + diff --git a/src/libraries/Microsoft.Bcl.Cryptography/tests/X509Certificates/TestFiles.cs b/src/libraries/Microsoft.Bcl.Cryptography/tests/X509Certificates/TestFiles.cs new file mode 100644 index 0000000000000..7015c10fd621f --- /dev/null +++ b/src/libraries/Microsoft.Bcl.Cryptography/tests/X509Certificates/TestFiles.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; +using Test.Cryptography; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + internal static class TestFiles + { + internal const string TestDataFolder = "TestData"; + + // Certs + internal static readonly string MsCertificateDerFile = Path.Combine(TestDataFolder, "MS.cer"); + internal static readonly string MsCertificatePemFile = Path.Combine(TestDataFolder, "MS.pem"); + + internal const string MicrosoftRootCertFileName = "microsoft.cer"; + internal static readonly string MicrosoftRootCertFile = Path.Combine(TestDataFolder, MicrosoftRootCertFileName); + + internal const string MyCertFileName = "My.cer"; + + internal static readonly string SignedMsuFile = Path.Combine(TestDataFolder, "Windows6.1-KB3004361-x64.msu"); + + internal const string TestCertFileName = "test.cer"; + internal static readonly string TestCertFile = Path.Combine(TestDataFolder, TestCertFileName); + + // PKCS#7 + internal static readonly string Pkcs7ChainDerFile = Path.Combine(TestDataFolder, "certchain.p7b"); + internal static readonly string Pkcs7ChainPemFile = Path.Combine(TestDataFolder, "certchain.p7c"); + internal static readonly string Pkcs7EmptyDerFile = Path.Combine(TestDataFolder, "empty.p7b"); + internal static readonly string Pkcs7EmptyPemFile = Path.Combine(TestDataFolder, "empty.p7c"); + internal static readonly string Pkcs7SingleDerFile = Path.Combine(TestDataFolder, "singlecert.p7b"); + internal static readonly string Pkcs7SinglePemFile = Path.Combine(TestDataFolder, "singlecert.p7c"); + + // PKCS#12 + private static readonly string PfxSuffix = PlatformSupport.IsRC2Supported ? ".pfx" : ".noRC2.pfx"; + + internal static readonly string ChainPfxFile = Path.Combine(TestDataFolder, "test" + PfxSuffix); + internal static readonly string DummyTcpServerPfxFile = Path.Combine(TestDataFolder, "DummyTcpServer" + PfxSuffix); + internal static readonly string PfxFileName = "My" + PfxSuffix; + internal static readonly string PfxFile = Path.Combine(TestDataFolder, PfxFileName); + } +} diff --git a/src/libraries/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj b/src/libraries/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj index 06f148fabc7ef..fb549df74f4b4 100644 --- a/src/libraries/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj +++ b/src/libraries/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj @@ -66,8 +66,6 @@ System.Security.Cryptography.Pkcs.EnvelopedCms Link="Common\System\HexConverter.cs" /> - Common\System\Security\Cryptography\Asn1\Pkcs7\ContentInfoAsn.xml @@ -619,8 +617,6 @@ System.Security.Cryptography.Pkcs.EnvelopedCms Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedDataAsn.xml.cs Common\System\Security\Cryptography\Asn1\Pkcs7\EncryptedDataAsn.xml - + diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 46ef9900dbf4c..e96500b4f2292 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -2887,6 +2887,30 @@ public enum OpenFlags OpenExistingOnly = 4, IncludeArchived = 8, } + public sealed partial class Pkcs12LoaderLimits + { + public Pkcs12LoaderLimits() { } + public Pkcs12LoaderLimits(System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits copyFrom) { } + public static System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits DangerousNoLimits { get { throw null; } } + public static System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits Defaults { get { throw null; } } + public bool IgnoreEncryptedAuthSafes { get { throw null; } set { } } + public bool IgnorePrivateKeys { get { throw null; } set { } } + public int? IndividualKdfIterationLimit { get { throw null; } set { } } + public bool IsReadOnly { get { throw null; } } + public int? MacIterationLimit { get { throw null; } set { } } + public int? MaxCertificates { get { throw null; } set { } } + public int? MaxKeys { get { throw null; } set { } } + public bool PreserveCertificateAlias { get { throw null; } set { } } + public bool PreserveKeyName { get { throw null; } set { } } + public bool PreserveStorageProvider { get { throw null; } set { } } + public bool PreserveUnknownAttributes { get { throw null; } set { } } + public int? TotalKdfIterationLimit { get { throw null; } set { } } + public void MakeReadOnly() { } + } + public sealed partial class Pkcs12LoadLimitExceededException : System.Security.Cryptography.CryptographicException + { + public Pkcs12LoadLimitExceededException(string propertyName) { } + } public sealed partial class PublicKey { public PublicKey(System.Security.Cryptography.AsymmetricAlgorithm key) { } @@ -3309,6 +3333,21 @@ public void Reset() { } void System.Collections.IEnumerator.Reset() { } } } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + public static partial class X509CertificateLoader + { + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadCertificate(byte[] data) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadCertificate(System.ReadOnlySpan data) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadCertificateFromFile(string path) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadPkcs12(byte[] data, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadPkcs12(System.ReadOnlySpan data, System.ReadOnlySpan password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2Collection LoadPkcs12Collection(byte[] data, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2Collection LoadPkcs12Collection(System.ReadOnlySpan data, System.ReadOnlySpan password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2Collection LoadPkcs12CollectionFromFile(string path, System.ReadOnlySpan password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2Collection LoadPkcs12CollectionFromFile(string path, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadPkcs12FromFile(string path, System.ReadOnlySpan password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + public static System.Security.Cryptography.X509Certificates.X509Certificate2 LoadPkcs12FromFile(string path, string? password, System.Security.Cryptography.X509Certificates.X509KeyStorageFlags keyStorageFlags = System.Security.Cryptography.X509Certificates.X509KeyStorageFlags.DefaultKeySet, System.Security.Cryptography.X509Certificates.Pkcs12LoaderLimits? loaderLimits = null) { throw null; } + } public partial class X509Chain : System.IDisposable { public X509Chain() { } diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 05c91987126a7..62b608677b74f 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -717,6 +717,12 @@ The PKCS#12 Exportable flag is not supported on this platform. + + The PKCS#12/PFX violated the '{0}' limit. + + + This Pkcs12LoaderLimits object has been made read-only and can no longer be modified. + The PKCS#12 PersistKeySet flag is not supported on this platform. diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj index 7d0fd8f152118..b9f0ce5317f7f 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -33,6 +33,8 @@ Link="Common\Microsoft\Win32\SafeHandles\SafeX509ChainHandle.cs" /> + - - + + + + @@ -547,11 +553,13 @@ + + @@ -683,6 +691,7 @@ + @@ -899,7 +908,6 @@ - @@ -908,10 +916,11 @@ - + + @@ -1029,7 +1038,6 @@ - @@ -1046,10 +1054,11 @@ - + + @@ -1162,9 +1171,9 @@ - + @@ -1257,12 +1266,12 @@ - + @@ -1286,12 +1295,11 @@ - - + @@ -1776,6 +1784,7 @@ + @@ -1790,6 +1799,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs index 42a1da0fa9d79..c456244223293 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidCertificatePal.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -8,6 +9,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.X509Certificates.Asn1; using System.Text; using Microsoft.Win32.SafeHandles; @@ -78,7 +80,6 @@ private static ICertificatePal FromBlob(ReadOnlySpan rawData, SafePassword { Debug.Assert(password != null); - bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); X509ContentType contentType = X509Certificate2.GetCertContentType(rawData); switch (contentType) @@ -89,14 +90,20 @@ private static ICertificatePal FromBlob(ReadOnlySpan rawData, SafePassword // We don't support determining this on Android right now, so we throw. throw new CryptographicException(SR.Cryptography_X509_PKCS7_NoSigner); case X509ContentType.Pkcs12: - if ((keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet) + try { - throw new PlatformNotSupportedException(SR.Cryptography_X509_PKCS12_PersistKeySetNotSupported); + return X509CertificateLoader.LoadPkcs12Pal( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(readingFromFile, password)); + } + catch (Pkcs12LoadLimitExceededException e) + { + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); } - - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile, password.PasswordProvided); - - return ReadPkcs12(rawData, password, ephemeralSpecified); case X509ContentType.Cert: default: { @@ -126,8 +133,26 @@ public static ICertificatePal FromFile(string fileName, SafePasswordHandle passw } // Handles both DER and PEM - internal static bool TryReadX509(ReadOnlySpan rawData, [NotNullWhen(true)] out ICertificatePal? handle) + internal static unsafe bool TryReadX509(ReadOnlySpan rawData, [NotNullWhen(true)] out ICertificatePal? handle) { + if (rawData.IsEmpty) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // Prevent Android PKCS7 content sniffing + if (rawData[0] == 0x30) + { + fixed (byte* rawDataPtr = rawData) + { + using (PointerMemoryManager manager = new(rawDataPtr, rawData.Length)) + { + AsnValueReader reader = new AsnValueReader(rawData, AsnEncodingRules.DER); + CertificateAsn.Decode(ref reader, manager.Memory, out _); + } + } + } + handle = null; SafeX509Handle certHandle = Interop.AndroidCrypto.X509Decode( ref MemoryMarshal.GetReference(rawData), @@ -143,24 +168,6 @@ ref MemoryMarshal.GetReference(rawData), return true; } - private static AndroidCertificatePal ReadPkcs12(ReadOnlySpan rawData, SafePasswordHandle password, bool ephemeralSpecified) - { - using (var reader = new AndroidPkcs12Reader()) - { - reader.ParsePkcs12(rawData); - reader.Decrypt(password, ephemeralSpecified); - - UnixPkcs12Reader.CertAndKey certAndKey = reader.GetSingleCert(); - AndroidCertificatePal pal = (AndroidCertificatePal)certAndKey.Cert!; - if (certAndKey.Key != null) - { - pal.SetPrivateKey(AndroidPkcs12Reader.GetPrivateKey(certAndKey.Key)); - } - - return pal; - } - } - internal AndroidCertificatePal(SafeJObjectHandle handle) { _cert = Interop.AndroidCrypto.GetPrivateKeyEntryCertificate(handle); @@ -453,7 +460,7 @@ public ICertificatePal CopyWithPrivateKey(DSA privateKey) { typedKey.ImportParameters(dsaParameters); return CopyWithPrivateKeyHandle(typedKey.DuplicateKeyHandle()); - }; + } } public ICertificatePal CopyWithPrivateKey(ECDsa privateKey) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidPkcs12Reader.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidPkcs12Reader.cs deleted file mode 100644 index 10800a71d537b..0000000000000 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AndroidPkcs12Reader.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Formats.Asn1; -using System.Runtime.InteropServices; -using System.Security.Cryptography.Asn1; -using Microsoft.Win32.SafeHandles; - -namespace System.Security.Cryptography.X509Certificates -{ - internal sealed class AndroidPkcs12Reader : UnixPkcs12Reader - { - internal AndroidPkcs12Reader() - { - } - - public static bool IsPkcs12(ReadOnlySpan data) - { - try - { - using (var reader = new AndroidPkcs12Reader()) - { - reader.ParsePkcs12(data); - return true; - } - } - catch (CryptographicException) - { - } - - return false; - } - - protected override ICertificatePalCore ReadX509Der(ReadOnlyMemory data) - { - ICertificatePal? cert; - if (!AndroidCertificatePal.TryReadX509(data.Span, out cert)) - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - - return cert; - } - - protected override AsymmetricAlgorithm LoadKey(ReadOnlyMemory pkcs8) - { - PrivateKeyInfoAsn privateKeyInfo = PrivateKeyInfoAsn.Decode(pkcs8, AsnEncodingRules.BER); - AsymmetricAlgorithm key; - - string algorithm = privateKeyInfo.PrivateKeyAlgorithm.Algorithm; - switch (algorithm) - { - case Oids.Rsa: - key = new RSAImplementation.RSAAndroid(); - break; - case Oids.Dsa: - key = new DSAImplementation.DSAAndroid(); - break; - case Oids.EcDiffieHellman: - case Oids.EcPublicKey: - key = new ECDsaImplementation.ECDsaAndroid(); - break; - default: - throw new CryptographicException(SR.Cryptography_UnknownAlgorithmIdentifier, algorithm); - } - - key.ImportPkcs8PrivateKey(pkcs8.Span, out int bytesRead); - if (bytesRead != pkcs8.Length) - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - - return key; - } - - internal static SafeKeyHandle GetPrivateKey(AsymmetricAlgorithm key) - { - if (key is ECDsaImplementation.ECDsaAndroid ecdsa) - { - return ecdsa.DuplicateKeyHandle(); - } - - if (key is RSAImplementation.RSAAndroid rsa) - { - return rsa.DuplicateKeyHandle(); - } - - if (key is DSAImplementation.DSAAndroid dsa) - { - return dsa.DuplicateKeyHandle(); - } - - throw new NotImplementedException($"{nameof(GetPrivateKey)} ({key.GetType()})"); - } - } -} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.ImportExport.iOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.ImportExport.iOS.cs index dbbdf1d5c2eaa..e317dc7fb67c5 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.ImportExport.iOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.ImportExport.iOS.cs @@ -98,8 +98,6 @@ internal static ICertificatePal FromDerBlob( { Debug.Assert(password != null); - bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); - if (contentType == X509ContentType.Pkcs7) { throw new CryptographicException( @@ -109,18 +107,20 @@ internal static ICertificatePal FromDerBlob( if (contentType == X509ContentType.Pkcs12) { - if ((keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable) + try { - throw new PlatformNotSupportedException(SR.Cryptography_X509_PKCS12_ExportableNotSupported); + return (AppleCertificatePal)X509CertificateLoader.LoadPkcs12Pal( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(readingFromFile, password)); } - - if ((keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet) + catch (Pkcs12LoadLimitExceededException e) { - throw new PlatformNotSupportedException(SR.Cryptography_X509_PKCS12_PersistKeySetNotSupported); + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); } - - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile, password.PasswordProvided); - return ImportPkcs12(rawData, password, ephemeralSpecified); } SafeSecIdentityHandle identityHandle; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.ImportExport.macOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.ImportExport.macOS.cs index ce4745c42d58f..2d1b84b113863 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.ImportExport.macOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.ImportExport.macOS.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Formats.Asn1; using System.Security.Cryptography.Apple; -using System.Threading; using Microsoft.Win32.SafeHandles; namespace System.Security.Cryptography.X509Certificates @@ -44,24 +43,19 @@ private static AppleCertificatePal FromBlob( if (contentType == X509ContentType.Pkcs12) { - if ((keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) == X509KeyStorageFlags.EphemeralKeySet) + try { - throw new PlatformNotSupportedException(SR.Cryptography_X509_NoEphemeralPfx); + return (AppleCertificatePal)X509CertificateLoader.LoadPkcs12Pal( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(readingFromFile, password)); } - - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile, password.PasswordProvided); - bool exportable = (keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable; - - bool persist = - (keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet; - - SafeKeychainHandle keychain = persist - ? Interop.AppleCrypto.SecKeychainCopyDefault() - : Interop.AppleCrypto.CreateTemporaryKeychain(); - - using (keychain) + catch (Pkcs12LoadLimitExceededException e) { - return ImportPkcs12(rawData, password, exportable, keychain); + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.iOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.iOS.cs index 9076f0c4899a1..e5903066a14bf 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.iOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Keys.iOS.cs @@ -20,17 +20,17 @@ public ICertificatePal CopyWithPrivateKey(DSA privateKey) public ICertificatePal CopyWithPrivateKey(ECDsa privateKey) { - return ImportPkcs12(new UnixPkcs12Reader.CertAndKey { Cert = this, Key = privateKey }); + return ImportPkcs12(this, privateKey); } public ICertificatePal CopyWithPrivateKey(ECDiffieHellman privateKey) { - return ImportPkcs12(new UnixPkcs12Reader.CertAndKey { Cert = this, Key = privateKey }); + return ImportPkcs12(this, privateKey); } public ICertificatePal CopyWithPrivateKey(RSA privateKey) { - return ImportPkcs12(new UnixPkcs12Reader.CertAndKey { Cert = this, Key = privateKey }); + return ImportPkcs12(this, privateKey); } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Pkcs12.iOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Pkcs12.iOS.cs index 1de7e8ce9bee7..7616da6ffe4ec 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Pkcs12.iOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Pkcs12.iOS.cs @@ -9,26 +9,11 @@ internal sealed partial class AppleCertificatePal : ICertificatePal { private static readonly SafePasswordHandle s_passwordExportHandle = new SafePasswordHandle("DotnetExportPassphrase", passwordProvided: true); - private static AppleCertificatePal ImportPkcs12( - ReadOnlySpan rawData, - SafePasswordHandle password, - bool ephemeralSpecified) + internal static AppleCertificatePal ImportPkcs12(AppleCertificatePal pal, AsymmetricAlgorithm? key) { - using (ApplePkcs12Reader reader = new ApplePkcs12Reader()) + if (key is not null) { - reader.ParsePkcs12(rawData); - reader.Decrypt(password, ephemeralSpecified); - return ImportPkcs12(reader.GetSingleCert()); - } - } - - internal static AppleCertificatePal ImportPkcs12(UnixPkcs12Reader.CertAndKey certAndKey) - { - AppleCertificatePal pal = (AppleCertificatePal)certAndKey.Cert!; - - if (certAndKey.Key != null) - { - AppleCertificateExporter exporter = new AppleCertificateExporter(new TempExportPal(pal), certAndKey.Key); + AppleCertificateExporter exporter = new AppleCertificateExporter(new TempExportPal(pal), key); byte[] smallPfx = exporter.Export(X509ContentType.Pkcs12, s_passwordExportHandle)!; SafeSecIdentityHandle identityHandle; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Pkcs12.macOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Pkcs12.macOS.cs index 6e329434278de..1951e7dc8bd09 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Pkcs12.macOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/AppleCertificatePal.Pkcs12.macOS.cs @@ -9,53 +9,6 @@ namespace System.Security.Cryptography.X509Certificates { internal sealed partial class AppleCertificatePal : ICertificatePal { - private static AppleCertificatePal ImportPkcs12( - ReadOnlySpan rawData, - SafePasswordHandle password, - bool exportable, - SafeKeychainHandle keychain) - { - using (ApplePkcs12Reader reader = new ApplePkcs12Reader()) - { - reader.ParsePkcs12(rawData); - reader.Decrypt(password, ephemeralSpecified: false); - - UnixPkcs12Reader.CertAndKey certAndKey = reader.GetSingleCert(); - AppleCertificatePal pal = (AppleCertificatePal)certAndKey.Cert!; - - SafeSecKeyRefHandle? safeSecKeyRefHandle = - ApplePkcs12Reader.GetPrivateKey(certAndKey.Key); - - AppleCertificatePal? newPal; - - using (safeSecKeyRefHandle) - { - // SecItemImport doesn't seem to respect non-exportable import for PKCS#8, - // only PKCS#12. - // - // So, as part of reading this PKCS#12 we now need to write the minimum - // PKCS#12 in a normalized form, and ask the OS to import it. - if (!exportable && safeSecKeyRefHandle != null) - { - using (pal) - { - return ImportPkcs12NonExportable(pal, safeSecKeyRefHandle, password, keychain); - } - } - - newPal = pal.MoveToKeychain(keychain, safeSecKeyRefHandle); - - if (newPal != null) - { - pal.Dispose(); - } - } - - // If no new PAL came back, it means we moved the cert, but had no private key. - return newPal ?? pal; - } - } - internal static AppleCertificatePal ImportPkcs12NonExportable( AppleCertificatePal cert, SafeSecKeyRefHandle privateKey, diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12CertLoader.iOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12CertLoader.iOS.cs deleted file mode 100644 index c190f3400b959..0000000000000 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12CertLoader.iOS.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Threading; -using Microsoft.Win32.SafeHandles; - -namespace System.Security.Cryptography.X509Certificates -{ - internal sealed class ApplePkcs12CertLoader : ILoaderPal - { - private readonly ApplePkcs12Reader _pkcs12; - private SafePasswordHandle _password; - - public ApplePkcs12CertLoader( - ApplePkcs12Reader pkcs12, - SafePasswordHandle password) - { - _pkcs12 = pkcs12; - - bool addedRef = false; - password.DangerousAddRef(ref addedRef); - _password = password; - } - - public void Dispose() - { - _pkcs12.Dispose(); - - SafePasswordHandle? password = Interlocked.Exchange(ref _password, null!); - password?.DangerousRelease(); - } - - public void MoveTo(X509Certificate2Collection collection) - { - foreach (UnixPkcs12Reader.CertAndKey certAndKey in _pkcs12.EnumerateAll()) - { - collection.Add(new X509Certificate2(AppleCertificatePal.ImportPkcs12(certAndKey))); - } - } - } -} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12Reader.iOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12Reader.iOS.cs deleted file mode 100644 index e493436e01d7b..0000000000000 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12Reader.iOS.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Formats.Asn1; -using System.Security.Cryptography.Asn1; -using Microsoft.Win32.SafeHandles; - -namespace System.Security.Cryptography.X509Certificates -{ - internal sealed class ApplePkcs12Reader : UnixPkcs12Reader - { - internal ApplePkcs12Reader() - { - } - - protected override ICertificatePalCore ReadX509Der(ReadOnlyMemory data) - { - SafeSecCertificateHandle certHandle = Interop.AppleCrypto.X509ImportCertificate( - data.Span, - X509ContentType.Cert, - SafePasswordHandle.InvalidHandle, - out SafeSecIdentityHandle identityHandle); - - if (identityHandle.IsInvalid) - { - identityHandle.Dispose(); - return new AppleCertificatePal(certHandle); - } - - Debug.Fail("Non-PKCS12 import produced an identity handle"); - - identityHandle.Dispose(); - certHandle.Dispose(); - throw new CryptographicException(); - } - - protected override AsymmetricAlgorithm LoadKey(ReadOnlyMemory pkcs8) - { - PrivateKeyInfoAsn privateKeyInfo = PrivateKeyInfoAsn.Decode(pkcs8, AsnEncodingRules.BER); - AsymmetricAlgorithm key; - - switch (privateKeyInfo.PrivateKeyAlgorithm.Algorithm) - { - case Oids.Rsa: - key = new RSAImplementation.RSASecurityTransforms(); - break; - case Oids.EcDiffieHellman: - case Oids.EcPublicKey: - key = new ECDsaImplementation.ECDsaSecurityTransforms(); - break; - default: - throw new CryptographicException( - SR.Cryptography_UnknownAlgorithmIdentifier, - privateKeyInfo.PrivateKeyAlgorithm.Algorithm); - } - - key.ImportPkcs8PrivateKey(pkcs8.Span, out int bytesRead); - - if (bytesRead != pkcs8.Length) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - return key; - } - } -} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12Reader.macOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12Reader.macOS.cs deleted file mode 100644 index 8f3274d15d232..0000000000000 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/ApplePkcs12Reader.macOS.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics; -using System.Formats.Asn1; -using System.Security.Cryptography.Apple; -using System.Security.Cryptography.Asn1; -using Microsoft.Win32.SafeHandles; - -namespace System.Security.Cryptography.X509Certificates -{ - internal sealed class ApplePkcs12Reader : UnixPkcs12Reader - { - internal ApplePkcs12Reader() - { - } - - protected override ICertificatePalCore ReadX509Der(ReadOnlyMemory data) - { - SafeSecCertificateHandle certHandle = Interop.AppleCrypto.X509ImportCertificate( - data.Span, - X509ContentType.Cert, - SafePasswordHandle.InvalidHandle, - SafeTemporaryKeychainHandle.InvalidHandle, - exportable: true, - out SafeSecIdentityHandle identityHandle); - - if (identityHandle.IsInvalid) - { - identityHandle.Dispose(); - return new AppleCertificatePal(certHandle); - } - - Debug.Fail("Non-PKCS12 import produced an identity handle"); - - identityHandle.Dispose(); - certHandle.Dispose(); - throw new CryptographicException(); - } - - protected override AsymmetricAlgorithm LoadKey(ReadOnlyMemory pkcs8) - { - PrivateKeyInfoAsn privateKeyInfo = PrivateKeyInfoAsn.Decode(pkcs8, AsnEncodingRules.BER); - AsymmetricAlgorithm key; - - switch (privateKeyInfo.PrivateKeyAlgorithm.Algorithm) - { - case Oids.Rsa: - key = new RSAImplementation.RSASecurityTransforms(); - break; - case Oids.Dsa: - key = new DSAImplementation.DSASecurityTransforms(); - break; - case Oids.EcDiffieHellman: - case Oids.EcPublicKey: - key = new ECDsaImplementation.ECDsaSecurityTransforms(); - break; - default: - throw new CryptographicException( - SR.Cryptography_UnknownAlgorithmIdentifier, - privateKeyInfo.PrivateKeyAlgorithm.Algorithm); - } - - key.ImportPkcs8PrivateKey(pkcs8.Span, out int bytesRead); - - if (bytesRead != pkcs8.Length) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - return key; - } - - internal static SafeSecKeyRefHandle? GetPrivateKey(AsymmetricAlgorithm? key) - { - if (key == null) - { - return null; - } - - if (key is RSAImplementation.RSASecurityTransforms rsa) - { - // Convert data key to legacy CSSM key that can be imported into keychain - byte[] rsaPrivateKey = rsa.ExportRSAPrivateKey(); - using (PinAndClear.Track(rsaPrivateKey)) - { - return Interop.AppleCrypto.ImportEphemeralKey(rsaPrivateKey, true); - } - } - - if (key is DSAImplementation.DSASecurityTransforms dsa) - { - // DSA always uses legacy CSSM keys do no need to convert - return dsa.GetKeys().PrivateKey; - } - - if (key is ECDsaImplementation.ECDsaSecurityTransforms ecdsa) - { - // Convert data key to legacy CSSM key that can be imported into keychain - byte[] ecdsaPrivateKey = ecdsa.ExportECPrivateKey(); - using (PinAndClear.Track(ecdsaPrivateKey)) - { - return Interop.AppleCrypto.ImportEphemeralKey(ecdsaPrivateKey, true); - } - } - - Debug.Fail("Invalid key implementation"); - return null; - } - } -} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.Import.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.Import.cs index 2b5feeee7fe79..dee85ca9b9b1a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.Import.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.Import.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.IO; using System.Runtime.InteropServices; using Internal.Cryptography; using Microsoft.Win32.SafeHandles; @@ -27,13 +26,9 @@ private static CertificatePal FromBlobOrFile(ReadOnlySpan rawData, string? Debug.Assert(password != null); bool loadFromFile = (fileName != null); - - Interop.Crypt32.PfxCertStoreFlags pfxCertStoreFlags = MapKeyStorageFlags(keyStorageFlags); bool deleteKeyContainer = false; - Interop.Crypt32.CertEncodingType msgAndCertEncodingType; Interop.Crypt32.ContentType contentType; - Interop.Crypt32.FormatType formatType; SafeCertStoreHandle? hCertStore = null; SafeCryptMsgHandle? hCryptMsg = null; SafeCertContextHandle? pCertContext = null; @@ -57,13 +52,13 @@ private static CertificatePal FromBlobOrFile(ReadOnlySpan rawData, string? X509ExpectedContentTypeFlags, X509ExpectedFormatTypeFlags, 0, - out msgAndCertEncodingType, + out _, out contentType, - out formatType, + out _, out hCertStore, out hCryptMsg, - out pCertContext - ); + out pCertContext); + if (!success) { int hr = Marshal.GetHRForLastWin32Error(); @@ -79,21 +74,35 @@ out pCertContext } else if (contentType == Interop.Crypt32.ContentType.CERT_QUERY_CONTENT_PFX) { - if (loadFromFile) + try { - rawData = File.ReadAllBytes(fileName!); - } - - pCertContext?.Dispose(); - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile: loadFromFile, password.PasswordProvided); - pCertContext = FilterPFXStore(rawData, password, pfxCertStoreFlags); + Pkcs12LoaderLimits limits = X509Certificate.GetPkcs12Limits(loadFromFile, password); - // If PersistKeySet is set we don't delete the key, so that it persists. - // If EphemeralKeySet is set we don't delete the key, because there's no file, so it's a wasteful call. - const X509KeyStorageFlags DeleteUnless = - X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.EphemeralKeySet; + if (loadFromFile) + { + Debug.Assert(fileName is not null); - deleteKeyContainer = ((keyStorageFlags & DeleteUnless) == 0); + return (CertificatePal)X509CertificateLoader.LoadPkcs12PalFromFile( + fileName, + password.DangerousGetSpan(), + keyStorageFlags, + limits); + } + else + { + return (CertificatePal)X509CertificateLoader.LoadPkcs12Pal( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + limits); + } + } + catch (Pkcs12LoadLimitExceededException e) + { + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); + } } CertificatePal pal = new CertificatePal(pCertContext, deleteKeyContainer); @@ -149,112 +158,6 @@ private static unsafe SafeCertContextHandle GetSignerInPKCS7Store(SafeCertStoreH } } - private static SafeCertContextHandle FilterPFXStore( - ReadOnlySpan rawData, - SafePasswordHandle password, - Interop.Crypt32.PfxCertStoreFlags pfxCertStoreFlags) - { - SafeCertStoreHandle hStore; - unsafe - { - fixed (byte* pbRawData = rawData) - { - Interop.Crypt32.DATA_BLOB certBlob = new Interop.Crypt32.DATA_BLOB(new IntPtr(pbRawData), (uint)rawData.Length); - hStore = Interop.Crypt32.PFXImportCertStore(ref certBlob, password, pfxCertStoreFlags); - if (hStore.IsInvalid) - { - Exception e = Marshal.GetHRForLastWin32Error().ToCryptographicException(); - hStore.Dispose(); - throw e; - } - } - } - - try - { - // Find the first cert with private key. If none, then simply take the very first cert. Along the way, delete the keycontainers - // of any cert we don't accept. - SafeCertContextHandle pCertContext = SafeCertContextHandle.InvalidHandle; - SafeCertContextHandle? pEnumContext = null; - while (Interop.crypt32.CertEnumCertificatesInStore(hStore, ref pEnumContext)) - { - if (pEnumContext.ContainsPrivateKey) - { - if ((!pCertContext.IsInvalid) && pCertContext.ContainsPrivateKey) - { - // We already found our chosen one. Free up this one's key and move on. - - // If this one has a persisted private key, clean up the key file. - // If it was an ephemeral private key no action is required. - if (pEnumContext.HasPersistedPrivateKey) - { - SafeCertContextHandleWithKeyContainerDeletion.DeleteKeyContainer(pEnumContext); - } - } - else - { - // Found our first cert that has a private key. Set it up as our chosen one but keep iterating - // as we need to free up the keys of any remaining certs. - pCertContext.Dispose(); - pCertContext = pEnumContext.Duplicate(); - } - } - else - { - if (pCertContext.IsInvalid) - { - // Doesn't have a private key but hang on to it anyway in case we don't find any certs with a private key. - pCertContext.Dispose(); - pCertContext = pEnumContext.Duplicate(); - } - } - } - - if (pCertContext.IsInvalid) - { - pCertContext.Dispose(); - throw new CryptographicException(SR.Cryptography_Pfx_NoCertificates); - } - - return pCertContext; - } - finally - { - hStore.Dispose(); - } - } - - private static Interop.Crypt32.PfxCertStoreFlags MapKeyStorageFlags(X509KeyStorageFlags keyStorageFlags) - { - if ((keyStorageFlags & X509Certificate.KeyStorageFlagsAll) != keyStorageFlags) - throw new ArgumentException(SR.Argument_InvalidFlag, nameof(keyStorageFlags)); - - Interop.Crypt32.PfxCertStoreFlags pfxCertStoreFlags = 0; - if ((keyStorageFlags & X509KeyStorageFlags.UserKeySet) == X509KeyStorageFlags.UserKeySet) - pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.CRYPT_USER_KEYSET; - else if ((keyStorageFlags & X509KeyStorageFlags.MachineKeySet) == X509KeyStorageFlags.MachineKeySet) - pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.CRYPT_MACHINE_KEYSET; - - if ((keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable) - pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.CRYPT_EXPORTABLE; - if ((keyStorageFlags & X509KeyStorageFlags.UserProtected) == X509KeyStorageFlags.UserProtected) - pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.CRYPT_USER_PROTECTED; - - // If a user is asking for an Ephemeral key they should be willing to test their code to find out - // that it will no longer import into CAPI. This solves problems of legacy CSPs being - // difficult to do SHA-2 RSA signatures with, simplifies the story for UWP, and reduces the - // complexity of pointer interpretation. - if ((keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) == X509KeyStorageFlags.EphemeralKeySet) - pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.PKCS12_NO_PERSIST_KEY | Interop.Crypt32.PfxCertStoreFlags.PKCS12_ALWAYS_CNG_KSP; - - // In .NET Framework loading a PFX then adding the key to the Windows Certificate Store would - // enable a native application compiled against CAPI to find that private key and interoperate with it. - // - // For .NET Core this behavior is being retained. - - return pfxCertStoreFlags; - } - private const Interop.Crypt32.ExpectedContentTypeFlags X509ExpectedContentTypeFlags = Interop.Crypt32.ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_CERT | Interop.Crypt32.ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_SERIALIZED_CERT | diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.cs index 9b15a429e7c22..ad3077da24724 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/CertificatePal.Windows.cs @@ -520,7 +520,7 @@ private CertificatePal(CertificatePal copyFrom) _certContext = new SafeCertContextHandle(copyFrom._certContext); } - private CertificatePal(SafeCertContextHandle certContext, bool deleteKeyContainer) + internal CertificatePal(SafeCertContextHandle certContext, bool deleteKeyContainer) { if (deleteKeyContainer) { diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/LocalAppContextSwitches.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/LocalAppContextSwitches.cs index 0af05ab604b67..c87cb0cb4c922 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/LocalAppContextSwitches.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/LocalAppContextSwitches.cs @@ -7,7 +7,7 @@ namespace System { internal static partial class LocalAppContextSwitches { - internal const long DefaultPkcs12UnspecifiedPasswordIterationLimit = 600_000; + internal const int DefaultPkcs12UnspecifiedPasswordIterationLimit = 600_000; internal static long Pkcs12UnspecifiedPasswordIterationLimit { get; } = InitializePkcs12UnspecifiedPasswordIterationLimit(); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslPkcs12Reader.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslPkcs12Reader.cs deleted file mode 100644 index c0a4616273c04..0000000000000 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslPkcs12Reader.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Diagnostics.CodeAnalysis; -using System.Formats.Asn1; -using System.Security.Cryptography.Asn1; - -namespace System.Security.Cryptography.X509Certificates -{ - internal sealed class OpenSslPkcs12Reader : UnixPkcs12Reader - { - private OpenSslPkcs12Reader() - { - } - - protected override ICertificatePalCore ReadX509Der(ReadOnlyMemory data) - { - if (OpenSslX509CertificateReader.TryReadX509Der(data.Span, out ICertificatePal? ret)) - { - return ret; - } - - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - public static bool TryRead(ReadOnlySpan data, [NotNullWhen(true)] out OpenSslPkcs12Reader? pkcs12Reader) => - TryRead(data, out pkcs12Reader, out _, captureException: false); - - public static bool TryRead(ReadOnlySpan data, [NotNullWhen(true)] out OpenSslPkcs12Reader? pkcs12Reader, [NotNullWhen(false)] out Exception? openSslException) => - TryRead(data, out pkcs12Reader, out openSslException!, captureException: true); - - protected override AsymmetricAlgorithm LoadKey(ReadOnlyMemory pkcs8) - { - PrivateKeyInfoAsn privateKeyInfo = PrivateKeyInfoAsn.Decode(pkcs8, AsnEncodingRules.BER); - AsymmetricAlgorithm key; - - switch (privateKeyInfo.PrivateKeyAlgorithm.Algorithm) - { - case Oids.Rsa: - key = new RSAOpenSsl(); - break; - case Oids.Dsa: - key = new DSAOpenSsl(); - break; - case Oids.EcDiffieHellman: - case Oids.EcPublicKey: - key = new ECDiffieHellmanOpenSsl(); - break; - default: - throw new CryptographicException( - SR.Cryptography_UnknownAlgorithmIdentifier, - privateKeyInfo.PrivateKeyAlgorithm.Algorithm); - } - - key.ImportPkcs8PrivateKey(pkcs8.Span, out int bytesRead); - - if (bytesRead != pkcs8.Length) - { - key.Dispose(); - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - return key; - } - - internal static SafeEvpPKeyHandle GetPrivateKey(AsymmetricAlgorithm key) - { - if (key is RSAOpenSsl rsa) - { - return rsa.DuplicateKeyHandle(); - } - - if (key is DSAOpenSsl dsa) - { - return dsa.DuplicateKeyHandle(); - } - - return ((ECDiffieHellmanOpenSsl)key).DuplicateKeyHandle(); - } - - private static bool TryRead( - ReadOnlySpan data, - [NotNullWhen(true)] out OpenSslPkcs12Reader? pkcs12Reader, - out Exception? openSslException, - bool captureException) - { - openSslException = null; - - try - { - pkcs12Reader = new OpenSslPkcs12Reader(); - pkcs12Reader.ParsePkcs12(data); - return true; - } - catch (CryptographicException e) - { - if (captureException) - { - openSslException = e; - } - - pkcs12Reader = null; - return false; - } - } - } -} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslPkcsFormatReader.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslPkcsFormatReader.cs index 41a3c9bcd5a03..63d00a0ca6cd8 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslPkcsFormatReader.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslPkcsFormatReader.cs @@ -1,12 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; -using System.Security.Cryptography.X509Certificates; using Microsoft.Win32.SafeHandles; namespace System.Security.Cryptography.X509Certificates @@ -237,116 +234,5 @@ private static bool TryReadPkcs7( certPals = readPals; return true; } - - internal static bool TryReadPkcs12( - ReadOnlySpan rawData, - SafePasswordHandle password, - bool ephemeralSpecified, - bool readingFromFile, - [NotNullWhen(true)] out ICertificatePal? certPal, - out Exception? openSslException) - { - return TryReadPkcs12( - rawData, - password, - single: true, - ephemeralSpecified, - readingFromFile, - out certPal!, - out _, - out openSslException); - } - - internal static bool TryReadPkcs12( - ReadOnlySpan rawData, - SafePasswordHandle password, - bool ephemeralSpecified, - bool readingFromFile, - [NotNullWhen(true)] out List? certPals, - out Exception? openSslException) - { - return TryReadPkcs12( - rawData, - password, - single: false, - ephemeralSpecified, - readingFromFile, - out _, - out certPals!, - out openSslException); - } - - private static bool TryReadPkcs12( - ReadOnlySpan rawData, - SafePasswordHandle password, - bool single, - bool ephemeralSpecified, - bool readingFromFile, - out ICertificatePal? readPal, - out List? readCerts, - out Exception? openSslException) - { - // DER-PKCS12 - OpenSslPkcs12Reader? pfx; - - if (!OpenSslPkcs12Reader.TryRead(rawData, out pfx, out openSslException)) - { - readPal = null; - readCerts = null; - return false; - } - - using (pfx) - { - return TryReadPkcs12(rawData, pfx, password, single, ephemeralSpecified, readingFromFile, out readPal, out readCerts); - } - } - - private static bool TryReadPkcs12( - ReadOnlySpan rawData, - OpenSslPkcs12Reader pfx, - SafePasswordHandle password, - bool single, - bool ephemeralSpecified, - bool readingFromFile, - out ICertificatePal? readPal, - out List? readCerts) - { - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile, password.PasswordProvided); - pfx.Decrypt(password, ephemeralSpecified); - - if (single) - { - UnixPkcs12Reader.CertAndKey certAndKey = pfx.GetSingleCert(); - OpenSslX509CertificateReader pal = (OpenSslX509CertificateReader)certAndKey.Cert!; - - if (certAndKey.Key != null) - { - pal.SetPrivateKey(OpenSslPkcs12Reader.GetPrivateKey(certAndKey.Key)); - } - - readPal = pal; - readCerts = null; - return true; - } - - readPal = null; - List certs = new List(pfx.GetCertCount()); - - foreach (UnixPkcs12Reader.CertAndKey certAndKey in pfx.EnumerateAll()) - { - OpenSslX509CertificateReader pal = (OpenSslX509CertificateReader)certAndKey.Cert!; - - if (certAndKey.Key != null) - { - pal.SetPrivateKey(OpenSslPkcs12Reader.GetPrivateKey(certAndKey.Key)); - } - - certs.Add(pal); - } - - readCerts = certs; - return true; - } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs index 706d2a024b047..8c07209c61cf1 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509CertificateReader.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.IO; using System.Runtime.InteropServices; using System.Text; using Microsoft.Win32.SafeHandles; @@ -45,33 +44,35 @@ public static ICertificatePal FromBlob(ReadOnlySpan rawData, SafePasswordH Debug.Assert(password != null); ICertificatePal? cert; - Exception? openSslException; - bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); if (TryReadX509Der(rawData, out cert) || TryReadX509Pem(rawData, out cert) || OpenSslPkcsFormatReader.TryReadPkcs7Der(rawData, out cert) || - OpenSslPkcsFormatReader.TryReadPkcs7Pem(rawData, out cert) || - OpenSslPkcsFormatReader.TryReadPkcs12(rawData, password, ephemeralSpecified, readingFromFile: false, out cert, out openSslException)) + OpenSslPkcsFormatReader.TryReadPkcs7Pem(rawData, out cert)) { - if (cert == null) - { - // Empty collection, most likely. - throw new CryptographicException(); - } - + Debug.Assert(cert is not null); return cert; } - // Unsupported - Debug.Assert(openSslException != null); - throw openSslException; + try + { + return X509CertificateLoader.LoadPkcs12Pal( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(fromFile: false, password)); + } + catch (Pkcs12LoadLimitExceededException e) + { + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); + } } 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")) @@ -83,20 +84,20 @@ public static ICertificatePal FromFile(string fileName, SafePasswordHandle passw if (pal == null) { - OpenSslPkcsFormatReader.TryReadPkcs12( - File.ReadAllBytes(fileName), - password, - ephemeralSpecified, - readingFromFile: true, - out pal, - out Exception? exception); - - if (exception != null) + try { - throw exception; + pal = X509CertificateLoader.LoadPkcs12PalFromFile( + fileName, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(fromFile: true, password)); + } + catch (Pkcs12LoadLimitExceededException e) + { + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); } - - Debug.Assert(pal != null); } return pal; @@ -109,7 +110,7 @@ public static ICertificatePal FromFile(string fileName, SafePasswordHandle passw Debug.Assert(bioPosition >= 0); ICertificatePal? certPal; - if (TryReadX509Pem(bio, out certPal)) + if (TryReadX509Der(bio, out certPal)) { return certPal; } @@ -117,7 +118,7 @@ public static ICertificatePal FromFile(string fileName, SafePasswordHandle passw // Rewind, try again. RewindBio(bio, bioPosition); - if (TryReadX509Der(bio, out certPal)) + if (TryReadX509Pem(bio, out certPal)) { return certPal; } @@ -125,7 +126,7 @@ public static ICertificatePal FromFile(string fileName, SafePasswordHandle passw // Rewind, try again. RewindBio(bio, bioPosition); - if (OpenSslPkcsFormatReader.TryReadPkcs7Pem(bio, out certPal)) + if (OpenSslPkcsFormatReader.TryReadPkcs7Der(bio, out certPal)) { return certPal; } @@ -133,7 +134,7 @@ public static ICertificatePal FromFile(string fileName, SafePasswordHandle passw // Rewind, try again. RewindBio(bio, bioPosition); - if (OpenSslPkcsFormatReader.TryReadPkcs7Der(bio, out certPal)) + if (OpenSslPkcsFormatReader.TryReadPkcs7Pem(bio, out certPal)) { return certPal; } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509Encoder.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509Encoder.cs index 4d43399e6f7c5..5a8c9697c7722 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509Encoder.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/OpenSslX509Encoder.cs @@ -80,14 +80,9 @@ public X509ContentType GetCertContentType(ReadOnlySpan rawData) return X509ContentType.Pkcs7; } + if (X509CertificateLoader.IsPkcs12(rawData)) { - OpenSslPkcs12Reader? pfx; - - if (OpenSslPkcs12Reader.TryRead(rawData, out pfx)) - { - pfx.Dispose(); - return X509ContentType.Pkcs12; - } + return X509ContentType.Pkcs12; } // Unsupported format. @@ -147,15 +142,9 @@ public X509ContentType GetCertContentType(string fileName) } // X509ContentType.Pkcs12 (aka PFX) + if (X509CertificateLoader.IsPkcs12(fileName)) { - OpenSslPkcs12Reader? pkcs12Reader; - - if (OpenSslPkcs12Reader.TryRead(File.ReadAllBytes(fileName), out pkcs12Reader)) - { - pkcs12Reader.Dispose(); - - return X509ContentType.Pkcs12; - } + return X509ContentType.Pkcs12; } // Unsupported format. diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.Android.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.Android.cs index 962287bc2630e..2f6a4c2cd44b7 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.Android.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.Android.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Generic; using System.Diagnostics; using System.IO; using Microsoft.Win32.SafeHandles; @@ -15,18 +14,29 @@ internal static partial IStorePal FromHandle(IntPtr storeHandle) throw new NotImplementedException($"{nameof(StorePal)}.{nameof(FromHandle)}"); } - private static AndroidCertLoader FromBlob(ReadOnlySpan rawData, SafePasswordHandle password, bool readingFromFile, X509KeyStorageFlags keyStorageFlags) + private static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandle password, bool readingFromFile, X509KeyStorageFlags keyStorageFlags) { Debug.Assert(password != null); X509ContentType contentType = X509Certificate2.GetCertContentType(rawData); - bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); if (contentType == X509ContentType.Pkcs12) { - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile, password.PasswordProvided); - ICertificatePal[] certPals = ReadPkcs12Collection(rawData, password, ephemeralSpecified); - return new AndroidCertLoader(certPals); + try + { + return new CollectionBasedLoader( + X509CertificateLoader.LoadPkcs12Collection( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(readingFromFile, password))); + } + catch (Pkcs12LoadLimitExceededException e) + { + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); + } } else { @@ -112,33 +122,5 @@ internal static partial IStorePal FromSystemStore(string storeName, StoreLocatio string message = SR.Format(SR.Cryptography_X509_StoreCannotCreate, storeName, storeLocation); throw new CryptographicException(message, new PlatformNotSupportedException(message)); } - - private static ICertificatePal[] ReadPkcs12Collection( - ReadOnlySpan rawData, - SafePasswordHandle password, - bool ephemeralSpecified) - { - using (var reader = new AndroidPkcs12Reader()) - { - reader.ParsePkcs12(rawData); - reader.Decrypt(password, ephemeralSpecified); - - ICertificatePal[] certs = new ICertificatePal[reader.GetCertCount()]; - int idx = 0; - foreach (UnixPkcs12Reader.CertAndKey certAndKey in reader.EnumerateAll()) - { - AndroidCertificatePal pal = (AndroidCertificatePal)certAndKey.Cert!; - if (certAndKey.Key != null) - { - pal.SetPrivateKey(AndroidPkcs12Reader.GetPrivateKey(certAndKey.Key)); - } - - certs[idx] = pal; - idx++; - } - - return certs; - } - } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.OpenSsl.cs index a87e89344d895..6e75c900a699a 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.OpenSsl.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.OpenSsl.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics; -using System.IO; using Microsoft.Win32.SafeHandles; namespace System.Security.Cryptography.X509Certificates @@ -22,7 +21,6 @@ internal static partial ILoaderPal FromBlob(ReadOnlySpan rawData, SafePass Debug.Assert(password != null); ICertificatePal? singleCert; - bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); if (OpenSslX509CertificateReader.TryReadX509Der(rawData, out singleCert) || OpenSslX509CertificateReader.TryReadX509Pem(rawData, out singleCert)) @@ -35,30 +33,39 @@ internal static partial ILoaderPal FromBlob(ReadOnlySpan rawData, SafePass } List? certPals; - Exception? openSslException; if (OpenSslPkcsFormatReader.TryReadPkcs7Der(rawData, out certPals) || - OpenSslPkcsFormatReader.TryReadPkcs7Pem(rawData, out certPals) || - OpenSslPkcsFormatReader.TryReadPkcs12(rawData, password, ephemeralSpecified, readingFromFile: false, out certPals, out openSslException)) + OpenSslPkcsFormatReader.TryReadPkcs7Pem(rawData, out certPals)) { Debug.Assert(certPals != null); return ListToLoaderPal(certPals); } - Debug.Assert(openSslException != null); - throw openSslException; + try + { + return new CollectionBasedLoader( + X509CertificateLoader.LoadPkcs12Collection( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(fromFile: false, password))); + } + catch (Pkcs12LoadLimitExceededException e) + { + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); + } } internal static partial 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, ephemeralSpecified); + return FromBio(fileName, bio, password, keyStorageFlags); } } @@ -66,7 +73,7 @@ private static ILoaderPal FromBio( string fileName, SafeBioHandle bio, SafePasswordHandle password, - bool ephemeralSpecified) + X509KeyStorageFlags keyStorageFlags) { int bioPosition = Interop.Crypto.BioTell(bio); Debug.Assert(bioPosition >= 0); @@ -104,29 +111,21 @@ private static ILoaderPal FromBio( return ListToLoaderPal(certPals); } - // Rewind, try again. - OpenSslX509CertificateReader.RewindBio(bio, bioPosition); - - // Capture the exception so in case of failure, the call to BioSeek does not override it. - Exception? openSslException; - byte[] data = File.ReadAllBytes(fileName); - if (OpenSslPkcsFormatReader.TryReadPkcs12(data, password, ephemeralSpecified, readingFromFile: true, out certPals, out openSslException)) + try { - return ListToLoaderPal(certPals); + return new CollectionBasedLoader( + X509CertificateLoader.LoadPkcs12CollectionFromFile( + fileName, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(fromFile: true, password))); } - - // Since we aren't going to finish reading, leaving the buffer where it was when we got - // it seems better than leaving it in some arbitrary other position. - // - // Use BioSeek directly for the last seek attempt, because any failure here should instead - // report the already created (but not yet thrown) exception. - if (Interop.Crypto.BioSeek(bio, bioPosition) < 0) + catch (Pkcs12LoadLimitExceededException e) { - Interop.Crypto.ErrClearError(); + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); } - - Debug.Assert(openSslException != null); - throw openSslException; } internal static partial IExportPal FromCertificate(ICertificatePalCore cert) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.Windows.Import.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.Windows.Import.cs index b34da971931c2..59155e2b76a2e 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.Windows.Import.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.Windows.Import.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using System.IO; using System.Runtime.InteropServices; using Internal.Cryptography; using Microsoft.Win32.SafeHandles; @@ -21,7 +20,7 @@ internal static partial ILoaderPal FromFile(string fileName, SafePasswordHandle return FromBlobOrFile(null, fileName, password, keyStorageFlags); } - private static StorePal FromBlobOrFile(ReadOnlySpan rawData, string? fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags) + private static ILoaderPal FromBlobOrFile(ReadOnlySpan rawData, string? fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags) { Debug.Assert(password != null); @@ -34,9 +33,6 @@ private static StorePal FromBlobOrFile(ReadOnlySpan rawData, string? fileN fixed (char* pFileName = fileName) { Interop.Crypt32.DATA_BLOB blob = new Interop.Crypt32.DATA_BLOB(new IntPtr(pRawData), (uint)(fromFile ? 0 : rawData!.Length)); - bool persistKeySet = (0 != (keyStorageFlags & X509KeyStorageFlags.PersistKeySet)); - Interop.Crypt32.PfxCertStoreFlags certStoreFlags = MapKeyStorageFlags(keyStorageFlags); - void* pvObject = fromFile ? (void*)pFileName : (void*)&blob; Interop.Crypt32.ContentType contentType; @@ -64,46 +60,55 @@ private static StorePal FromBlobOrFile(ReadOnlySpan rawData, string? fileN { certStore.Dispose(); - if (fromFile) - { - rawData = File.ReadAllBytes(fileName!); - } - else - { - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile: false, password.PasswordProvided); - } + X509Certificate2Collection coll; - fixed (byte* pRawData2 = rawData) + try { - Interop.Crypt32.DATA_BLOB blob2 = new Interop.Crypt32.DATA_BLOB(new IntPtr(pRawData2), (uint)rawData!.Length); - certStore = Interop.Crypt32.PFXImportCertStore(ref blob2, password, certStoreFlags); - if (certStore == null || certStore.IsInvalid) + Pkcs12LoaderLimits limits = X509Certificate.GetPkcs12Limits(fromFile, password); + + if (fromFile) { - Exception e = Marshal.GetLastPInvokeError().ToCryptographicException(); - certStore?.Dispose(); - throw e; - } - } + Debug.Assert(fileName is not null); - if (!persistKeySet) - { - // - // If the user did not want us to persist private keys, then we should loop through all - // the certificates in the collection and set our custom CERT_CLR_DELETE_KEY_PROP_ID property - // so the key container will be deleted when the cert contexts will go away. - // - SafeCertContextHandle? pCertContext = null; - while (Interop.crypt32.CertEnumCertificatesInStore(certStore, ref pCertContext)) + coll = X509CertificateLoader.LoadPkcs12CollectionFromFile( + fileName, + password.DangerousGetSpan(), + keyStorageFlags, + limits); + } + else { - Interop.Crypt32.DATA_BLOB nullBlob = new Interop.Crypt32.DATA_BLOB(IntPtr.Zero, 0); - if (!Interop.Crypt32.CertSetCertificateContextProperty(pCertContext, Interop.Crypt32.CertContextPropId.CERT_CLR_DELETE_KEY_PROP_ID, Interop.Crypt32.CertSetPropertyFlags.CERT_SET_PROPERTY_INHIBIT_PERSIST_FLAG, &nullBlob)) - { - Exception e = Marshal.GetLastPInvokeError().ToCryptographicException(); - certStore.Dispose(); - throw e; - } + coll = X509CertificateLoader.LoadPkcs12Collection( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + limits); } } + catch (Pkcs12LoadLimitExceededException e) + { + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); + } + + // The PFX-Collection loader for .NET Framework and .NET Core and .NET 5-8 assigned + // CERT_CLR_DELETE_KEY_PROP_ID on any certificate loaded when PersistKeySet wasn't asserted, + // which was different than the delete-tracking method utilized for single certificate PFX loads. + // + // The property-based approach meant that `new X509Certificate2(someCert.Handle)` would produce a + // second instance that was responsible for deleting the private key, and whenever the first one + // was disposed (or finalized) it would delete the key out from under the second. Since + // X509Certificate2Collection.Find produces clones, this made for some "interesting" interactions. + // + // X509CertificateLoader.LoadPkcs12Collection uses the same .NET/managed-only tracking, without + // setting a property on the native representation. + // + // If, for some reason, we want the old behavior back, we have two choices: + // 1) change it in X509CertificateLoader + // 2) Transform the returned certificates PALs here. + + return new CollectionBasedLoader(coll); } return new StorePal(certStore); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.cs index 54a29a92fd65a..93a00a240eaeb 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics; using Microsoft.Win32.SafeHandles; namespace System.Security.Cryptography.X509Certificates @@ -28,5 +29,36 @@ internal static partial IStorePal FromSystemStore( string storeName, StoreLocation storeLocation, OpenFlags openFlags); + + internal sealed class CollectionBasedLoader : ILoaderPal + { + private X509Certificate2Collection? _coll; + + internal CollectionBasedLoader(X509Certificate2Collection coll) + { + _coll = coll; + } + + public void Dispose() + { + X509Certificate2Collection? coll = _coll; + _coll = null; + + if (coll is not null) + { + foreach (X509Certificate2 cert in coll) + { + cert.Dispose(); + } + } + } + + public void MoveTo(X509Certificate2Collection collection) + { + Debug.Assert(_coll is not null); + collection.AddRange(_coll); + _coll = null; + } + } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.iOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.iOS.cs index edccc0b79e337..112092dcbed6c 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.iOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.iOS.cs @@ -33,7 +33,6 @@ private static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandl return new CertCollectionLoader(certificateList); } - bool ephemeralSpecified = keyStorageFlags.HasFlag(X509KeyStorageFlags.EphemeralKeySet); X509ContentType contentType = AppleCertificatePal.GetDerCertContentType(rawData); if (contentType == X509ContentType.Pkcs7) @@ -45,19 +44,20 @@ private static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandl if (contentType == X509ContentType.Pkcs12) { - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile, password.PasswordProvided); - ApplePkcs12Reader reader = new ApplePkcs12Reader(); - try { - reader.ParsePkcs12(rawData); - reader.Decrypt(password, ephemeralSpecified); - return new ApplePkcs12CertLoader(reader, password); + return new CollectionBasedLoader( + X509CertificateLoader.LoadPkcs12Collection( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(readingFromFile, password))); } - catch + catch (Pkcs12LoadLimitExceededException e) { - reader.Dispose(); - throw; + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.macOS.LoaderPal.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.macOS.LoaderPal.cs index fa00011b419c1..3a9d9e0ff56a4 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.macOS.LoaderPal.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.macOS.LoaderPal.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Security.Cryptography.Apple; -using System.Threading; using Microsoft.Win32.SafeHandles; namespace System.Security.Cryptography.X509Certificates @@ -53,80 +52,5 @@ public void MoveTo(X509Certificate2Collection collection) } } } - - private sealed class ApplePkcs12CertLoader : ILoaderPal - { - private readonly ApplePkcs12Reader _pkcs12; - private readonly SafeKeychainHandle _keychain; - private SafePasswordHandle _password; - private readonly bool _exportable; - - public ApplePkcs12CertLoader( - ApplePkcs12Reader pkcs12, - SafeKeychainHandle keychain, - SafePasswordHandle password, - bool exportable) - { - _pkcs12 = pkcs12; - _keychain = keychain; - _exportable = exportable; - - bool addedRef = false; - password.DangerousAddRef(ref addedRef); - _password = password; - } - - public void Dispose() - { - _pkcs12.Dispose(); - - // Only dispose the keychain if it's a temporary handle. - (_keychain as SafeTemporaryKeychainHandle)?.Dispose(); - - SafePasswordHandle? password = Interlocked.Exchange(ref _password, null!); - password?.DangerousRelease(); - } - - public void MoveTo(X509Certificate2Collection collection) - { - foreach (UnixPkcs12Reader.CertAndKey certAndKey in _pkcs12.EnumerateAll()) - { - AppleCertificatePal pal = (AppleCertificatePal)certAndKey.Cert!; - SafeSecKeyRefHandle? safeSecKeyRefHandle = - ApplePkcs12Reader.GetPrivateKey(certAndKey.Key); - - using (safeSecKeyRefHandle) - { - AppleCertificatePal newPal; - - // SecItemImport doesn't seem to respect non-exportable import for PKCS#8, - // only PKCS#12. - // - // So, as part of reading this PKCS#12 we now need to write the minimum - // PKCS#12 in a normalized form, and ask the OS to import it. - if (!_exportable && safeSecKeyRefHandle != null) - { - newPal = AppleCertificatePal.ImportPkcs12NonExportable( - pal, - safeSecKeyRefHandle, - _password, - _keychain); - } - else - { - newPal = pal.MoveToKeychain(_keychain, safeSecKeyRefHandle) ?? pal; - } - - X509Certificate2 cert = new X509Certificate2(newPal); - collection.Add(cert); - - if (newPal != pal) - { - pal.Dispose(); - } - } - } - } - } } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.macOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.macOS.cs index b424e971b09e4..46fbc922ddb33 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.macOS.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/StorePal.macOS.cs @@ -37,22 +37,21 @@ private static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandl if (contentType == X509ContentType.Pkcs12) { - if ((keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) == X509KeyStorageFlags.EphemeralKeySet) + try { - throw new PlatformNotSupportedException(SR.Cryptography_X509_NoEphemeralPfx); + return new CollectionBasedLoader( + X509CertificateLoader.LoadPkcs12Collection( + rawData, + password.DangerousGetSpan(), + keyStorageFlags, + X509Certificate.GetPkcs12Limits(readingFromFile, password))); + } + catch (Pkcs12LoadLimitExceededException e) + { + throw new CryptographicException( + SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded, + e); } - - X509Certificate.EnforceIterationCountLimit(ref rawData, readingFromFile, password.PasswordProvided); - bool exportable = (keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable; - - bool persist = - (keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet; - - SafeKeychainHandle keychain = persist - ? Interop.AppleCrypto.SecKeychainCopyDefault() - : Interop.AppleCrypto.CreateTemporaryKeychain(); - - return ImportPkcs12(rawData, password, exportable, ephemeralSpecified: false, keychain); } SafeCFArrayHandle certs = Interop.AppleCrypto.X509ImportCollection( @@ -65,29 +64,6 @@ private static ILoaderPal FromBlob(ReadOnlySpan rawData, SafePasswordHandl return new AppleCertLoader(certs, null); } - private static ApplePkcs12CertLoader ImportPkcs12( - ReadOnlySpan rawData, - SafePasswordHandle password, - bool exportable, - bool ephemeralSpecified, - SafeKeychainHandle keychain) - { - ApplePkcs12Reader reader = new ApplePkcs12Reader(); - - try - { - reader.ParsePkcs12(rawData); - reader.Decrypt(password, ephemeralSpecified); - return new ApplePkcs12CertLoader(reader, keychain, password, exportable); - } - catch - { - reader.Dispose(); - keychain.Dispose(); - throw; - } - } - internal static partial ILoaderPal FromFile(string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags) { Debug.Assert(password != null); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/UnixPkcs12Reader.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/UnixPkcs12Reader.cs deleted file mode 100644 index 1f5a24fa15be2..0000000000000 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/UnixPkcs12Reader.cs +++ /dev/null @@ -1,857 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Buffers; -using System.Collections.Generic; -using System.Diagnostics; -using System.Formats.Asn1; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using System.Security.Cryptography.Asn1; -using System.Security.Cryptography.Asn1.Pkcs12; -using System.Security.Cryptography.Asn1.Pkcs7; -using System.Threading; -using Internal.Cryptography; -using Microsoft.Win32.SafeHandles; - -namespace System.Security.Cryptography.X509Certificates -{ - internal abstract class UnixPkcs12Reader : IDisposable - { - private const string DecryptedSentinel = nameof(UnixPkcs12Reader); - private const int ErrorInvalidPasswordHResult = unchecked((int)0x80070056); - - private PfxAsn _pfxAsn; - private ContentInfoAsn[]? _safeContentsValues; - private CertAndKey[]? _certs; - private int _certCount; - private PointerMemoryManager? _tmpManager; - private bool _allowDoubleBind; - - protected abstract ICertificatePalCore ReadX509Der(ReadOnlyMemory data); - protected abstract AsymmetricAlgorithm LoadKey(ReadOnlyMemory safeBagBagValue); - - internal void ParsePkcs12(ReadOnlySpan data) - { - try - { - // RFC7292 specifies BER instead of DER - AsnValueReader reader = new AsnValueReader(data, AsnEncodingRules.BER); - - // Windows compatibility: Ignore trailing data. - ReadOnlySpan encodedData = reader.PeekEncodedValue(); - - unsafe - { - void* tmpPtr = NativeMemory.Alloc((uint)encodedData.Length); - - try - { - Span tmpSpan = new Span((byte*)tmpPtr, encodedData.Length); - encodedData.CopyTo(tmpSpan); - _tmpManager = new PointerMemoryManager(tmpPtr, encodedData.Length); - } - catch - { - NativeMemory.Free(tmpPtr); - throw; - } - } - - ReadOnlyMemory tmpMemory = _tmpManager.Memory; - reader = new AsnValueReader(tmpMemory.Span, AsnEncodingRules.BER); - - PfxAsn.Decode(ref reader, tmpMemory, out PfxAsn pfxAsn); - - if (pfxAsn.AuthSafe.ContentType != Oids.Pkcs7Data) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - _pfxAsn = pfxAsn; - } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - } - - internal CertAndKey GetSingleCert() - { - CertAndKey[]? certs = _certs; - Debug.Assert(certs != null); - - if (_certCount < 1) - { - throw new CryptographicException(SR.Cryptography_Pfx_NoCertificates); - } - - CertAndKey ret; - - for (int i = _certCount - 1; i >= 0; --i) - { - if (certs[i].Key != null) - { - ret = certs[i]; - certs[i] = default; - return ret; - } - } - - ret = certs[_certCount - 1]; - certs[_certCount - 1] = default; - return ret; - } - - internal int GetCertCount() - { - return _certCount; - } - - internal IEnumerable EnumerateAll() - { - while (_certCount > 0) - { - int idx = _certCount - 1; - CertAndKey ret = _certs![idx]; - _certs[idx] = default; - _certCount--; - yield return ret; - } - } - - public void Dispose() - { - // Generally, having a MemoryManager cleaned up in a Dispose is a bad practice. - // In this case, the UnixPkcs12Reader is only ever created in a using statement, - // never accessed by a second thread, and there isn't a manual call to Dispose - // mixed in anywhere outside of an aborted allocation path. - - PointerMemoryManager? manager = _tmpManager; - _tmpManager = null; - - if (manager != null) - { - unsafe - { - Span tmp = manager.GetSpan(); - CryptographicOperations.ZeroMemory(tmp); - NativeMemory.Free(Unsafe.AsPointer(ref MemoryMarshal.GetReference(tmp))); - } - - ((IDisposable)manager).Dispose(); - } - - ContentInfoAsn[]? rentedContents = _safeContentsValues; - CertAndKey[]? rentedCerts = _certs; - _safeContentsValues = null; - _certs = null; - - if (rentedContents != null) - { - ReturnRentedContentInfos(rentedContents); - } - - if (rentedCerts != null) - { - for (int i = _certCount - 1; i >= 0; --i) - { - rentedCerts[i].Dispose(); - } - - ArrayPool.Shared.Return(rentedCerts, clearArray: true); - } - } - - private static void ReturnRentedContentInfos(ContentInfoAsn[] rentedContents) - { - for (int i = 0; i < rentedContents.Length; i++) - { - string contentType = rentedContents[i].ContentType; - - if (contentType == null) - { - break; - } - - if (contentType == DecryptedSentinel) - { - ReadOnlyMemory content = rentedContents[i].Content; - rentedContents[i].Content = default; - - if (!MemoryMarshal.TryGetArray(content, out ArraySegment segment)) - { - Debug.Fail("Couldn't unpack decrypted buffer."); - } - - CryptoPool.Return(segment); - } - } - - ArrayPool.Shared.Return(rentedContents, clearArray: true); - } - - public void Decrypt(SafePasswordHandle password, bool ephemeralSpecified) - { - ReadOnlyMemory authSafeContents = - Helpers.DecodeOctetStringAsMemory(_pfxAsn.AuthSafe.Content); - - _allowDoubleBind = !ephemeralSpecified; - - bool hasRef = false; - - try - { - password.DangerousAddRef(ref hasRef); - ReadOnlySpan passwordChars = password.DangerousGetSpan(); - - if (_pfxAsn.MacData.HasValue) - { - VerifyAndDecrypt(passwordChars, authSafeContents); - } - else if (passwordChars.IsEmpty) - { - try - { - // Try the empty password first. - // If anything goes wrong, try the null password. - // - // The same password has to work for the entirety of the file, - // null and empty aren't interchangeable between parts. - Decrypt("", authSafeContents); - } - catch (CryptographicException) - { - ContentInfoAsn[]? partialSuccess = _safeContentsValues; - _safeContentsValues = null; - - if (partialSuccess != null) - { - ReturnRentedContentInfos(partialSuccess); - } - - Decrypt(null, authSafeContents); - } - } - else - { - Decrypt(passwordChars, authSafeContents); - } - } - catch (Exception e) - { - throw new CryptographicException(SR.Cryptography_Pfx_BadPassword, e) - { - HResult = ErrorInvalidPasswordHResult - }; - } - finally - { - if (hasRef) - { - password.DangerousRelease(); - } - } - } - - private void VerifyAndDecrypt(ReadOnlySpan password, ReadOnlyMemory authSafeContents) - { - Debug.Assert(_pfxAsn.MacData.HasValue); - ReadOnlySpan authSafeSpan = authSafeContents.Span; - - if (password.Length == 0) - { - // VerifyMac produces different answers for the empty string and the null string, - // when the length is 0 try empty first (more common), then null. - if (_pfxAsn.VerifyMac("", authSafeSpan)) - { - Decrypt("", authSafeContents); - return; - } - - if (_pfxAsn.VerifyMac(default, authSafeSpan)) - { - Decrypt(default, authSafeContents); - return; - } - } - else if (_pfxAsn.VerifyMac(password, authSafeSpan)) - { - Decrypt(password, authSafeContents); - return; - } - - throw new CryptographicException(SR.Cryptography_Pfx_BadPassword) - { - HResult = ErrorInvalidPasswordHResult - }; - } - - private void Decrypt(ReadOnlySpan password, ReadOnlyMemory authSafeContents) - { - _safeContentsValues ??= DecodeSafeContents(authSafeContents); - - // The average PFX contains one cert, and one key. - // The next most common PFX contains 3 certs, and one key. - // - // Nothing requires that there be fewer keys than certs, - // but it's sort of nonsensical when loading this way. - CertBagAsn[] certBags = ArrayPool.Shared.Rent(10); - AttributeAsn[]?[] certBagAttrs = ArrayPool.Shared.Rent(10); - SafeBagAsn[] keyBags = ArrayPool.Shared.Rent(10); - RentedSubjectPublicKeyInfo[]? publicKeyInfos = null; - AsymmetricAlgorithm[]? keys = null; - CertAndKey[]? certs = null; - int certBagIdx = 0; - int keyBagIdx = 0; - - try - { - DecryptAndProcessSafeContents( - password, - ref certBags, - ref certBagAttrs, - ref certBagIdx, - ref keyBags, - ref keyBagIdx); - - certs = ArrayPool.Shared.Rent(certBagIdx); - certs.AsSpan().Clear(); - - keys = ArrayPool.Shared.Rent(keyBagIdx); - keys.AsSpan().Clear(); - - publicKeyInfos = ArrayPool.Shared.Rent(keyBagIdx); - publicKeyInfos.AsSpan().Clear(); - - ExtractPrivateKeys(password, keyBags, keyBagIdx, keys, publicKeyInfos); - - BuildCertsWithKeys( - password, - certBags, - certBagAttrs, - certs, - certBagIdx, - keyBags, - publicKeyInfos, - keys, - keyBagIdx); - - _certCount = certBagIdx; - _certs = certs; - } - catch - { - if (certs != null) - { - for (int i = 0; i < certBagIdx; i++) - { - CertAndKey certAndKey = certs[i]; - certAndKey.Dispose(); - } - } - - throw; - } - finally - { - if (keys != null) - { - foreach (AsymmetricAlgorithm key in keys) - { - key?.Dispose(); - } - - ArrayPool.Shared.Return(keys); - } - - if (publicKeyInfos != null) - { - for (int i = 0; i < keyBagIdx; i++) - { - publicKeyInfos[i].Dispose(); - } - - ArrayPool.Shared.Return(publicKeyInfos, clearArray: true); - } - - ArrayPool.Shared.Return(certBags, clearArray: true); - ArrayPool.Shared.Return(certBagAttrs, clearArray: true); - ArrayPool.Shared.Return(keyBags, clearArray: true); - } - } - - private static ContentInfoAsn[] DecodeSafeContents(ReadOnlyMemory authSafeContents) - { - // The expected number of ContentInfoAsns to read is 2, one encrypted (contains certs), - // and one plain (contains encrypted keys) - ContentInfoAsn[] rented = ArrayPool.Shared.Rent(10); - - try - { - AsnValueReader outer = new AsnValueReader(authSafeContents.Span, AsnEncodingRules.BER); - AsnValueReader reader = outer.ReadSequence(); - outer.ThrowIfNotEmpty(); - int i = 0; - - while (reader.HasData) - { - GrowIfNeeded(ref rented, i); - ContentInfoAsn.Decode(ref reader, authSafeContents, out rented[i]); - i++; - } - - rented.AsSpan(i).Clear(); - return rented; - } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - } - - private void DecryptAndProcessSafeContents( - ReadOnlySpan password, - ref CertBagAsn[] certBags, - ref AttributeAsn[]?[] certBagAttrs, - ref int certBagIdx, - ref SafeBagAsn[] keyBags, - ref int keyBagIdx) - { - for (int i = 0; i < _safeContentsValues!.Length; i++) - { - string contentType = _safeContentsValues[i].ContentType; - bool process = false; - - if (contentType == null) - { - break; - } - - // Should enveloped throw here? - if (contentType == Oids.Pkcs7Data) - { - process = true; - } - else if (contentType == Oids.Pkcs7Encrypted) - { - DecryptSafeContents(password, ref _safeContentsValues[i]); - process = true; - } - - if (process) - { - ProcessSafeContents( - _safeContentsValues[i], - ref certBags, - ref certBagAttrs, - ref certBagIdx, - ref keyBags, - ref keyBagIdx); - } - } - } - - private void ExtractPrivateKeys( - ReadOnlySpan password, - SafeBagAsn[] keyBags, - int keyBagIdx, - AsymmetricAlgorithm[] keys, - RentedSubjectPublicKeyInfo[] publicKeyInfos) - { - byte[]? spkiBuf = null; - - for (int i = keyBagIdx - 1; i >= 0; i--) - { - ref RentedSubjectPublicKeyInfo cur = ref publicKeyInfos[i]; - - try - { - SafeBagAsn keyBag = keyBags[i]; - AsymmetricAlgorithm key = LoadKey(keyBag, password); - - int pubLength; - - while (!key.TryExportSubjectPublicKeyInfo(spkiBuf, out pubLength)) - { - byte[]? toReturn = spkiBuf; - spkiBuf = CryptoPool.Rent((toReturn?.Length ?? 128) * 2); - - if (toReturn != null) - { - // public key info doesn't need to be cleared - CryptoPool.Return(toReturn, clearSize: 0); - } - } - - cur.Value = SubjectPublicKeyInfoAsn.Decode( - spkiBuf.AsMemory(0, pubLength), - AsnEncodingRules.DER); - - keys[i] = key; - cur.TrackArray(spkiBuf, clearSize: 0); - spkiBuf = null; - } - catch (CryptographicException) - { - // Windows 10 compatibility: - // If anything goes wrong loading this key, just ignore it. - // If no one ended up needing it, no harm/no foul. - // If this has a LocalKeyId and something references it, then it'll fail. - } - finally - { - if (spkiBuf != null) - { - // Public key data doesn't need to be cleared. - CryptoPool.Return(spkiBuf, clearSize: 0); - } - } - } - } - - private void BuildCertsWithKeys( - ReadOnlySpan password, - CertBagAsn[] certBags, - AttributeAsn[]?[] certBagAttrs, - CertAndKey[] certs, - int certBagIdx, - SafeBagAsn[] keyBags, - RentedSubjectPublicKeyInfo[] publicKeyInfos, - AsymmetricAlgorithm?[] keys, - int keyBagIdx) - { - for (certBagIdx--; certBagIdx >= 0; certBagIdx--) - { - int matchingKeyIdx = -1; - - foreach (AttributeAsn attr in certBagAttrs[certBagIdx] ?? Array.Empty()) - { - if (attr.AttrType == Oids.LocalKeyId && attr.AttrValues.Length > 0) - { - matchingKeyIdx = FindMatchingKey( - keyBags, - keyBagIdx, - Helpers.DecodeOctetStringAsMemory(attr.AttrValues[0]).Span); - - // Only try the first one. - break; - } - } - - ReadOnlyMemory x509Data = - Helpers.DecodeOctetStringAsMemory(certBags[certBagIdx].CertValue); - - certs[certBagIdx].Cert = ReadX509Der(x509Data); - - // If no matching key was found, but there are keys, - // compare SubjectPublicKeyInfo values - if (matchingKeyIdx == -1 && keyBagIdx > 0) - { - ICertificatePalCore cert = certs[certBagIdx].Cert!; - string algorithm = cert.KeyAlgorithm; - byte[] keyParams = cert.KeyAlgorithmParameters; - byte[] keyValue = cert.PublicKeyValue; - - for (int i = 0; i < keyBagIdx; i++) - { - if (PublicKeyMatches(algorithm, keyParams, keyValue, ref publicKeyInfos[i].Value)) - { - matchingKeyIdx = i; - break; - } - } - } - - 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) - { - 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; - } - } - } - } - - private static bool PublicKeyMatches( - string algorithm, - byte[] keyParams, - byte[] keyValue, - ref SubjectPublicKeyInfoAsn publicKeyInfo) - { - if (!publicKeyInfo.SubjectPublicKey.Span.SequenceEqual(keyValue)) - { - return false; - } - - switch (algorithm) - { - case Oids.Rsa: - case Oids.RsaPss: - switch (publicKeyInfo.Algorithm.Algorithm) - { - case Oids.Rsa: - case Oids.RsaPss: - break; - default: - return false; - } - - return - publicKeyInfo.Algorithm.HasNullEquivalentParameters() && - AlgorithmIdentifierAsn.RepresentsNull(keyParams); - case Oids.EcPublicKey: - case Oids.EcDiffieHellman: - switch (publicKeyInfo.Algorithm.Algorithm) - { - case Oids.EcPublicKey: - case Oids.EcDiffieHellman: - break; - default: - return false; - } - - return - publicKeyInfo.Algorithm.Parameters.HasValue && - publicKeyInfo.Algorithm.Parameters.Value.Span.SequenceEqual(keyParams); - } - - if (algorithm != publicKeyInfo.Algorithm.Algorithm) - { - return false; - } - - if (!publicKeyInfo.Algorithm.Parameters.HasValue) - { - return (keyParams?.Length ?? 0) == 0; - } - - return publicKeyInfo.Algorithm.Parameters.Value.Span.SequenceEqual(keyParams); - } - - private static int FindMatchingKey( - SafeBagAsn[] keyBags, - int keyBagCount, - ReadOnlySpan localKeyId) - { - for (int i = 0; i < keyBagCount; i++) - { - foreach (AttributeAsn attr in keyBags[i].BagAttributes ?? Array.Empty()) - { - if (attr.AttrType == Oids.LocalKeyId && attr.AttrValues.Length > 0) - { - ReadOnlyMemory curKeyId = - Helpers.DecodeOctetStringAsMemory(attr.AttrValues[0]); - - if (curKeyId.Span.SequenceEqual(localKeyId)) - { - return i; - } - - break; - } - } - } - - return -1; - } - - private static void DecryptSafeContents( - ReadOnlySpan password, - ref ContentInfoAsn safeContentsAsn) - { - EncryptedDataAsn encryptedData = - EncryptedDataAsn.Decode(safeContentsAsn.Content, AsnEncodingRules.BER); - - // https://tools.ietf.org/html/rfc5652#section-8 - if (encryptedData.Version != 0 && encryptedData.Version != 2) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - // Since the contents are supposed to be the BER-encoding of an instance of - // SafeContents (https://tools.ietf.org/html/rfc7292#section-4.1) that implies the - // content type is simply "data", and that content is present. - if (encryptedData.EncryptedContentInfo.ContentType != Oids.Pkcs7Data) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - if (!encryptedData.EncryptedContentInfo.EncryptedContent.HasValue) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - int encryptedValueLength = encryptedData.EncryptedContentInfo.EncryptedContent.Value.Length; - byte[] destination = CryptoPool.Rent(encryptedValueLength); - int written; - - try - { - written = PasswordBasedEncryption.Decrypt( - encryptedData.EncryptedContentInfo.ContentEncryptionAlgorithm, - password, - default, - encryptedData.EncryptedContentInfo.EncryptedContent.Value.Span, - destination); - } - catch - { - // Clear the whole thing, since we don't know what state we're in. - CryptoPool.Return(destination); - throw; - } - - // The DecryptedSentiel content type value will cause Dispose to return - // `destination` to the pool. - safeContentsAsn.Content = destination.AsMemory(0, written); - safeContentsAsn.ContentType = DecryptedSentinel; - } - - private static void ProcessSafeContents( - in ContentInfoAsn safeContentsAsn, - ref CertBagAsn[] certBags, - ref AttributeAsn[]?[] certBagAttrs, - ref int certBagIdx, - ref SafeBagAsn[] keyBags, - ref int keyBagIdx) - { - ReadOnlyMemory contentData = safeContentsAsn.Content; - - if (safeContentsAsn.ContentType == Oids.Pkcs7Data) - { - contentData = Helpers.DecodeOctetStringAsMemory(contentData); - } - - try - { - AsnValueReader outer = new AsnValueReader(contentData.Span, AsnEncodingRules.BER); - AsnValueReader reader = outer.ReadSequence(); - outer.ThrowIfNotEmpty(); - - while (reader.HasData) - { - SafeBagAsn.Decode(ref reader, contentData, out SafeBagAsn bag); - - if (bag.BagId == Oids.Pkcs12CertBag) - { - CertBagAsn certBag = CertBagAsn.Decode(bag.BagValue, AsnEncodingRules.BER); - - if (certBag.CertId == Oids.Pkcs12X509CertBagType) - { - GrowIfNeeded(ref certBags, certBagIdx); - GrowIfNeeded(ref certBagAttrs, certBagIdx); - certBags[certBagIdx] = certBag; - certBagAttrs[certBagIdx] = bag.BagAttributes; - certBagIdx++; - } - } - else if (bag.BagId == Oids.Pkcs12KeyBag || bag.BagId == Oids.Pkcs12ShroudedKeyBag) - { - GrowIfNeeded(ref keyBags, keyBagIdx); - keyBags[keyBagIdx] = bag; - keyBagIdx++; - } - } - } - catch (AsnContentException e) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding, e); - } - } - - private AsymmetricAlgorithm LoadKey(SafeBagAsn safeBag, ReadOnlySpan password) - { - if (safeBag.BagId == Oids.Pkcs12ShroudedKeyBag) - { - ArraySegment decrypted = KeyFormatHelper.DecryptPkcs8( - password, - safeBag.BagValue, - out int localRead); - - try - { - if (localRead != safeBag.BagValue.Length) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - return LoadKey(decrypted.AsMemory()); - } - finally - { - CryptoPool.Return(decrypted); - } - } - - Debug.Assert(safeBag.BagId == Oids.Pkcs12KeyBag); - return LoadKey(safeBag.BagValue); - } - - private static void GrowIfNeeded(ref T[] array, int idx) - { - T[] oldRent = array; - - if (idx >= oldRent.Length) - { - T[] newRent = ArrayPool.Shared.Rent(oldRent.Length * 2); - Array.Copy(oldRent, 0, newRent, 0, idx); - array = newRent; - ArrayPool.Shared.Return(oldRent, clearArray: true); - } - } - - internal struct CertAndKey - { - internal ICertificatePalCore? Cert; - internal AsymmetricAlgorithm? Key; - - internal void Dispose() - { - Cert?.Dispose(); - Key?.Dispose(); - } - } - - private struct RentedSubjectPublicKeyInfo - { - private byte[]? _rented; - private int _clearSize; - internal SubjectPublicKeyInfoAsn Value; - - internal void TrackArray(byte[]? rented, int clearSize = CryptoPool.ClearAll) - { - Debug.Assert(_rented == null); - - _rented = rented; - _clearSize = clearSize; - } - - public void Dispose() - { - byte[]? rented = Interlocked.Exchange(ref _rented, null); - - if (rented != null) - { - CryptoPool.Return(rented, _clearSize); - } - } - } - } -} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.LegacyLimits.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.LegacyLimits.cs new file mode 100644 index 0000000000000..0149a61deb346 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.LegacyLimits.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Win32.SafeHandles; + +namespace System.Security.Cryptography.X509Certificates +{ + public partial class X509Certificate + { + private static Pkcs12LoaderLimits? s_legacyLimits; + + internal static Pkcs12LoaderLimits GetPkcs12Limits(bool fromFile, SafePasswordHandle safePasswordHandle) + { + if (fromFile || safePasswordHandle.PasswordProvided) + { + return Pkcs12LoaderLimits.DangerousNoLimits; + } + + return (s_legacyLimits ??= MakeLegacyLimits()); + } + + private static Pkcs12LoaderLimits MakeLegacyLimits() + { + // Start with "no limits", then add back the ones we had from before X509CertificateLoader. + Pkcs12LoaderLimits limits = new Pkcs12LoaderLimits(Pkcs12LoaderLimits.DangerousNoLimits) + { + MacIterationLimit = 600_000, + IndividualKdfIterationLimit = 600_000, + }; + + long totalKdfLimit = LocalAppContextSwitches.Pkcs12UnspecifiedPasswordIterationLimit; + + if (totalKdfLimit == -1) + { + limits.TotalKdfIterationLimit = null; + } + else if (totalKdfLimit < 0) + { + limits.TotalKdfIterationLimit = LocalAppContextSwitches.DefaultPkcs12UnspecifiedPasswordIterationLimit; + } + else + { + limits.TotalKdfIterationLimit = (int)long.Min(int.MaxValue, totalKdfLimit); + } + + limits.MakeReadOnly(); + return limits; + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs index d010f6f21c96b..d2be71772e589 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Certificate.cs @@ -1,22 +1,19 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Buffers; using System.ComponentModel; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Formats.Asn1; using System.Globalization; using System.Runtime.Serialization; using System.Runtime.Versioning; -using System.Security.Cryptography.Asn1.Pkcs12; using System.Text; using Internal.Cryptography; using Microsoft.Win32.SafeHandles; namespace System.Security.Cryptography.X509Certificates { - public class X509Certificate : IDisposable, IDeserializationCallback, ISerializable + public partial class X509Certificate : IDisposable, IDeserializationCallback, ISerializable { private volatile byte[]? _lazyCertHash; private volatile string? _lazyIssuer; @@ -670,125 +667,15 @@ protected static string FormatDate(DateTime date) return date.ToString(culture); } - internal static void ValidateKeyStorageFlags(X509KeyStorageFlags keyStorageFlags) - { - if ((keyStorageFlags & ~KeyStorageFlagsAll) != 0) - throw new ArgumentException(SR.Argument_InvalidFlag, nameof(keyStorageFlags)); - - const X509KeyStorageFlags EphemeralPersist = - X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.PersistKeySet; - - X509KeyStorageFlags persistenceFlags = keyStorageFlags & EphemeralPersist; - - if (persistenceFlags == EphemeralPersist) - { - throw new ArgumentException( - SR.Format(SR.Cryptography_X509_InvalidFlagCombination, persistenceFlags), - nameof(keyStorageFlags)); - } - } +#pragma warning disable CA1416 // Not callable on browser. + internal static void ValidateKeyStorageFlags(X509KeyStorageFlags keyStorageFlags) => + X509CertificateLoader.ValidateKeyStorageFlags(keyStorageFlags); +#pragma warning restore CA1416 private static void VerifyContentType(X509ContentType contentType) { if (!(contentType == X509ContentType.Cert || contentType == X509ContentType.SerializedCert || contentType == X509ContentType.Pkcs12)) throw new CryptographicException(SR.Cryptography_X509_InvalidContentType); } - - internal static void EnforceIterationCountLimit(ref ReadOnlySpan pkcs12, bool readingFromFile, bool passwordProvided) - { - if (readingFromFile || passwordProvided) - { - return; - } - - long pkcs12UnspecifiedPasswordIterationLimit = LocalAppContextSwitches.Pkcs12UnspecifiedPasswordIterationLimit; - - // -1 = no limit - if (LocalAppContextSwitches.Pkcs12UnspecifiedPasswordIterationLimit == -1) - { - return; - } - - // any other negative number means use default limits - if (pkcs12UnspecifiedPasswordIterationLimit < 0) - { - pkcs12UnspecifiedPasswordIterationLimit = LocalAppContextSwitches.DefaultPkcs12UnspecifiedPasswordIterationLimit; - } - - try - { - try - { - checked - { - KdfWorkLimiter.SetIterationLimit((ulong)pkcs12UnspecifiedPasswordIterationLimit); - ulong observedIterationCount = GetIterationCount(pkcs12, out int bytesConsumed); - pkcs12 = pkcs12.Slice(0, bytesConsumed); - - // Check both conditions: we want a KDF-exceeded failure anywhere in the system to produce a failure here. - // There are some places within the GetIterationCount method where we optimistically try processing the - // PFX in one manner, and if we see failures we'll swallow any exceptions and try a different manner - // instead. The problem with this is that when we swallow failures, we don't have the ability to add the - // so-far-observed iteration count back to the running total returned by GetIterationCount. This - // potentially allows a clever adversary a window through which to squeeze in work beyond our configured - // limits. To mitigate this risk, we'll fail now if we observed *any* KDF-exceeded failure while processing - // this PFX. - if (observedIterationCount > (ulong)pkcs12UnspecifiedPasswordIterationLimit || KdfWorkLimiter.WasWorkLimitExceeded()) - { - throw new X509IterationCountExceededException(); // iteration count exceeded - } - } - } - finally - { - KdfWorkLimiter.ResetIterationLimit(); - } - } - catch (X509IterationCountExceededException) - { - throw new CryptographicException(SR.Cryptography_X509_PfxWithoutPassword_MaxAllowedIterationsExceeded); - } - catch (Exception ex) - { - // It's important for this catch-all block to be *outside* the inner try/finally - // so that we can prevent exception filters from running before we've had a chance - // to clean up the threadstatic. - throw new CryptographicException(SR.Cryptography_X509_PfxWithoutPassword_ProblemFound, ex); - } - } - - internal static ulong GetIterationCount(ReadOnlySpan pkcs12, out int bytesConsumed) - { - ulong iterations; - - unsafe - { - fixed (byte* pin = pkcs12) - { - using (var manager = new PointerMemoryManager(pin, pkcs12.Length)) - { - AsnValueReader reader = new AsnValueReader(pkcs12, AsnEncodingRules.BER); - int encodedLength = reader.PeekEncodedValue().Length; - PfxAsn.Decode(ref reader, manager.Memory, out PfxAsn pfx); - - // Don't throw when trailing data is present. - // Windows doesn't have such enforcement as well. - - iterations = pfx.CountTotalIterations(); - bytesConsumed = encodedLength; - } - } - } - - return iterations; - } - - internal const X509KeyStorageFlags KeyStorageFlagsAll = - X509KeyStorageFlags.UserKeySet | - X509KeyStorageFlags.MachineKeySet | - X509KeyStorageFlags.Exportable | - X509KeyStorageFlags.UserProtected | - X509KeyStorageFlags.PersistKeySet | - X509KeyStorageFlags.EphemeralKeySet; } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Android.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Android.cs new file mode 100644 index 0000000000000..8a1da9bc03d53 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Android.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Formats.Asn1; +using System.IO; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + private static partial ICertificatePal LoadCertificatePal(ReadOnlySpan data) + { + if (!AndroidCertificatePal.TryReadX509(data, out ICertificatePal? cert)) + { + cert?.Dispose(); + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return cert; + } + + private static partial ICertificatePal LoadCertificatePalFromFile(string path) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + using (FileStream stream = File.OpenRead(path)) + { + int length = (int)long.Min(int.MaxValue, stream.Length); + byte[] buf = CryptoPool.Rent(length); + + try + { + stream.ReadAtLeast(buf, length); + return LoadCertificatePal(buf.AsSpan(0, length)); + } + finally + { + CryptoPool.Return(buf, length); + } + } + } + + static partial void ValidatePlatformKeyStorageFlags(X509KeyStorageFlags keyStorageFlags) + { + if ((keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet) + { + throw new PlatformNotSupportedException(SR.Cryptography_X509_PKCS12_PersistKeySetNotSupported); + } + } + + private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, ImportState importState) + { + AndroidCertificatePal pal = (AndroidCertificatePal)certAndKey.Cert!; + + if (certAndKey.Key != null) + { + pal.SetPrivateKey(GetPrivateKey(certAndKey.Key)); + certAndKey.Key.Dispose(); + } + + return new Pkcs12Return(pal); + } + + private static partial AsymmetricAlgorithm? CreateKey(string algorithm) + { + return algorithm switch + { + Oids.Rsa or Oids.RsaPss => new RSAImplementation.RSAAndroid(), + Oids.EcPublicKey or Oids.EcDiffieHellman => new ECDsaImplementation.ECDsaAndroid(), + Oids.Dsa => new DSAImplementation.DSAAndroid(), + _ => null, + }; + } + + internal static SafeKeyHandle GetPrivateKey(AsymmetricAlgorithm key) + { + if (key is ECDsaImplementation.ECDsaAndroid ecdsa) + { + return ecdsa.DuplicateKeyHandle(); + } + + if (key is RSAImplementation.RSAAndroid rsa) + { + return rsa.DuplicateKeyHandle(); + } + + if (key is DSAImplementation.DSAAndroid dsa) + { + return dsa.DuplicateKeyHandle(); + } + + throw new NotImplementedException($"{nameof(GetPrivateKey)} ({key.GetType()})"); + } + + private static partial ICertificatePalCore LoadX509Der(ReadOnlyMemory data) + { + ReadOnlySpan span = data.Span; + + AsnValueReader reader = new AsnValueReader(span, AsnEncodingRules.DER); + reader.ReadSequence(); + reader.ThrowIfNotEmpty(); + + if (!AndroidCertificatePal.TryReadX509(span, out ICertificatePal? cert)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return cert; + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.NotSupported.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.NotSupported.cs new file mode 100644 index 0000000000000..95e2f393d6940 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.NotSupported.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + private static partial ICertificatePal LoadCertificatePal(ReadOnlySpan data) + { + throw new PlatformNotSupportedException(SR.SystemSecurityCryptographyX509Certificates_PlatformNotSupported); + } + + private static partial ICertificatePal LoadCertificatePalFromFile(string path) + { + throw new PlatformNotSupportedException(SR.SystemSecurityCryptographyX509Certificates_PlatformNotSupported); + } + + private static partial Pkcs12Return LoadPkcs12( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + throw new PlatformNotSupportedException(SR.SystemSecurityCryptographyX509Certificates_PlatformNotSupported); + } + + private static partial X509Certificate2Collection LoadPkcs12Collection( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + throw new PlatformNotSupportedException(SR.SystemSecurityCryptographyX509Certificates_PlatformNotSupported); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.OpenSsl.cs new file mode 100644 index 0000000000000..1873f3ea934ac --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.OpenSsl.cs @@ -0,0 +1,98 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Microsoft.Win32.SafeHandles; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + private static partial ICertificatePal LoadCertificatePal(ReadOnlySpan data) + { + ICertificatePal? pal; + + if (OpenSslX509CertificateReader.TryReadX509Der(data, out pal) || + OpenSslX509CertificateReader.TryReadX509Pem(data, out pal)) + { + return pal; + } + + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + private static partial ICertificatePal LoadCertificatePalFromFile(string path) + { + ICertificatePal? pal; + + using (SafeBioHandle fileBio = Interop.Crypto.BioNewFile(path, "rb")) + { + Interop.Crypto.CheckValidOpenSslHandle(fileBio); + + int bioPosition = Interop.Crypto.BioTell(fileBio); + Debug.Assert(bioPosition >= 0); + + if (!OpenSslX509CertificateReader.TryReadX509Der(fileBio, out pal)) + { + OpenSslX509CertificateReader.RewindBio(fileBio, bioPosition); + + if (!OpenSslX509CertificateReader.TryReadX509Pem(fileBio, out pal)) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } + } + + return pal; + } + + private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, ImportState importState) + { + OpenSslX509CertificateReader pal = (OpenSslX509CertificateReader)certAndKey.Cert!; + + if (certAndKey.Key is not null) + { + pal.SetPrivateKey(GetPrivateKey(certAndKey.Key)); + certAndKey.Key.Dispose(); + } + + return new Pkcs12Return(pal); + } + + private static partial AsymmetricAlgorithm? CreateKey(string algorithm) + { + return algorithm switch + { + Oids.Rsa or Oids.RsaPss => new RSAOpenSsl(), + Oids.EcPublicKey or Oids.EcDiffieHellman => new ECDiffieHellmanOpenSsl(), + Oids.Dsa => new DSAOpenSsl(), + _ => null, + }; + } + + internal static SafeEvpPKeyHandle GetPrivateKey(AsymmetricAlgorithm key) + { + if (key is RSAOpenSsl rsa) + { + return rsa.DuplicateKeyHandle(); + } + + if (key is DSAOpenSsl dsa) + { + return dsa.DuplicateKeyHandle(); + } + + return ((ECDiffieHellmanOpenSsl)key).DuplicateKeyHandle(); + } + + private static partial ICertificatePalCore LoadX509Der(ReadOnlyMemory data) + { + if (OpenSslX509CertificateReader.TryReadX509Der(data.Span, out ICertificatePal? ret)) + { + return ret; + } + + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Unix.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Unix.cs new file mode 100644 index 0000000000000..f851d163fc8fc --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Unix.cs @@ -0,0 +1,620 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Formats.Asn1; +using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs12; +using Internal.Cryptography; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, ImportState importState); + + private static partial AsymmetricAlgorithm? CreateKey(string algorithm); + + private static partial ICertificatePalCore LoadX509Der(ReadOnlyMemory data); + + static partial void InitializeImportState(ref ImportState importState, X509KeyStorageFlags keyStorageFlags); + + private static partial Pkcs12Return LoadPkcs12( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + bool ephemeral = (keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) != 0; + + CertKeyMatcher matcher = default; + CertAndKey[]? certsAndKeys = null; + ImportState importState = default; + + try + { + matcher.LoadCerts(ref bagState); + matcher.LoadKeys(ref bagState); + + // Windows compat: Don't allow double-bind for EphemeralKeySet loads. + certsAndKeys = matcher.MatchCertAndKeys(ref bagState, !ephemeral); + + int matchIndex; + + for (matchIndex = bagState.CertCount - 1; matchIndex >= 0; matchIndex--) + { + if (certsAndKeys[matchIndex].Key is not null) + { + break; + } + } + + if (matchIndex < 0) + { + matchIndex = bagState.CertCount - 1; + } + + Debug.Assert(matchIndex >= 0); + + InitializeImportState(ref importState, keyStorageFlags); + Pkcs12Return ret = FromCertAndKey(certsAndKeys[matchIndex], importState); + certsAndKeys[matchIndex] = default; + + return ret; + } + finally + { + if (certsAndKeys is not null) + { + CertKeyMatcher.Free(certsAndKeys, bagState.CertCount); + } + + importState.Dispose(); + matcher.Dispose(); + } + } + + private static partial X509Certificate2Collection LoadPkcs12Collection( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + bool ephemeral = (keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) != 0; + + CertKeyMatcher matcher = default; + CertAndKey[]? certsAndKeys = null; + ImportState importState = default; + + try + { + matcher.LoadCerts(ref bagState); + matcher.LoadKeys(ref bagState); + + // Windows compat: Don't allow double-bind for EphemeralKeySet loads. + certsAndKeys = matcher.MatchCertAndKeys(ref bagState, !ephemeral); + + InitializeImportState(ref importState, keyStorageFlags); + + X509Certificate2Collection coll = new X509Certificate2Collection(); + + for (int i = bagState.CertCount - 1; i >= 0; i--) + { + coll.Add(FromCertAndKey(certsAndKeys[i], importState).ToCertificate()); + certsAndKeys[i] = default; + } + + return coll; + } + finally + { + if (certsAndKeys is not null) + { + CertKeyMatcher.Free(certsAndKeys, bagState.CertCount); + } + + importState.Dispose(); + matcher.Dispose(); + } + } + + internal static unsafe bool IsPkcs12(ReadOnlySpan data) + { + if (data.IsEmpty) + { + return false; + } + + fixed (byte* ptr = data) + { + using (PointerMemoryManager manager = new(ptr, data.Length)) + { + try + { + ReadOnlyMemory memory = manager.Memory; + AsnValueReader reader = new AsnValueReader(memory.Span, AsnEncodingRules.BER); + PfxAsn.Decode(ref reader, memory, out _); + return true; + } + catch (AsnContentException) + { + } + catch (CryptographicException) + { + } + + return false; + } + } + } + + internal static bool IsPkcs12(string path) + { + Debug.Assert(!string.IsNullOrEmpty(path)); + + (byte[]? rented, int length, MemoryManager? manager) = ReadAllBytesIfBerSequence(path); + + try + { + ReadOnlyMemory memory = manager?.Memory ?? new ReadOnlyMemory(rented, 0, length); + + AsnValueReader reader = new AsnValueReader(memory.Span, AsnEncodingRules.BER); + PfxAsn.Decode(ref reader, memory, out _); + return true; + } + catch (AsnContentException) + { + } + catch (CryptographicException) + { + } + finally + { + (manager as IDisposable)?.Dispose(); + + if (rented is not null) + { + CryptoPool.Return(rented, length); + } + } + + return false; + } + + private partial struct BagState + { + internal ReadOnlySpan GetCertsSpan() + { + return new ReadOnlySpan(_certBags, 0, _certCount); + } + + internal ReadOnlySpan GetKeysSpan() + { + return new ReadOnlySpan(_keyBags, 0, _keyCount); + } + } + + private struct CertAndKey + { + internal ICertificatePalCore? Cert; + internal AsymmetricAlgorithm? Key; + + internal void Dispose() + { + Cert?.Dispose(); + Key?.Dispose(); + } + } + + private struct CertKeyMatcher + { + private CertAndKey[] _certAndKeys; + private int _certCount; + private AsymmetricAlgorithm?[] _keys; + private RentedSubjectPublicKeyInfo[] _rentedSpki; + private int _keyCount; + + internal void LoadCerts(ref BagState bagState) + { + if (bagState.CertCount == 0) + { + return; + } + + _certAndKeys = ArrayPool.Shared.Rent(bagState.CertCount); + + foreach (SafeBagAsn safeBag in bagState.GetCertsSpan()) + { + Debug.Assert(safeBag.BagId == Oids.Pkcs12CertBag); + + CertBagAsn certBag = CertBagAsn.Decode(safeBag.BagValue, AsnEncodingRules.BER); + + // Non-X.509 cert-type bags should have already been removed. + Debug.Assert(certBag.CertId == Oids.Pkcs12X509CertBagType); + ReadOnlyMemory certData = Helpers.DecodeOctetStringAsMemory(certBag.CertValue); + + ICertificatePalCore pal = LoadX509Der(certData); + Debug.Assert(pal is not null); + + _certAndKeys[_certCount].Cert = pal; + _certCount++; + } + } + + internal void LoadKeys(ref BagState bagState) + { + if (bagState.KeyCount == 0) + { + return; + } + + _keys = ArrayPool.Shared.Rent(bagState.KeyCount); + + foreach (SafeBagAsn safeBag in bagState.GetKeysSpan()) + { + AsymmetricAlgorithm? key = null; + + try + { + if (safeBag.BagId == Oids.Pkcs12KeyBag) + { + PrivateKeyInfoAsn privateKeyInfo = + PrivateKeyInfoAsn.Decode(safeBag.BagValue, AsnEncodingRules.BER); + + key = CreateKey(privateKeyInfo.PrivateKeyAlgorithm.Algorithm); + + if (key is not null) + { + ImportPrivateKey(key, safeBag.BagValue.Span); + + if (_rentedSpki is null) + { + _rentedSpki = + ArrayPool.Shared.Rent(bagState.KeyCount); + _rentedSpki.AsSpan().Clear(); + } + + ExtractPublicKey(ref _rentedSpki[_keyCount], key, safeBag.BagValue.Length); + } + } + else + { + // There may still be shrouded bags in the state, signifying that + // decryption failed for them. They get ignored, unless matched + // by keyId, which produces a failure. + // + // If there's any other kind of bag here, there's a mismatch between + // this code and the main extractor. + Debug.Assert(safeBag.BagId == Oids.Pkcs12ShroudedKeyBag); + } + } + catch (AsnContentException) + { + key?.Dispose(); + key = null; + } + catch (CryptographicException) + { + key?.Dispose(); + key = null; + } + + if (key is not null) + { + _keys[_keyCount] = key; + } + + _keyCount++; + } + } + + internal CertAndKey[] MatchCertAndKeys(ref BagState bagState, bool allowDoubleBind) + { + ReadOnlySpan certBags = bagState.GetCertsSpan(); + ReadOnlySpan keyBags = bagState.GetKeysSpan(); + + for (int certBagIdx = certBags.Length - 1; certBagIdx >= 0; certBagIdx--) + { + int matchingKeyIdx = -1; + + foreach (AttributeAsn attr in certBags[certBagIdx].BagAttributes ?? Array.Empty()) + { + if (attr.AttrType == Oids.LocalKeyId && attr.AttrValues.Length > 0) + { + matchingKeyIdx = FindMatchingKey( + keyBags, + Helpers.DecodeOctetStringAsMemory(attr.AttrValues[0]).Span); + + // Only try the first one. + break; + } + } + + ICertificatePalCore cert = _certAndKeys[certBagIdx].Cert!; + + // If no matching key was found, but there are keys, + // compare SubjectPublicKeyInfo values + if (matchingKeyIdx == -1 && _rentedSpki is not null) + { + for (int i = 0; i < keyBags.Length; i++) + { + if (PublicKeyMatches(cert, ref _rentedSpki[i].Value)) + { + matchingKeyIdx = i; + break; + } + } + } + + 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] is null) + { + // The key could be null because we already matched it (and made it null), + // or because it never loaded. + SafeBagAsn keyBag = keyBags[matchingKeyIdx]; + + if (keyBag.BagId != Oids.Pkcs12KeyBag) + { + // The key bag didn't get transformed. + // That means the password didn't decrypt it. + + if (bagState.ConfirmedPassword) + { + throw new CryptographicException(SR.Cryptography_Pfx_BadKeyReference); + } + + throw new CryptographicException(SR.Cryptography_Pfx_BadPassword) + { + HResult = ERROR_INVALID_PASSWORD, + }; + } + + if (allowDoubleBind) + { + + AsymmetricAlgorithm? key = CreateKey(cert.KeyAlgorithm); + + if (key is null) + { + // The key is actually an algorithm that isn't supported... + + throw new CryptographicException( + SR.Cryptography_UnknownAlgorithmIdentifier, + cert.KeyAlgorithm); + } + + _certAndKeys[certBagIdx].Key = key; + ImportPrivateKey(key, keyBag.BagValue.Span); + } + else + { + throw new CryptographicException(SR.Cryptography_Pfx_BadKeyReference); + } + } + else + { + _certAndKeys[certBagIdx].Key = _keys[matchingKeyIdx]; + _keys[matchingKeyIdx] = null; + } + } + } + + CertAndKey[] ret = _certAndKeys; + _certAndKeys = null!; + return ret; + } + + private static int FindMatchingKey( + ReadOnlySpan keyBags, + ReadOnlySpan localKeyId) + { + for (int i = 0; i < keyBags.Length; i++) + { + foreach (AttributeAsn attr in keyBags[i].BagAttributes ?? Array.Empty()) + { + if (attr.AttrType == Oids.LocalKeyId && attr.AttrValues.Length > 0) + { + ReadOnlyMemory curKeyId = + Helpers.DecodeOctetStringAsMemory(attr.AttrValues[0]); + + if (curKeyId.Span.SequenceEqual(localKeyId)) + { + return i; + } + + break; + } + } + } + + return -1; + } + + private static bool PublicKeyMatches( + ICertificatePalCore cert, + ref SubjectPublicKeyInfoAsn publicKeyInfo) + { + string certAlgorithm = cert.KeyAlgorithm; + string keyAlgorithm = publicKeyInfo.Algorithm.Algorithm; + + bool algorithmMatches = certAlgorithm switch + { + // RSA/RSA-PSS are interchangeable + Oids.Rsa or Oids.Rsa => + keyAlgorithm is Oids.Rsa or Oids.RsaPss, + + // id-ecPublicKey and id-ecDH are interchangeable + Oids.EcPublicKey or Oids.EcDiffieHellman => + keyAlgorithm is Oids.EcPublicKey or Oids.EcDiffieHellman, + + // Everything else is an exact match. + _ => certAlgorithm.Equals(keyAlgorithm, StringComparison.Ordinal), + }; + + if (!algorithmMatches) + { + return false; + } + + // Both cert.PublicKeyValue and cert.KeyAlgorithmParameters use memoization + // on all applicable platforms, but they are still worth deferring past the + // algorithm family check. Once they need to be queried at all, + // PublicKeyValue is more likely to be distinct, so query it first. + + byte[] certEncodedKeyValue = cert.PublicKeyValue; + + if (!publicKeyInfo.SubjectPublicKey.Span.SequenceEqual(certEncodedKeyValue)) + { + return false; + } + + byte[] certKeyParameters = cert.KeyAlgorithmParameters; + + switch (certAlgorithm) + { + // Accept either DER-NULL or missing for RSA algorithm parameters + case Oids.Rsa: + case Oids.RsaPss: + return + publicKeyInfo.Algorithm.HasNullEquivalentParameters() && + AlgorithmIdentifierAsn.RepresentsNull(certKeyParameters); + + // For ECC the parameters are required, and must match exactly. + case Oids.EcPublicKey: + case Oids.EcDiffieHellman: + return + publicKeyInfo.Algorithm.Parameters.HasValue && + publicKeyInfo.Algorithm.Parameters.Value.Span.SequenceEqual(certKeyParameters); + } + + // Any other algorithm matches null/empty parameters as equivalent + if (!publicKeyInfo.Algorithm.Parameters.HasValue) + { + return (certKeyParameters?.Length ?? 0) == 0; + } + + return publicKeyInfo.Algorithm.Parameters.Value.Span.SequenceEqual(certKeyParameters); + } + + private static void ExtractPublicKey( + ref RentedSubjectPublicKeyInfo spki, + AsymmetricAlgorithm key, + int sizeHint) + { + Debug.Assert(sizeHint > 0); + + byte[] buf = CryptoPool.Rent(sizeHint); + int written; + + while (!key.TryExportSubjectPublicKeyInfo(buf, out written)) + { + sizeHint = checked(buf.Length * 2); + CryptoPool.Return(buf); + buf = CryptoPool.Rent(sizeHint); + } + + spki.TrackArray(buf, written); + + spki.Value = SubjectPublicKeyInfoAsn.Decode( + buf.AsMemory(0, written), + AsnEncodingRules.BER); + } + + internal static void Free(CertAndKey[] certAndKeys, int count) + { + for (int i = count - 1; i >= 0; i--) + { + certAndKeys[i].Dispose(); + } + + ArrayPool.Shared.Return(certAndKeys, clearArray: true); + } + + internal void Dispose() + { + if (_certAndKeys is not null) + { + Free(_certAndKeys, _certCount); + } + + if (_keys is not null) + { + for (int i = _keyCount - 1; i >= 0; i--) + { + _keys[i]?.Dispose(); + } + + ArrayPool.Shared.Return(_keys, clearArray: true); + } + + if (_rentedSpki is not null) + { + for (int i = _keyCount - 1; i >= 0; i--) + { + _rentedSpki[i].Dispose(); + } + } + + this = default; + } + + private static void ImportPrivateKey(AsymmetricAlgorithm key, ReadOnlySpan pkcs8) + { + try + { + key.ImportPkcs8PrivateKey(pkcs8, out int bytesRead); + + // The key should have already been run through PrivateKeyInfoAsn.Decode, + // verifying no trailing data. + Debug.Assert(bytesRead == pkcs8.Length); + } + catch (PlatformNotSupportedException nse) + { + // Turn a "curve not supported" PNSE (or other PNSE) + // into a standardized CryptographicException. + throw new CryptographicException(SR.Cryptography_NotValidPrivateKey, nse); + } + } + } + + private struct RentedSubjectPublicKeyInfo + { + private byte[]? _rented; + private int _clearSize; + internal SubjectPublicKeyInfoAsn Value; + + internal void TrackArray(byte[]? rented, int clearSize = CryptoPool.ClearAll) + { + Debug.Assert(_rented is null); + + _rented = rented; + _clearSize = clearSize; + } + + internal void Dispose() + { + byte[]? rented = _rented; + _rented = null; + + if (rented != null) + { + CryptoPool.Return(rented, _clearSize); + } + } + } + + private partial struct ImportState + { + partial void DisposeCore(); + + internal void Dispose() + { + DisposeCore(); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Windows.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Windows.cs new file mode 100644 index 0000000000000..346e8062d3456 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.Windows.cs @@ -0,0 +1,321 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Runtime.InteropServices; +using Internal.Cryptography; +using Microsoft.Win32.SafeHandles; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + private static partial ICertificatePal LoadCertificatePal(ReadOnlySpan data) + { + unsafe + { + fixed (byte* dataPtr = data) + { + Interop.Crypt32.DATA_BLOB blob = new Interop.Crypt32.DATA_BLOB( + (IntPtr)dataPtr, + (uint)data.Length); + + return LoadCertificate( + Interop.Crypt32.CertQueryObjectType.CERT_QUERY_OBJECT_BLOB, + &blob); + } + } + } + + private static partial ICertificatePal LoadCertificatePalFromFile(string path) + { + unsafe + { + fixed (char* pathPtr = path) + { + return LoadCertificate( + Interop.Crypt32.CertQueryObjectType.CERT_QUERY_OBJECT_FILE, + pathPtr); + } + } + } + + static partial void LoadPkcs12NoLimits( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + ref Pkcs12Return earlyReturn) + { + bool deleteKeyContainer = ShouldDeleteKeyContainer(keyStorageFlags); + + using (SafeCertStoreHandle storeHandle = ImportPfx(data.Span, password, keyStorageFlags)) + { + CertificatePal pal = LoadPkcs12(storeHandle, deleteKeyContainer); + earlyReturn = new Pkcs12Return(pal); + } + } + + static partial void LoadPkcs12NoLimits( + ReadOnlyMemory data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + ref X509Certificate2Collection? earlyReturn) + { + bool deleteKeyContainers = ShouldDeleteKeyContainer(keyStorageFlags); + + using (SafeCertStoreHandle storeHandle = ImportPfx(data.Span, password, keyStorageFlags)) + { + earlyReturn = LoadPkcs12Collection(storeHandle, deleteKeyContainers); + } + } + + private static partial Pkcs12Return LoadPkcs12( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + bool deleteKeyContainer = ShouldDeleteKeyContainer(keyStorageFlags); + + using (SafeCertStoreHandle storeHandle = ImportPfx(ref bagState, password, keyStorageFlags)) + { + CertificatePal pal = LoadPkcs12(storeHandle, deleteKeyContainer); + return new Pkcs12Return(pal); + } + } + + private static CertificatePal LoadPkcs12( + SafeCertStoreHandle storeHandle, + bool deleteKeyContainer) + { + // Find the first cert with private key. If none, then simply take the very first cert. + // Along the way, delete the persisted keys of any cert we don't accept. + SafeCertContextHandle? bestCert = null; + SafeCertContextHandle? nextCert = null; + bool havePrivKey = false; + + while (Interop.crypt32.CertEnumCertificatesInStore(storeHandle, ref nextCert)) + { + Debug.Assert(nextCert is not null); + Debug.Assert(!nextCert.IsInvalid); + + if (nextCert.ContainsPrivateKey) + { + if (bestCert is not null && bestCert.ContainsPrivateKey) + { + // We already found our chosen one. Free up this one's key and move on. + + // If this one has a persisted private key, clean up the key file. + // If it was an ephemeral private key no action is required. + if (nextCert.HasPersistedPrivateKey) + { + SafeCertContextHandleWithKeyContainerDeletion.DeleteKeyContainer(nextCert); + } + } + else + { + // Found our first cert that has a private key. + // + // Set it up as our chosen one but keep iterating + // as we need to free up the keys of any remaining certs. + bestCert?.Dispose(); + bestCert = nextCert.Duplicate(); + havePrivKey = true; + } + } + else + { + // Doesn't have a private key but hang on to it anyway, + // in case we don't find any certs with a private key. + bestCert ??= nextCert.Duplicate(); + } + } + + if (bestCert is null) + { + throw new CryptographicException(SR.Cryptography_Pfx_NoCertificates); + } + + bool deleteThisKeyContainer = havePrivKey && deleteKeyContainer; + CertificatePal pal = new CertificatePal(bestCert, deleteThisKeyContainer); + return pal; + } + + private static partial X509Certificate2Collection LoadPkcs12Collection( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + bool deleteKeyContainers = ShouldDeleteKeyContainer(keyStorageFlags); + + using (SafeCertStoreHandle storeHandle = ImportPfx(ref bagState, password, keyStorageFlags)) + { + return LoadPkcs12Collection(storeHandle, deleteKeyContainers); + } + } + + private static X509Certificate2Collection LoadPkcs12Collection( + SafeCertStoreHandle storeHandle, + bool deleteKeyContainers) + { + X509Certificate2Collection coll = new X509Certificate2Collection(); + SafeCertContextHandle? nextCert = null; + + while (Interop.crypt32.CertEnumCertificatesInStore(storeHandle, ref nextCert)) + { + Debug.Assert(nextCert is not null); + Debug.Assert(!nextCert.IsInvalid); + + bool deleteThis = deleteKeyContainers && nextCert.HasPersistedPrivateKey; + CertificatePal pal = new CertificatePal(nextCert.Duplicate(), deleteThis); + coll.Add(new X509Certificate2(pal)); + } + + return coll; + } + + private static unsafe CertificatePal LoadCertificate( + Interop.Crypt32.CertQueryObjectType objectType, + void* pvObject) + { + Debug.Assert(objectType != 0); + Debug.Assert(pvObject != (void*)0); + + const Interop.Crypt32.ContentType ContentType = + Interop.Crypt32.ContentType.CERT_QUERY_CONTENT_CERT; + const Interop.Crypt32.ExpectedContentTypeFlags ExpectedContentType = + Interop.Crypt32.ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_CERT; + + bool loaded = Interop.Crypt32.CryptQueryObject( + objectType, + pvObject, + ExpectedContentType, + Interop.Crypt32.ExpectedFormatTypeFlags.CERT_QUERY_FORMAT_FLAG_ALL, + dwFlags: 0, + pdwMsgAndCertEncodingType: IntPtr.Zero, + out Interop.Crypt32.ContentType actualType, + pdwFormatType: IntPtr.Zero, + phCertStore: IntPtr.Zero, + phMsg: IntPtr.Zero, + out SafeCertContextHandle singleContext); + + if (!loaded) + { + singleContext.Dispose(); + throw Marshal.GetHRForLastWin32Error().ToCryptographicException(); + } + + // Since contentType is an input filter, actualType should not be possible to disagree. + // + // Since contentType is only CERT, singleContext should either be valid, or the + // function should have returned false. + if (actualType != ContentType || singleContext.IsInvalid) + { + singleContext.Dispose(); + throw new CryptographicException(); + } + + CertificatePal pal = new CertificatePal(singleContext, deleteKeyContainer: false); + return pal; + } + + private static SafeCertStoreHandle ImportPfx( + ref BagState bagState, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + ArraySegment reassembled = bagState.ToPfx(password); + SafeCertStoreHandle storeHandle = ImportPfx(reassembled, password, keyStorageFlags); + CryptoPool.Return(reassembled); + + return storeHandle; + } + + private static unsafe SafeCertStoreHandle ImportPfx( + ReadOnlySpan data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags) + { + const int MaxStackPasswordLength = 64; + Span szPassword = stackalloc char[MaxStackPasswordLength + 1]; + Interop.Crypt32.PfxCertStoreFlags flags = MapKeyStorageFlags(keyStorageFlags); + + if (password.Length >= MaxStackPasswordLength) + { + szPassword = new char[password.Length + 1]; + } + + SafeCertStoreHandle storeHandle; + + fixed (byte* dataPtr = data) + fixed (char* szPtr = szPassword) + { + try + { + password.CopyTo(szPassword); + szPassword[password.Length] = '\0'; + + Interop.Crypt32.DATA_BLOB blob = new((IntPtr)dataPtr, (uint)data.Length); + + storeHandle = Interop.Crypt32.PFXImportCertStore( + ref blob, + szPtr, + flags); + } + finally + { + CryptographicOperations.ZeroMemory(MemoryMarshal.AsBytes(szPassword)); + } + } + + if (storeHandle.IsInvalid) + { + Exception e = Marshal.GetHRForLastWin32Error().ToCryptographicException(); + storeHandle.Dispose(); + throw e; + } + + return storeHandle; + } + + private static Interop.Crypt32.PfxCertStoreFlags MapKeyStorageFlags(X509KeyStorageFlags keyStorageFlags) + { + Debug.Assert((keyStorageFlags & KeyStorageFlagsAll) == keyStorageFlags); + + Interop.Crypt32.PfxCertStoreFlags pfxCertStoreFlags = 0; + if ((keyStorageFlags & X509KeyStorageFlags.UserKeySet) == X509KeyStorageFlags.UserKeySet) + pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.CRYPT_USER_KEYSET; + else if ((keyStorageFlags & X509KeyStorageFlags.MachineKeySet) == X509KeyStorageFlags.MachineKeySet) + pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.CRYPT_MACHINE_KEYSET; + + if ((keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable) + pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.CRYPT_EXPORTABLE; + if ((keyStorageFlags & X509KeyStorageFlags.UserProtected) == X509KeyStorageFlags.UserProtected) + pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.CRYPT_USER_PROTECTED; + + // If a user is asking for an Ephemeral key they should be willing to test their code to find out + // that it will no longer import into CAPI. This solves problems of legacy CSPs being + // difficult to do SHA-2 RSA signatures with, simplifies the story for UWP, and reduces the + // complexity of pointer interpretation. + if ((keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) == X509KeyStorageFlags.EphemeralKeySet) + pfxCertStoreFlags |= Interop.Crypt32.PfxCertStoreFlags.PKCS12_NO_PERSIST_KEY | Interop.Crypt32.PfxCertStoreFlags.PKCS12_ALWAYS_CNG_KSP; + + // In .NET Framework loading a PFX then adding the key to the Windows Certificate Store would + // enable a native application compiled against CAPI to find that private key and interoperate with it. + // + // For .NET Core this behavior is being retained. + + return pfxCertStoreFlags; + } + + private static bool ShouldDeleteKeyContainer(X509KeyStorageFlags keyStorageFlags) + { + // If PersistKeySet is set we don't delete the key, so that it persists. + // If EphemeralKeySet is set we don't delete the key, because there's no file, so it's a wasteful call. + const X509KeyStorageFlags DeleteUnless = + X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.EphemeralKeySet; + + bool deleteKeyContainer = ((keyStorageFlags & DeleteUnless) == 0); + return deleteKeyContainer; + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.iOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.iOS.cs new file mode 100644 index 0000000000000..11e253150053c --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.iOS.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Formats.Asn1; +using System.IO; +using Microsoft.Win32.SafeHandles; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + private static partial ICertificatePal LoadCertificatePal(ReadOnlySpan data) + { + if (data.IsEmpty) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (X509Certificate2.GetCertContentType(data) != X509ContentType.Cert) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return LoadX509(data); + } + + private static partial ICertificatePal LoadCertificatePalFromFile(string path) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + using (FileStream stream = File.OpenRead(path)) + { + int length = (int)long.Min(int.MaxValue, stream.Length); + byte[] buf = CryptoPool.Rent(length); + + try + { + stream.ReadAtLeast(buf, length); + return LoadCertificatePal(buf.AsSpan(0, length)); + } + finally + { + CryptoPool.Return(buf, length); + } + } + } + + static partial void ValidatePlatformKeyStorageFlags(X509KeyStorageFlags keyStorageFlags) + { + // Unlike macOS, iOS does support EphemeralKeySet. + + if ((keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable) + { + throw new PlatformNotSupportedException(SR.Cryptography_X509_PKCS12_ExportableNotSupported); + } + + if ((keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet) + { + throw new PlatformNotSupportedException(SR.Cryptography_X509_PKCS12_PersistKeySetNotSupported); + } + } + + private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, ImportState importState) + { + AppleCertificatePal pal = (AppleCertificatePal)certAndKey.Cert!; + + if (certAndKey.Key is not null) + { + AppleCertificatePal newPal = AppleCertificatePal.ImportPkcs12(pal, certAndKey.Key); + pal.Dispose(); + pal = newPal; + } + + return new Pkcs12Return(pal); + } + + private static partial AsymmetricAlgorithm? CreateKey(string algorithm) + { + return algorithm switch + { + Oids.Rsa or Oids.RsaPss => new RSAImplementation.RSASecurityTransforms(), + Oids.EcPublicKey or Oids.EcDiffieHellman => new ECDsaImplementation.ECDsaSecurityTransforms(), + // There's no DSA support on iOS/tvOS. + _ => null, + }; + } + + private static partial ICertificatePalCore LoadX509Der(ReadOnlyMemory data) + { + ReadOnlySpan span = data.Span; + + AsnValueReader reader = new AsnValueReader(span, AsnEncodingRules.DER); + reader.ReadSequence(); + reader.ThrowIfNotEmpty(); + + return LoadX509(span); + } + + private static AppleCertificatePal LoadX509(ReadOnlySpan data) + { + SafeSecIdentityHandle identityHandle; + SafeSecCertificateHandle certHandle = Interop.AppleCrypto.X509ImportCertificate( + data, + X509ContentType.Cert, + SafePasswordHandle.InvalidHandle, + out identityHandle); + + if (identityHandle.IsInvalid) + { + identityHandle.Dispose(); + return new AppleCertificatePal(certHandle); + } + + Debug.Fail("Non-PKCS12 import produced an identity handle"); + + identityHandle.Dispose(); + certHandle.Dispose(); + throw new CryptographicException(); + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.macOS.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.macOS.cs new file mode 100644 index 0000000000000..62216ebeeefb2 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.macOS.cs @@ -0,0 +1,232 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; +using System.Formats.Asn1; +using System.IO; +using System.IO.MemoryMappedFiles; +using System.Security.Cryptography.Apple; +using Microsoft.Win32.SafeHandles; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + private static partial ICertificatePal LoadCertificatePal(ReadOnlySpan data) + { + if (data.IsEmpty) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + if (X509Certificate2.GetCertContentType(data) != X509ContentType.Cert) + { + + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + return LoadX509(data); + } + + private static partial ICertificatePal LoadCertificatePalFromFile(string path) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + using (FileStream stream = File.OpenRead(path)) + { + int length = (int)long.Min(int.MaxValue, stream.Length); + byte[]? rented = null; + MemoryManager? manager = null; + + try + { + ReadOnlySpan span; + + if (length > MemoryMappedFileCutoff) + { + manager = MemoryMappedFileMemoryManager.CreateFromFileClamped(stream); + span = manager.Memory.Span; + } + else + { + rented = CryptoPool.Rent(length); + stream.ReadAtLeast(rented, length); + span = rented.AsSpan(0, length); + } + + return LoadCertificatePal(span); + } + finally + { + (manager as IDisposable)?.Dispose(); + + if (rented is not null) + { + CryptoPool.Return(rented, length); + } + } + } + } + + static partial void ValidatePlatformKeyStorageFlags(X509KeyStorageFlags keyStorageFlags) + { + if ((keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) == X509KeyStorageFlags.EphemeralKeySet) + { + throw new PlatformNotSupportedException(SR.Cryptography_X509_NoEphemeralPfx); + } + } + + static partial void InitializeImportState(ref ImportState importState, X509KeyStorageFlags keyStorageFlags) + { + bool exportable = (keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable; + + bool persist = + (keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet; + + SafeKeychainHandle keychain = persist + ? Interop.AppleCrypto.SecKeychainCopyDefault() + : Interop.AppleCrypto.CreateTemporaryKeychain(); + + importState.Exportable = exportable; + importState.Persisted = persist; + importState.Keychain = keychain; + } + + private static partial Pkcs12Return FromCertAndKey(CertAndKey certAndKey, ImportState importState) + { + AppleCertificatePal pal = (AppleCertificatePal)certAndKey.Cert!; + SafeSecKeyRefHandle? key = null; + + if (certAndKey.Key is not null) + { + key = GetPrivateKey(certAndKey.Key); + certAndKey.Key.Dispose(); + } + + if (key is not null || importState.Persisted) + { + if (key is not null && !importState.Exportable) + { + AppleCertificatePal newPal = AppleCertificatePal.ImportPkcs12NonExportable( + pal, + key, + SafePasswordHandle.InvalidHandle, + importState.Keychain); + + pal.Dispose(); + pal = newPal; + } + else + { + AppleCertificatePal? identity = pal.MoveToKeychain(importState.Keychain, key); + + if (identity is not null) + { + pal.Dispose(); + pal = identity; + } + } + } + + return new Pkcs12Return(pal); + } + + private static partial AsymmetricAlgorithm? CreateKey(string algorithm) + { + return algorithm switch + { + Oids.Rsa or Oids.RsaPss => new RSAImplementation.RSASecurityTransforms(), + Oids.EcPublicKey or Oids.EcDiffieHellman => new ECDsaImplementation.ECDsaSecurityTransforms(), + Oids.Dsa => new DSAImplementation.DSASecurityTransforms(), + _ => null, + }; + } + + internal static SafeSecKeyRefHandle? GetPrivateKey(AsymmetricAlgorithm? key) + { + if (key == null) + { + return null; + } + + if (key is RSAImplementation.RSASecurityTransforms rsa) + { + byte[] rsaPrivateKey = rsa.ExportRSAPrivateKey(); + using (PinAndClear.Track(rsaPrivateKey)) + { + return Interop.AppleCrypto.ImportEphemeralKey(rsaPrivateKey, true); + } + } + + if (key is DSAImplementation.DSASecurityTransforms dsa) + { + DSAParameters dsaParameters = dsa.ExportParameters(true); + + using (PinAndClear.Track(dsaParameters.X!)) + { + return DSAImplementation.DSASecurityTransforms.ImportKey(dsaParameters); + } + } + + if (key is ECDsaImplementation.ECDsaSecurityTransforms ecdsa) + { + byte[] ecdsaPrivateKey = ecdsa.ExportECPrivateKey(); + using (PinAndClear.Track(ecdsaPrivateKey)) + { + return Interop.AppleCrypto.ImportEphemeralKey(ecdsaPrivateKey, true); + } + } + + Debug.Fail("Invalid key implementation"); + return null; + } + + private static partial ICertificatePalCore LoadX509Der(ReadOnlyMemory data) + { + ReadOnlySpan span = data.Span; + + AsnValueReader reader = new AsnValueReader(span, AsnEncodingRules.DER); + reader.ReadSequence(); + reader.ThrowIfNotEmpty(); + + return LoadX509(span); + } + + private static AppleCertificatePal LoadX509(ReadOnlySpan data) + { + SafeSecCertificateHandle certHandle = Interop.AppleCrypto.X509ImportCertificate( + data, + X509ContentType.Cert, + SafePasswordHandle.InvalidHandle, + SafeTemporaryKeychainHandle.InvalidHandle, + exportable: true, + out SafeSecIdentityHandle identityHandle); + + if (identityHandle.IsInvalid) + { + identityHandle.Dispose(); + return new AppleCertificatePal(certHandle); + } + + Debug.Fail("Non-PKCS12 import produced an identity handle"); + + identityHandle.Dispose(); + certHandle.Dispose(); + throw new CryptographicException(); + } + + private partial struct ImportState + { + internal bool Exportable; + internal bool Persisted; + internal SafeKeychainHandle Keychain; + + partial void DisposeCore() + { + Keychain?.Dispose(); + this = default; + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netcore.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netcore.cs new file mode 100644 index 0000000000000..9eaed37784a65 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509CertificateLoader.netcore.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Buffers; +using System.Diagnostics; + +namespace System.Security.Cryptography.X509Certificates +{ + public static partial class X509CertificateLoader + { + public static partial X509Certificate2 LoadCertificate(byte[] data) + { + ThrowIfNull(data); + + return LoadCertificate(new ReadOnlySpan(data)); + } + + public static partial X509Certificate2 LoadCertificate(ReadOnlySpan data) + { + if (data.IsEmpty) + { + ThrowWithHResult(SR.Cryptography_Der_Invalid_Encoding, CRYPT_E_BAD_DECODE); + } + + ICertificatePal pal = LoadCertificatePal(data); + Debug.Assert(pal is not null); + return new X509Certificate2(pal); + } + + public static partial X509Certificate2 LoadCertificateFromFile(string path) + { + ArgumentException.ThrowIfNullOrEmpty(path); + + ICertificatePal pal = LoadCertificatePalFromFile(path); + Debug.Assert(pal is not null); + return new X509Certificate2(pal); + } + + private static partial ICertificatePal LoadCertificatePal(ReadOnlySpan data); + private static partial ICertificatePal LoadCertificatePalFromFile(string path); + + internal static ICertificatePal LoadPkcs12Pal( + ReadOnlySpan data, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + Debug.Assert(loaderLimits is not null); + + unsafe + { + fixed (byte* pinned = data) + { + using (PointerMemoryManager manager = new(pinned, data.Length)) + { + return LoadPkcs12( + manager.Memory, + password, + keyStorageFlags, + loaderLimits).GetPal(); + } + } + } + } + + internal static ICertificatePal LoadPkcs12PalFromFile( + string path, + ReadOnlySpan password, + X509KeyStorageFlags keyStorageFlags, + Pkcs12LoaderLimits loaderLimits) + { + Debug.Assert(loaderLimits is not null); + + ThrowIfNullOrEmpty(path); + + return LoadFromFile( + path, + password, + keyStorageFlags, + loaderLimits, + LoadPkcs12).GetPal(); + } + + private const X509KeyStorageFlags KeyStorageFlagsAll = + X509KeyStorageFlags.UserKeySet | + X509KeyStorageFlags.MachineKeySet | + X509KeyStorageFlags.Exportable | + X509KeyStorageFlags.UserProtected | + X509KeyStorageFlags.PersistKeySet | + X509KeyStorageFlags.EphemeralKeySet; + + internal static void ValidateKeyStorageFlags(X509KeyStorageFlags keyStorageFlags) + { + ValidateKeyStorageFlagsCore(keyStorageFlags); + } + + static partial void ValidateKeyStorageFlagsCore(X509KeyStorageFlags keyStorageFlags) + { + if ((keyStorageFlags & ~KeyStorageFlagsAll) != 0) + { + throw new ArgumentException(SR.Argument_InvalidFlag, nameof(keyStorageFlags)); + } + + const X509KeyStorageFlags EphemeralPersist = + X509KeyStorageFlags.EphemeralKeySet | X509KeyStorageFlags.PersistKeySet; + + X509KeyStorageFlags persistenceFlags = keyStorageFlags & EphemeralPersist; + + if (persistenceFlags == EphemeralPersist) + { + throw new ArgumentException( + SR.Format(SR.Cryptography_X509_InvalidFlagCombination, persistenceFlags), + nameof(keyStorageFlags)); + } + + ValidatePlatformKeyStorageFlags(keyStorageFlags); + } + + static partial void ValidatePlatformKeyStorageFlags(X509KeyStorageFlags keyStorageFlags); + + private readonly partial struct Pkcs12Return + { + private readonly ICertificatePal? _pal; + + internal Pkcs12Return(ICertificatePal pal) + { + _pal = pal; + } + + internal ICertificatePal GetPal() + { + Debug.Assert(_pal is not null); + return _pal; + } + + internal partial bool HasValue() => _pal is not null; + + internal partial X509Certificate2 ToCertificate() + { + Debug.Assert(_pal is not null); + + return new X509Certificate2(_pal); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Pal.Android.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Pal.Android.cs index 0b82be4079ff7..4f8dd772449e8 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Pal.Android.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/X509Certificates/X509Pal.Android.cs @@ -98,7 +98,7 @@ public X509ContentType GetCertContentType(ReadOnlySpan rawData) return contentType; } - if (AndroidPkcs12Reader.IsPkcs12(rawData)) + if (X509CertificateLoader.IsPkcs12(rawData)) { return X509ContentType.Pkcs12; } diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index 09ffc37aca351..699b36cc33ddf 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -19,6 +19,8 @@ + + + + + + - - @@ -389,6 +399,8 @@ + diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CtorTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CtorTests.cs index 76b45bf66dc35..2847c4221109c 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/CtorTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/CtorTests.cs @@ -354,7 +354,13 @@ public static void InvalidCertificateBlob() } else // Any Unix { - Assert.Equal(new CryptographicException("message").HResult, ex.HResult); + const int COR_E_SYSTEM = -2146233087; + const int CRYPT_E_BAD_DECODE = -2146885630; + + if (ex.HResult != CRYPT_E_BAD_DECODE) + { + Assert.Equal(COR_E_SYSTEM, ex.HResult); + } } } diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests.cs index c73af1ac3173a..452d3ad02d534 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests.cs @@ -90,9 +90,10 @@ private void ReadUnreadablePfx( string bestPassword, // NTE_FAIL int win32Error = -2146893792, - int altWin32Error = 0) + int altWin32Error = 0, + int secondAltWin32Error = 0) { - ReadUnreadablePfx(pfxBytes, bestPassword, s_importFlags, win32Error, altWin32Error); + ReadUnreadablePfx(pfxBytes, bestPassword, s_importFlags, win32Error, altWin32Error, secondAltWin32Error); } protected abstract void ReadEmptyPfx(byte[] pfxBytes, string correctPassword); @@ -104,7 +105,8 @@ protected abstract void ReadUnreadablePfx( X509KeyStorageFlags importFlags, // NTE_FAIL int win32Error = -2146893792, - int altWin32Error = 0); + int altWin32Error = 0, + int secondAltWin32Error = 0); [Fact] public void EmptyPfx_NoMac() @@ -226,8 +228,10 @@ public void OneCert_EncryptedEmptyPassword_OneKey_EncryptedNullPassword_NoMac(bo if (s_loaderFailsKeysEarly || associateKey || encryptKeySafe) { // NTE_FAIL, falling back to CRYPT_E_BAD_ENCODE if padding happened to work out. - ReadUnreadablePfx(pfxBytes, null, altWin32Error: -2146885630); - ReadUnreadablePfx(pfxBytes, string.Empty, altWin32Error: -2146885630); + // The new cert loader gets ERROR_INVALID_PASSWORD since it doesn't see both a + // success and a failure in the win32 layer. + ReadUnreadablePfx(pfxBytes, null, altWin32Error: -2146885630, secondAltWin32Error: -2147024810); + ReadUnreadablePfx(pfxBytes, string.Empty, altWin32Error: -2146885630, secondAltWin32Error: -2147024810); } else { @@ -1110,6 +1114,120 @@ public void TwoCerts_TwoKeys_ManySafeContentsValues(bool invertCertOrder, bool i } } + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + [SkipOnPlatform(TestPlatforms.iOS | TestPlatforms.MacCatalyst | TestPlatforms.tvOS, "The PKCS#12 Exportable flag is not supported on iOS/MacCatalyst/tvOS")] + public void TwoCerts_TwoKeys_ManySafeContentsValues_UnencryptedAuthSafes_NoMac(bool invertCertOrder, bool invertKeyOrder) + { + string pw = invertCertOrder ? "" : null; + + using (ImportedCollection ic = Cert.Import(TestData.MultiPrivateKeyPfx, null, s_exportableImportFlags)) + { + X509Certificate2Collection certs = ic.Collection; + X509Certificate2 first = certs[0]; + X509Certificate2 second = certs[1]; + + if (invertCertOrder) + { + X509Certificate2 tmp = first; + first = second; + second = tmp; + } + + using (AsymmetricAlgorithm firstKey = first.GetRSAPrivateKey()) + using (AsymmetricAlgorithm secondKey = second.GetRSAPrivateKey()) + { + AsymmetricAlgorithm firstAdd = firstKey; + AsymmetricAlgorithm secondAdd = secondKey; + + if (invertKeyOrder != invertCertOrder) + { + AsymmetricAlgorithm tmp = firstKey; + firstAdd = secondAdd; + secondAdd = tmp; + } + + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents firstKeyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents secondKeyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents firstCertContents = new Pkcs12SafeContents(); + Pkcs12SafeContents secondCertContents = new Pkcs12SafeContents(); + + Pkcs12SafeContents irrelevant = new Pkcs12SafeContents(); + irrelevant.AddSecret(new Oid("0.0"), new byte[] { 0x05, 0x00 }); + + Pkcs12SafeBag firstAddedKeyBag = firstKeyContents.AddShroudedKey(firstAdd, pw, s_windowsPbe); + Pkcs12SafeBag secondAddedKeyBag = secondKeyContents.AddShroudedKey(secondAdd, pw, s_windowsPbe); + Pkcs12SafeBag firstCertBag = firstCertContents.AddCertificate(first); + Pkcs12SafeBag secondCertBag = secondCertContents.AddCertificate(second); + Pkcs12SafeBag firstKeyBag = firstAddedKeyBag; + Pkcs12SafeBag secondKeyBag = secondAddedKeyBag; + + if (invertKeyOrder != invertCertOrder) + { + Pkcs12SafeBag tmp = firstKeyBag; + firstKeyBag = secondKeyBag; + secondKeyBag = tmp; + } + + firstCertBag.Attributes.Add(s_keyIdOne); + firstKeyBag.Attributes.Add(s_keyIdOne); + + Pkcs9LocalKeyId secondKeyId = new Pkcs9LocalKeyId(second.GetCertHash()); + secondCertBag.Attributes.Add(secondKeyId); + secondKeyBag.Attributes.Add(secondKeyId); + + // 2C, 1K, 1C, 2K + // With some non-participating contents values sprinkled in for good measure. + AddContents(irrelevant, builder, pw, encrypt: false); + AddContents(secondCertContents, builder, pw, encrypt: false); + AddContents(irrelevant, builder, pw, encrypt: false); + AddContents(firstKeyContents, builder, pw, encrypt: false); + AddContents(firstCertContents, builder, pw, encrypt: false); + AddContents(irrelevant, builder, pw, encrypt: false); + AddContents(secondKeyContents, builder, pw, encrypt: false); + AddContents(irrelevant, builder, pw, encrypt: false); + + builder.SealWithoutIntegrity(); + byte[] pfxBytes = builder.Encode(); + + X509Certificate2[] expectedOrder = { first, second }; + + Action followup = CheckKeyConsistency; + + // For unknown reasons, CheckKeyConsistency on this test fails + // on Windows 7 with an Access Denied in all variations for + // Collections, and in invertCertOrder: true for Single. + // + // Obviously this hit some sort of weird corner case in the Win7 + // loader, but it's not important to the test. + + if (OperatingSystem.IsWindows() && + !PlatformDetection.IsWindows8xOrLater) + { + followup = null; + } + + ReadMultiPfx( + pfxBytes, + "", + first, + expectedOrder, + followup); + + ReadMultiPfx( + pfxBytes, + null, + first, + expectedOrder, + followup); + } + } + } + private static void CheckKeyConsistency(X509Certificate2 cert) { byte[] data = { 2, 7, 4 }; diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests_Collection.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests_Collection.cs index f8c6f843800f2..8de41a83c80a8 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests_Collection.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests_Collection.cs @@ -96,7 +96,8 @@ protected override void ReadUnreadablePfx( string bestPassword, X509KeyStorageFlags importFlags, int win32Error, - int altWin32Error) + int altWin32Error, + int secondAltWin32Error) { X509Certificate2Collection coll = new X509Certificate2Collection(); @@ -105,14 +106,27 @@ protected override void ReadUnreadablePfx( if (OperatingSystem.IsWindows()) { - if (altWin32Error != 0 && ex.HResult != altWin32Error) + if (altWin32Error == 0 || ex.HResult != altWin32Error) { - Assert.Equal(win32Error, ex.HResult); + if (secondAltWin32Error == 0 || ex.HResult != secondAltWin32Error) + { + Assert.Equal(win32Error, ex.HResult); + } } } - else + + ex = Assert.ThrowsAny( + () => X509CertificateLoader.LoadPkcs12Collection(pfxBytes, bestPassword, importFlags)); + + if (OperatingSystem.IsWindows()) { - Assert.NotNull(ex.InnerException); + if (altWin32Error == 0 || ex.HResult != altWin32Error) + { + if (secondAltWin32Error == 0 || ex.HResult != secondAltWin32Error) + { + Assert.Equal(win32Error, ex.HResult); + } + } } } } diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests_SingleCert.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests_SingleCert.cs index 9e11b356069e2..c2368dfdd38f2 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests_SingleCert.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxFormatTests_SingleCert.cs @@ -55,6 +55,11 @@ protected override void ReadEmptyPfx(byte[] pfxBytes, string correctPassword) () => new X509Certificate2(pfxBytes, correctPassword, s_importFlags)); AssertMessageContains("no certificates", ex); + + ex = Assert.Throws( + () => X509CertificateLoader.LoadPkcs12(pfxBytes, correctPassword, s_importFlags)); + + AssertMessageContains("no certificates", ex); } protected override void ReadWrongPassword(byte[] pfxBytes, string wrongPassword) @@ -64,6 +69,12 @@ protected override void ReadWrongPassword(byte[] pfxBytes, string wrongPassword) AssertMessageContains("password", ex); Assert.Equal(ErrorInvalidPasswordHResult, ex.HResult); + + ex = Assert.ThrowsAny( + () => X509CertificateLoader.LoadPkcs12(pfxBytes, wrongPassword, s_importFlags)); + + AssertMessageContains("password", ex); + Assert.Equal(ErrorInvalidPasswordHResult, ex.HResult); } protected override void ReadUnreadablePfx( @@ -71,21 +82,35 @@ protected override void ReadUnreadablePfx( string bestPassword, X509KeyStorageFlags importFlags, int win32Error, - int altWin32Error) + int altWin32Error, + int secondAltWin32Error) { CryptographicException ex = Assert.ThrowsAny( () => new X509Certificate2(pfxBytes, bestPassword, importFlags)); if (OperatingSystem.IsWindows()) { - if (altWin32Error != 0 && ex.HResult != altWin32Error) + if (altWin32Error == 0 || ex.HResult != altWin32Error) { - Assert.Equal(win32Error, ex.HResult); + if (secondAltWin32Error == 0 || ex.HResult != secondAltWin32Error) + { + Assert.Equal(win32Error, ex.HResult); + } } } - else + + ex = Assert.ThrowsAny( + () => X509CertificateLoader.LoadPkcs12(pfxBytes, bestPassword, importFlags)); + + if (OperatingSystem.IsWindows()) { - Assert.NotNull(ex.InnerException); + if (altWin32Error == 0 || ex.HResult != altWin32Error) + { + if (secondAltWin32Error == 0 || ex.HResult != secondAltWin32Error) + { + Assert.Equal(win32Error, ex.HResult); + } + } } } diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxIterationCountTests.CustomAppContextDataLimit.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxIterationCountTests.CustomAppContextDataLimit.cs index bed126455a039..d950b9e619b07 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxIterationCountTests.CustomAppContextDataLimit.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxIterationCountTests.CustomAppContextDataLimit.cs @@ -128,16 +128,10 @@ public void Import_AppContextDataWithValueMinusOne_IterationCountExceedingDefaul PfxInfo pfxInfo = s_certificatesDictionary[certName]; - if (OperatingSystem.IsWindows()) - { - // Opting-out with AppContext data value -1 will still give us error because cert is beyond Windows limit. - // But we will get the CryptoThrowHelper+WindowsCryptographicException. - PfxIterationCountTests.VerifyThrowsCryptoExButDoesNotThrowPfxWithoutPassword(() => Import(pfxInfo.Blob)); - } - else - { - Assert.NotNull(Import(pfxInfo.Blob)); - } + // The total iteration count filter is disabled, but individual items are + // still limited to 600k. + CryptographicException ce = Assert.Throws(() => Import(pfxInfo.Blob)); + Assert.Contains(PfxIterationCountTests.FwlinkId, ce.Message); }, name).Dispose(); } } diff --git a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxTests.cs b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxTests.cs index c70260c9dc0db..7a6a8db356791 100644 --- a/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxTests.cs +++ b/src/libraries/System.Security.Cryptography/tests/X509Certificates/PfxTests.cs @@ -481,37 +481,6 @@ public static void CollectionPerphemeralImport_HasKeyName() } } - [ConditionalTheory] - [MemberData(memberName: nameof(PfxIterationCountTests.GetCertsWith_IterationCountNotExceedingDefaultLimit_AndNullOrEmptyPassword_MemberData), MemberType = typeof(PfxIterationCountTests))] - public static void TestIterationCounter(string name, bool usesPbes2, byte[] blob, int iterationCount, bool usesRC2) - { - _ = iterationCount; - - MethodInfo method = typeof(X509Certificate).GetMethod("GetIterationCount", BindingFlags.Static | BindingFlags.NonPublic); - GetIterationCountDelegate target = method.CreateDelegate(); - - if (usesPbes2 && !Pkcs12PBES2Supported) - { - throw new SkipTestException(name + " uses PBES2, which is not supported on this version."); - } - - if (usesRC2 && !PlatformSupport.IsRC2Supported) - { - throw new SkipTestException(name + " uses RC2, which is not supported on this platform."); - } - - try - { - long count = (long)target(blob, out int bytesConsumed); - Assert.Equal(iterationCount, count); - Assert.Equal(blob.Length, bytesConsumed); // we currently don't have any cert with trailing data. - } - catch (Exception e) - { - throw new Exception($"There's an error on certificate {name}, see inner exception for details", e); - } - } - internal static bool IsPkcs12IterationCountAllowed(long iterationCount, long allowedIterations) { if (allowedIterations == UnlimitedIterations)