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 f7cf15985b686b..c170c89d8e0544 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 7df9fb7e1474f0..dcc641c64f9ed1 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 00000000000000..10fb24e05443dd --- /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 38e60abcebc640..67ee5593e6586b 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 47396768b09d97..05fd3676f76166 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 462e4d26a8f64d..341931377ddd8d 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 b60bac3f6ede6e..7f78979af07357 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 7500212fe27d9c..00000000000000 --- 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 8e96369bca9178..24b238e502b1f3 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 b7364108222ad1..033c9c9db0d9f8 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 4bf056ffd5f4f9..0e931591200a77 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 1fa1d0ee033914..8264a6586589eb 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 00000000000000..2edafaeb7db12c --- /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 00000000000000..3200771cee0adb --- /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 00000000000000..ccea2de41fc007 --- /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 00000000000000..a4ef6480b0eea9 --- /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 60ee5d47a2af61..00000000000000 --- 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 8a1a95bb0984be..569ef966f13cb6 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 a6efa3a1f19173..23b844cd655492 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 0e0295ecf7f6e7..9233360c941420 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 00000000000000..84d57f6e7e74e7 --- /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 00000000000000..444094c15b0ca8 --- /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 00000000000000..6568be9756eb2f --- /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 00000000000000..f29eaf7c0e0235 --- /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 5f6e0fbf895762..b7cb4dae61d56a 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 83dc7a9f3fa30d..e13db0f1f5e4ec 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 ef8ae599f15b4c..d6c00334523640 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 2c71f52147943a..88c60b8f42ede1 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 00000000000000..918c1cbd4ea507 --- /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 85b7fab4f321a6..02e47cb969f047 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 00000000000000..0d2af61b8d6347 --- /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 4dc6e7b867d7e9..1e81d7a01b023c 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 00000000000000..f73df6b4e081a0 --- /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 00000000000000..890f5b32c4934f --- /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 00000000000000..25a778d09a194b --- /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 00000000000000..649979d0186bed --- /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 00000000000000..c867617e66b154 --- /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 02ee9aae4af719..309d9b2e1c16b0 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 00000000000000..7015c10fd621f3 --- /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 06f148fabc7ef3..fb549df74f4b4c 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 46ef9900dbf4ce..e96500b4f2292d 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 05c91987126a78..62b608677b74fa 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 7d0fd8f152118e..b9f0ce5317f7f9 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 42a1da0fa9d790..c456244223293c 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 10800a71d537bf..00000000000000 --- 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 dbbdf1d5c2eaa2..e317dc7fb67c56 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 ce4745c42d58f3..2d1b84b1138638 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 9076f0c4899a1b..e5903066a14bfd 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 1de7e8ce9bee72..7616da6ffe4ec1 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 6e329434278de1..1951e7dc8bd09f 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 c190f3400b9590..00000000000000 --- 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 e493436e01d7b4..00000000000000 --- 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 8f3274d15d2329..00000000000000 --- 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 2b5feeee7fe79f..dee85ca9b9b1a1 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 9b15a429e7c22d..ad3077da247245 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 0af05ab604b677..c87cb0cb4c9228 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 c0a4616273c049..00000000000000 --- 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 41a3c9bcd5a03b..63d00a0ca6cd84 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 706d2a024b0470..8c07209c61cf1b 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 4d43399e6f7c5e..5a8c9697c77224 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 962287bc2630e6..2f6a4c2cd44b71 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 a87e89344d8951..6e75c900a699af 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 b34da971931c2c..59155e2b76a2e8 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 54a29a92fd65a6..93a00a240eaebb 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 edccc0b79e337f..112092dcbed6cd 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 fa00011b419c12..3a9d9e0ff56a42 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 b424e971b09e4a..46fbc922ddb33a 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 1f5a24fa15be26..00000000000000 --- 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 00000000000000..0149a61deb3469 --- /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 e186cdd15823fa..ec1be141b98a2e 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; @@ -662,125 +659,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 00000000000000..8a1da9bc03d53e --- /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 00000000000000..95e2f393d69405 --- /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 00000000000000..1873f3ea934ac7 --- /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 00000000000000..f851d163fc8fcb --- /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 00000000000000..346e8062d34561 --- /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 00000000000000..11e253150053cf --- /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 00000000000000..62216ebeeefb23 --- /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 00000000000000..9eaed37784a652 --- /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 0b82be4079ff7e..4f8dd772449e8c 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 09ffc37aca351a..699b36cc33ddf3 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 76b45bf66dc352..2847c4221109c0 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 c73af1ac3173a7..452d3ad02d534f 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 f8c6f843800f2f..8de41a83c80a81 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 9e11b356069e20..c2368dfdd38f23 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 bed126455a0399..d950b9e619b07f 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 c70260c9dc0db7..7a6a8db3567914 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)