diff --git a/src/Common/src/Interop/OSX/Interop.CoreFoundation.CFData.cs b/src/Common/src/Interop/OSX/Interop.CoreFoundation.CFData.cs index e4eee4e0781f..2d5713c740b2 100644 --- a/src/Common/src/Interop/OSX/Interop.CoreFoundation.CFData.cs +++ b/src/Common/src/Interop/OSX/Interop.CoreFoundation.CFData.cs @@ -20,6 +20,13 @@ internal static partial class CoreFoundation [DllImport(Libraries.CoreFoundationLibrary)] private static extern CFIndex CFDataGetLength(SafeCFDataHandle cfData); + internal static unsafe Span CFDataDangerousGetSpan(SafeCFDataHandle cfData) + { + long length = CFDataGetLength(cfData).ToInt64(); + byte* dataBytes = CFDataGetBytePtr(cfData); + return new Span(dataBytes, checked((int)length)); + } + internal static byte[] CFGetData(SafeCFDataHandle cfData) { bool addedRef = false; @@ -27,23 +34,7 @@ internal static byte[] CFGetData(SafeCFDataHandle cfData) try { cfData.DangerousAddRef(ref addedRef); - long length = CFDataGetLength(cfData).ToInt64(); - - if (length == 0) - { - return Array.Empty(); - } - - byte[] bytes = new byte[length]; - - unsafe - { - byte* dataBytes = CFDataGetBytePtr(cfData); - Marshal.Copy((IntPtr)dataBytes, bytes, 0, bytes.Length); - } - - return bytes; - + return CFDataDangerousGetSpan(cfData).ToArray(); } finally { diff --git a/src/Common/src/Interop/OSX/Interop.CoreFoundation.cs b/src/Common/src/Interop/OSX/Interop.CoreFoundation.cs index bbe4c1a8c08b..254e54eee6b6 100644 --- a/src/Common/src/Interop/OSX/Interop.CoreFoundation.cs +++ b/src/Common/src/Interop/OSX/Interop.CoreFoundation.cs @@ -10,7 +10,7 @@ using CFStringRef = System.IntPtr; using CFArrayRef = System.IntPtr; - +using CFIndex = System.IntPtr; internal static partial class Interop { @@ -38,6 +38,24 @@ private enum CFStringBuiltInEncodings : uint kCFStringEncodingUTF32LE = 0x1c000100 } + /// + /// Creates a CFStringRef from a specified range of memory with a specified encoding. + /// Follows the "Create Rule" where if you create it, you delete it. + /// + /// Should be IntPtr.Zero + /// The pointer to the beginning of the encoded string. + /// The number of bytes in the encoding to read. + /// The encoding type. + /// Whether or not a BOM is present. + /// A CFStringRef on success, otherwise a SafeCreateHandle(IntPtr.Zero). + [DllImport(Interop.Libraries.CoreFoundationLibrary)] + private static extern SafeCreateHandle CFStringCreateWithBytes( + IntPtr alloc, + IntPtr bytes, + CFIndex numBytes, + CFStringBuiltInEncodings encoding, + bool isExternalRepresentation); + /// /// Creates a CFStringRef from a 8-bit String object. Follows the "Create Rule" where if you create it, you delete it. /// @@ -86,6 +104,25 @@ internal static SafeCreateHandle CFStringCreateWithCString(IntPtr utf8str) return CFStringCreateWithCString(IntPtr.Zero, utf8str, CFStringBuiltInEncodings.kCFStringEncodingUTF8); } + /// + /// Creates a CFStringRef from a span of chars. + /// Follows the "Create Rule" where if you create it, you delete it. + /// + /// The chars to make a CFString version of. + /// A CFStringRef on success, otherwise a SafeCreateHandle(IntPtr.Zero). + internal static unsafe SafeCreateHandle CFStringCreateFromSpan(ReadOnlySpan source) + { + fixed (char* sourcePtr = source) + { + return CFStringCreateWithBytes( + IntPtr.Zero, + (IntPtr)sourcePtr, + new CFIndex(source.Length * 2), + CFStringBuiltInEncodings.kCFStringEncodingUTF16, + isExternalRepresentation: false); + } + } + /// /// Creates a pointer to an unmanaged CFArray containing the input values. Follows the "Create Rule" where if you create it, you delete it. /// diff --git a/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.SecKeyRef.Export.cs b/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.SecKeyRef.Export.cs index 3c3345bfbae3..2bf6bbe01701 100644 --- a/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.SecKeyRef.Export.cs +++ b/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.SecKeyRef.Export.cs @@ -23,13 +23,13 @@ private static extern int AppleCryptoNative_SecKeyExport( out SafeCFDataHandle cfDataOut, out int pOSStatus); - internal static byte[] SecKeyExport( + internal static SafeCFDataHandle SecKeyExportData( SafeSecKeyRefHandle key, bool exportPrivate, - string password) + ReadOnlySpan password) { SafeCreateHandle exportPassword = exportPrivate - ? CoreFoundation.CFStringCreateWithCString(password) + ? CoreFoundation.CFStringCreateFromSpan(password) : s_nullExportString; int ret; @@ -53,25 +53,31 @@ internal static byte[] SecKeyExport( } } - byte[] exportedData; - - using (cfData) + if (ret == 1) { - if (ret == 0) - { - throw CreateExceptionForOSStatus(osStatus); - } + return cfData; + } - if (ret != 1) - { - Debug.Fail($"AppleCryptoNative_SecKeyExport returned {ret}"); - throw new CryptographicException(); - } + cfData.Dispose(); - exportedData = CoreFoundation.CFGetData(cfData); + if (ret == 0) + { + throw CreateExceptionForOSStatus(osStatus); } - return exportedData; + Debug.Fail($"AppleCryptoNative_SecKeyExport returned {ret}"); + throw new CryptographicException(); + } + + internal static byte[] SecKeyExport( + SafeSecKeyRefHandle key, + bool exportPrivate, + string password) + { + using (SafeCFDataHandle cfData = SecKeyExportData(key, exportPrivate, password)) + { + return CoreFoundation.CFGetData(cfData); + } } } } diff --git a/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.SecKeyRef.cs b/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.SecKeyRef.cs index c80d75c289d1..5b7ad2f8074e 100644 --- a/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.SecKeyRef.cs +++ b/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.SecKeyRef.cs @@ -357,5 +357,18 @@ namespace System.Security.Cryptography.Apple { internal sealed class SafeSecKeyRefHandle : SafeKeychainItemHandle { + protected override void Dispose(bool disposing) + { + if (disposing && SafeHandleCache.IsCachedInvalidHandle(this)) + { + return; + } + + base.Dispose(disposing); + } + + public static SafeSecKeyRefHandle InvalidHandle => + SafeHandleCache.GetInvalidHandle( + () => new SafeSecKeyRefHandle()); } } diff --git a/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X509.cs b/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X509.cs index 85b3d29ac5df..c787ca07d0a6 100644 --- a/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X509.cs +++ b/src/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.X509.cs @@ -85,6 +85,14 @@ private static extern int AppleCryptoNative_X509CopyWithPrivateKey( out SafeSecIdentityHandle pIdentityHandleOut, out int pOSStatus); + [DllImport(Libraries.AppleCryptoNative)] + private static extern int AppleCryptoNative_X509MoveToKeychain( + SafeSecCertificateHandle certHandle, + SafeKeychainHandle targetKeychain, + SafeSecKeyRefHandle privateKeyHandle, + out SafeSecIdentityHandle pIdentityHandleOut, + out int pOSStatus); + internal static byte[] X509GetRawData(SafeSecCertificateHandle cert) { int osStatus; @@ -117,11 +125,7 @@ internal static SafeSecCertificateHandle X509ImportCertificate( bool exportable, out SafeSecIdentityHandle identityHandle) { - SafeSecCertificateHandle certHandle; - int osStatus; - int ret; - - SafeCreateHandle cfPassphrase = s_nullExportString; + SafeCreateHandle cfPassphrase = null; bool releasePassword = false; try @@ -129,27 +133,16 @@ internal static SafeSecCertificateHandle X509ImportCertificate( if (!importPassword.IsInvalid) { importPassword.DangerousAddRef(ref releasePassword); - IntPtr passwordHandle = importPassword.DangerousGetHandle(); - - if (passwordHandle != IntPtr.Zero) - { - cfPassphrase = CoreFoundation.CFStringCreateWithCString(passwordHandle); - } + cfPassphrase = CoreFoundation.CFStringCreateFromSpan(importPassword.DangerousGetSpan()); } - ret = AppleCryptoNative_X509ImportCertificate( + return X509ImportCertificate( bytes, - bytes.Length, contentType, cfPassphrase, keychain, - exportable ? 1 : 0, - out certHandle, - out identityHandle, - out osStatus); - - SafeTemporaryKeychainHandle.TrackItem(certHandle); - SafeTemporaryKeychainHandle.TrackItem(identityHandle); + exportable, + out identityHandle); } finally { @@ -158,11 +151,36 @@ internal static SafeSecCertificateHandle X509ImportCertificate( importPassword.DangerousRelease(); } - if (cfPassphrase != s_nullExportString) - { - cfPassphrase.Dispose(); - } + cfPassphrase?.Dispose(); } + } + + internal static SafeSecCertificateHandle X509ImportCertificate( + byte[] bytes, + X509ContentType contentType, + SafeCreateHandle importPassword, + SafeKeychainHandle keychain, + bool exportable, + out SafeSecIdentityHandle identityHandle) + { + SafeSecCertificateHandle certHandle; + int osStatus; + + SafeCreateHandle cfPassphrase = importPassword ?? s_nullExportString; + + int ret = AppleCryptoNative_X509ImportCertificate( + bytes, + bytes.Length, + contentType, + cfPassphrase, + keychain, + exportable ? 1 : 0, + out certHandle, + out identityHandle, + out osStatus); + + SafeTemporaryKeychainHandle.TrackItem(certHandle); + SafeTemporaryKeychainHandle.TrackItem(identityHandle); if (ret == 1) { @@ -385,6 +403,56 @@ internal static SafeSecIdentityHandle X509CopyWithPrivateKey( throw new CryptographicException(); } + internal static SafeSecIdentityHandle X509MoveToKeychain( + SafeSecCertificateHandle cert, + SafeKeychainHandle targetKeychain, + SafeSecKeyRefHandle privateKey) + { + SafeSecIdentityHandle identityHandle; + int osStatus; + + int result = AppleCryptoNative_X509MoveToKeychain( + cert, + targetKeychain, + privateKey ?? SafeSecKeyRefHandle.InvalidHandle, + out identityHandle, + out osStatus); + + if (result == 0) + { + identityHandle.Dispose(); + throw CreateExceptionForOSStatus(osStatus); + } + + if (result != 1) + { + Debug.Fail($"AppleCryptoNative_X509MoveToKeychain returned {result}"); + throw new CryptographicException(); + } + + if (privateKey?.IsInvalid == false) + { + // If a PFX has a mismatched association between a private key and the + // certificate public key then MoveToKeychain will write the NULL SecIdentityRef + // (after cleaning up the temporary key). + // + // When that happens, just treat the import as public-only. + if (!identityHandle.IsInvalid) + { + return identityHandle; + } + } + + // If the cert in the PFX had no key, but it was imported with PersistKeySet (imports into + // the default keychain) and a matching private key was already there, then an + // identityHandle could be found. But that's not desirable, since neither Windows or Linux would + // do that matching. + // + // So dispose the handle, no matter what. + identityHandle.Dispose(); + return null; + } + private static byte[] X509Export(X509ContentType contentType, SafeCreateHandle cfPassphrase, IntPtr[] certHandles) { Debug.Assert(contentType == X509ContentType.Pkcs7 || contentType == X509ContentType.Pkcs12); diff --git a/src/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.X509.cs b/src/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.X509.cs index 8ffc70af6a54..1a871f788d51 100644 --- a/src/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.X509.cs +++ b/src/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.X509.cs @@ -21,7 +21,7 @@ internal static partial class Crypto internal static extern SafeX509CrlHandle DecodeX509Crl(byte[] buf, int len); [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_DecodeX509")] - internal static extern SafeX509Handle DecodeX509(byte[] buf, int len); + internal static extern SafeX509Handle DecodeX509(ref byte buf, int len); [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_GetX509DerSize")] internal static extern int GetX509DerSize(SafeX509Handle x); diff --git a/src/Common/src/System/Security/Cryptography/Asn1/AlgorithmIdentifierAsn.manual.cs b/src/Common/src/System/Security/Cryptography/Asn1/AlgorithmIdentifierAsn.manual.cs index 9a599738f439..f35038735b46 100644 --- a/src/Common/src/System/Security/Cryptography/Asn1/AlgorithmIdentifierAsn.manual.cs +++ b/src/Common/src/System/Security/Cryptography/Asn1/AlgorithmIdentifierAsn.manual.cs @@ -38,7 +38,7 @@ internal bool HasNullEquivalentParameters() return RepresentsNull(Parameters); } - private static bool RepresentsNull(ReadOnlyMemory? parameters) + internal static bool RepresentsNull(ReadOnlyMemory? parameters) { if (parameters == null) { diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/DigestInfoAsn.xml b/src/Common/src/System/Security/Cryptography/Asn1/DigestInfoAsn.xml similarity index 91% rename from src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/DigestInfoAsn.xml rename to src/Common/src/System/Security/Cryptography/Asn1/DigestInfoAsn.xml index 2484455f82f4..06a0a07e8d45 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/DigestInfoAsn.xml +++ b/src/Common/src/System/Security/Cryptography/Asn1/DigestInfoAsn.xml @@ -2,7 +2,7 @@ + namespace="System.Security.Cryptography.Asn1"> - + diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/MacData.xml.cs b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/MacData.xml.cs similarity index 94% rename from src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/MacData.xml.cs rename to src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/MacData.xml.cs index a5b7368e9958..3c2d465e68c5 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/MacData.xml.cs +++ b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/MacData.xml.cs @@ -8,14 +8,14 @@ using System.Security.Cryptography; using System.Security.Cryptography.Asn1; -namespace System.Security.Cryptography.Pkcs.Asn1 +namespace System.Security.Cryptography.Asn1.Pkcs12 { [StructLayout(LayoutKind.Sequential)] internal partial struct MacData { private static readonly byte[] s_defaultIterationCount = { 0x02, 0x01, 0x01 }; - internal System.Security.Cryptography.Pkcs.Asn1.DigestInfoAsn Mac; + internal System.Security.Cryptography.Asn1.DigestInfoAsn Mac; internal ReadOnlyMemory MacSalt; internal int IterationCount; @@ -96,7 +96,7 @@ internal static void Decode(AsnReader reader, Asn1Tag expectedTag, out MacData d AsnReader sequenceReader = reader.ReadSequence(expectedTag); AsnReader defaultReader; - System.Security.Cryptography.Pkcs.Asn1.DigestInfoAsn.Decode(sequenceReader, out decoded.Mac); + System.Security.Cryptography.Asn1.DigestInfoAsn.Decode(sequenceReader, out decoded.Mac); if (sequenceReader.TryReadPrimitiveOctetStringBytes(out ReadOnlyMemory tmpMacSalt)) { diff --git a/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs new file mode 100644 index 000000000000..0856a0f314a0 --- /dev/null +++ b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.Security.Cryptography.Pkcs; + +namespace System.Security.Cryptography.Asn1.Pkcs12 +{ + internal partial struct PfxAsn + { + internal bool VerifyMac( + ReadOnlySpan macPassword, + ReadOnlySpan authSafeContents) + { + Debug.Assert(MacData.HasValue); + + HashAlgorithmName hashAlgorithm; + int expectedOutputSize; + + string algorithmValue = MacData.Value.Mac.DigestAlgorithm.Algorithm.Value; + + switch (algorithmValue) + { + case Oids.Md5: + expectedOutputSize = 128 >> 3; + hashAlgorithm = HashAlgorithmName.MD5; + break; + case Oids.Sha1: + expectedOutputSize = 160 >> 3; + hashAlgorithm = HashAlgorithmName.SHA1; + break; + case Oids.Sha256: + expectedOutputSize = 256 >> 3; + hashAlgorithm = HashAlgorithmName.SHA256; + break; + case Oids.Sha384: + expectedOutputSize = 384 >> 3; + hashAlgorithm = HashAlgorithmName.SHA384; + break; + case Oids.Sha512: + expectedOutputSize = 512 >> 3; + hashAlgorithm = HashAlgorithmName.SHA512; + break; + default: + throw new CryptographicException( + SR.Format(SR.Cryptography_UnknownHashAlgorithm, algorithmValue)); + } + + if (MacData.Value.Mac.Digest.Length != expectedOutputSize) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // Cannot use the ArrayPool or stackalloc here because CreateHMAC needs a properly bounded array. + byte[] derived = new byte[expectedOutputSize]; + + int iterationCount = + PasswordBasedEncryption.NormalizeIterationCount(MacData.Value.IterationCount); + + Pkcs12Kdf.DeriveMacKey( + macPassword, + hashAlgorithm, + iterationCount, + MacData.Value.MacSalt.Span, + derived); + + using (IncrementalHash hmac = IncrementalHash.CreateHMAC(hashAlgorithm, derived)) + { + hmac.AppendData(authSafeContents); + + if (!hmac.TryGetHashAndReset(derived, out int bytesWritten) || bytesWritten != expectedOutputSize) + { + Debug.Fail($"TryGetHashAndReset wrote {bytesWritten} bytes when {expectedOutputSize} was expected"); + throw new CryptographicException(); + } + + return CryptographicOperations.FixedTimeEquals( + derived, + MacData.Value.Mac.Digest.Span); + } + } + } +} diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/PfxAsn.xml b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.xml similarity index 79% rename from src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/PfxAsn.xml rename to src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.xml index f27a9879fa07..a46703b33d3a 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/PfxAsn.xml +++ b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.xml @@ -2,7 +2,7 @@ + namespace="System.Security.Cryptography.Asn1.Pkcs12"> - - + + diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/PfxAsn.xml.cs b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.xml.cs similarity index 83% rename from src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/PfxAsn.xml.cs rename to src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.xml.cs index 85d8a6113f6b..3ba2fc3a40e3 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/PfxAsn.xml.cs +++ b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.xml.cs @@ -8,14 +8,14 @@ using System.Security.Cryptography; using System.Security.Cryptography.Asn1; -namespace System.Security.Cryptography.Pkcs.Asn1 +namespace System.Security.Cryptography.Asn1.Pkcs12 { [StructLayout(LayoutKind.Sequential)] internal partial struct PfxAsn { internal byte Version; - internal System.Security.Cryptography.Pkcs.Asn1.ContentInfoAsn AuthSafe; - internal System.Security.Cryptography.Pkcs.Asn1.MacData? MacData; + internal System.Security.Cryptography.Asn1.Pkcs7.ContentInfoAsn AuthSafe; + internal System.Security.Cryptography.Asn1.Pkcs12.MacData? MacData; internal void Encode(AsnWriter writer) { @@ -73,12 +73,12 @@ internal static void Decode(AsnReader reader, Asn1Tag expectedTag, out PfxAsn de sequenceReader.ThrowIfNotEmpty(); } - System.Security.Cryptography.Pkcs.Asn1.ContentInfoAsn.Decode(sequenceReader, out decoded.AuthSafe); + System.Security.Cryptography.Asn1.Pkcs7.ContentInfoAsn.Decode(sequenceReader, out decoded.AuthSafe); if (sequenceReader.HasData && sequenceReader.PeekTag().HasSameClassAndValue(Asn1Tag.Sequence)) { - System.Security.Cryptography.Pkcs.Asn1.MacData tmpMacData; - System.Security.Cryptography.Pkcs.Asn1.MacData.Decode(sequenceReader, out tmpMacData); + System.Security.Cryptography.Asn1.Pkcs12.MacData tmpMacData; + System.Security.Cryptography.Asn1.Pkcs12.MacData.Decode(sequenceReader, out tmpMacData); decoded.MacData = tmpMacData; } diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/SafeBagAsn.xml b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/SafeBagAsn.xml similarity index 92% rename from src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/SafeBagAsn.xml rename to src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/SafeBagAsn.xml index f4ae8d356b87..8c5d83492d7d 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/SafeBagAsn.xml +++ b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/SafeBagAsn.xml @@ -2,7 +2,7 @@ + namespace="System.Security.Cryptography.Asn1.Pkcs12"> - + diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EncryptedDataAsn.xml.cs b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs7/EncryptedDataAsn.xml.cs similarity index 92% rename from src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EncryptedDataAsn.xml.cs rename to src/Common/src/System/Security/Cryptography/Asn1/Pkcs7/EncryptedDataAsn.xml.cs index e200d8882efc..1186c71f0da8 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EncryptedDataAsn.xml.cs +++ b/src/Common/src/System/Security/Cryptography/Asn1/Pkcs7/EncryptedDataAsn.xml.cs @@ -9,13 +9,13 @@ using System.Security.Cryptography; using System.Security.Cryptography.Asn1; -namespace System.Security.Cryptography.Pkcs.Asn1 +namespace System.Security.Cryptography.Asn1.Pkcs7 { [StructLayout(LayoutKind.Sequential)] internal partial struct EncryptedDataAsn { internal int Version; - internal System.Security.Cryptography.Pkcs.Asn1.EncryptedContentInfoAsn EncryptedContentInfo; + internal System.Security.Cryptography.Asn1.Pkcs7.EncryptedContentInfoAsn EncryptedContentInfo; internal System.Security.Cryptography.Asn1.AttributeAsn[] UnprotectedAttributes; internal void Encode(AsnWriter writer) @@ -82,7 +82,7 @@ internal static void Decode(AsnReader reader, Asn1Tag expectedTag, out Encrypted sequenceReader.ThrowIfNotEmpty(); } - System.Security.Cryptography.Pkcs.Asn1.EncryptedContentInfoAsn.Decode(sequenceReader, out decoded.EncryptedContentInfo); + System.Security.Cryptography.Asn1.Pkcs7.EncryptedContentInfoAsn.Decode(sequenceReader, out decoded.EncryptedContentInfo); if (sequenceReader.HasData && sequenceReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 1))) { diff --git a/src/Common/src/System/Security/Cryptography/CryptoPool.cs b/src/Common/src/System/Security/Cryptography/CryptoPool.cs index 7592a9dc7cfc..73d1a70125e6 100644 --- a/src/Common/src/System/Security/Cryptography/CryptoPool.cs +++ b/src/Common/src/System/Security/Cryptography/CryptoPool.cs @@ -13,6 +13,14 @@ internal static class CryptoPool internal static byte[] Rent(int minimumLength) => ArrayPool.Shared.Rent(minimumLength); + internal static void Return(ArraySegment arraySegment) + { + Debug.Assert(arraySegment.Array != null); + Debug.Assert(arraySegment.Offset == 0); + + Return(arraySegment.Array, arraySegment.Count); + } + internal static void Return(byte[] array, int clearSize = ClearAll) { Debug.Assert(clearSize <= array.Length); diff --git a/src/Native/Unix/System.Security.Cryptography.Native.Apple/pal_x509.c b/src/Native/Unix/System.Security.Cryptography.Native.Apple/pal_x509.c index ecd5b6c5806e..b6d620de3bd3 100644 --- a/src/Native/Unix/System.Security.Cryptography.Native.Apple/pal_x509.c +++ b/src/Native/Unix/System.Security.Cryptography.Native.Apple/pal_x509.c @@ -528,13 +528,12 @@ int32_t AppleCryptoNative_X509GetRawData(SecCertificateRef cert, CFDataRef* ppDa return (*pOSStatus == noErr); } -static OSStatus AddKeyToKeychain(SecKeyRef privateKey, SecKeychainRef targetKeychain) +static OSStatus AddKeyToKeychain(SecKeyRef privateKey, SecKeychainRef targetKeychain, SecKeyRef* importedKey) { // This is quite similar to pal_seckey's ExportImportKey, but // a) is used to put something INTO a keychain, instead of to take it out. // b) Doesn't assume that the input should be CFRelease()d and overwritten. - // c) Doesn't return/emit the imported key reference. - // d) Works on private keys. + // c) Works on private keys. SecExternalFormat dataFormat = kSecFormatWrappedPKCS8; CFDataRef exportData = NULL; @@ -556,6 +555,17 @@ static OSStatus AddKeyToKeychain(SecKeyRef privateKey, SecKeychainRef targetKeyc SecItemImport(exportData, NULL, &actualFormat, &actualType, 0, &keyParams, targetKeychain, &outItems); } + if (status == noErr && importedKey != NULL && outItems != NULL && CFArrayGetCount(outItems) == 1) + { + CFTypeRef outItem = CFArrayGetValueAtIndex(outItems, 0); + + if (CFGetTypeID(outItem) == SecKeyGetTypeID()) + { + CFRetain(outItem); + *importedKey = (SecKeyRef)CONST_CAST(void*, outItem); + } + } + if (exportData != NULL) CFRelease(exportData); @@ -593,7 +603,7 @@ int32_t AppleCryptoNative_X509CopyWithPrivateKey(SecCertificateRef cert, // This only happens with an ephemeral key, so the keychain we're adding it to is temporary. if (status == errSecNoSuchKeychain) { - status = AddKeyToKeychain(privateKey, targetKeychain); + status = AddKeyToKeychain(privateKey, targetKeychain, NULL); } if (itemCopy != NULL) @@ -719,3 +729,188 @@ int32_t AppleCryptoNative_X509CopyWithPrivateKey(SecCertificateRef cert, *pOSStatus = status; return status == noErr; } + +int32_t AppleCryptoNative_X509MoveToKeychain(SecCertificateRef cert, + SecKeychainRef targetKeychain, + SecKeyRef privateKey, + SecIdentityRef* pIdentityOut, + int32_t* pOSStatus) +{ + if (pIdentityOut != NULL) + *pIdentityOut = NULL; + if (pOSStatus != NULL) + *pOSStatus = noErr; + + if (cert == NULL || targetKeychain == NULL || pIdentityOut == NULL || pOSStatus == NULL) + { + return -1; + } + + SecKeychainRef curKeychain = NULL; + SecKeyRef importedKey = NULL; + OSStatus status = SecKeychainItemCopyKeychain((SecKeychainItemRef)cert, &curKeychain); + + if (status == errSecNoSuchKeychain) + { + status = noErr; + } + else + { + if (curKeychain != NULL) + { + CFRelease(curKeychain); + } + + if (status == noErr) + { + // Usage error: The certificate should have been freshly imported by the PFX loader, + // and therefore have no keychain. + return -2; + } + } + + if (status == noErr && privateKey != NULL) + { + status = SecKeychainItemCopyKeychain((SecKeychainItemRef)privateKey, &curKeychain); + + if (status == errSecNoSuchKeychain) + { + status = AddKeyToKeychain(privateKey, targetKeychain, &importedKey); + + // A duplicate key import will be the only time that status is noErr + // and importedKey is NULL. + if (status == errSecDuplicateItem) + { + status = noErr; + } + } + else + { + if (curKeychain != NULL) + { + CFRelease(curKeychain); + } + + if (status == noErr) + { + // This is a usage error, the only expected call is from the PFX loader, + // which has an ephemeral key reference, therefore no keychain. + return -3; + } + } + } + + if (status == noErr) + { + status = SecCertificateAddToKeychain(cert, targetKeychain); + + if (status == errSecDuplicateItem) + { + status = noErr; + } + } + + if (status == noErr && privateKey != NULL) + { + CFMutableDictionaryRef query = NULL; + CFArrayRef searchList = NULL; + CFArrayRef itemMatch = NULL; + CFTypeRef result = NULL; + + if (status == noErr) + { + query = CFDictionaryCreateMutable( + kCFAllocatorDefault, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); + + if (query == NULL) + { + status = errSecAllocate; + } + } + + if (status == noErr) + { + const void* constTargetKeychain = targetKeychain; + searchList = CFArrayCreate(NULL, (const void**)(&constTargetKeychain), 1, &kCFTypeArrayCallBacks); + + if (searchList == NULL) + { + status = errSecAllocate; + } + } + + if (status == noErr) + { + const void* constCert = cert; + itemMatch = CFArrayCreate(NULL, (const void**)(&constCert), 1, &kCFTypeArrayCallBacks); + + if (itemMatch == NULL) + { + status = errSecAllocate; + } + } + + if (status == noErr) + { + CFDictionarySetValue(query, kSecReturnRef, kCFBooleanTrue); + CFDictionarySetValue(query, kSecMatchSearchList, searchList); + CFDictionarySetValue(query, kSecMatchItemList, itemMatch); + CFDictionarySetValue(query, kSecClass, kSecClassIdentity); + + status = SecItemCopyMatching(query, &result); + + if (status != noErr && result != NULL) + { + CFRelease(result); + result = NULL; + } + + if (result != NULL) + { + if (CFGetTypeID(result) == SecIdentityGetTypeID()) + { + SecIdentityRef identity = (SecIdentityRef)CONST_CAST(void*, result); + CFRetain(identity); + *pIdentityOut = identity; + } + } + + if (status == errSecItemNotFound && importedKey != NULL) + { + // An identity can't be found. + // That means that the private key does not match the certificate public key. + // Since we know we added the key, and nothing will reference it now, try to remove it. + const void* constKey = importedKey; + CFArrayRef newItemMatch = CFArrayCreate(NULL, (const void**)(&constKey), 1, &kCFTypeArrayCallBacks); + CFDictionarySetValue(query, kSecMatchItemList, newItemMatch); + CFRelease(itemMatch); + itemMatch = newItemMatch; + + CFDictionarySetValue(query, kSecClass, kSecClassKey); + + // Even if the key delete failed, there's nothing the user can do about it now. + // Ignore the result of delete and just return to noErr + SecItemDelete(query); + status = noErr; + } + } + + if (result != NULL) + CFRelease(result); + + if (itemMatch != NULL) + CFRelease(itemMatch); + + if (searchList != NULL) + CFRelease(searchList); + + if (query != NULL) + CFRelease(query); + } + + if (importedKey != NULL) + CFRelease(importedKey); + + *pOSStatus = status; + return status == noErr; +} diff --git a/src/Native/Unix/System.Security.Cryptography.Native.Apple/pal_x509.h b/src/Native/Unix/System.Security.Cryptography.Native.Apple/pal_x509.h index 1f468bd221c9..844065f92427 100644 --- a/src/Native/Unix/System.Security.Cryptography.Native.Apple/pal_x509.h +++ b/src/Native/Unix/System.Security.Cryptography.Native.Apple/pal_x509.h @@ -174,3 +174,20 @@ DLLEXPORT int32_t AppleCryptoNative_X509CopyWithPrivateKey(SecCertificateRef cer SecKeychainRef targetKeychain, SecIdentityRef* pIdentityOut, int32_t* pOSStatus); + +/* +Move the specified certificate and key to the target keychain. +Both the certificate and the key must be ephemeral (not a member of any keychain). +If the private key was specified then search for an identity and present it via pIdentityOut. + +Returns 1 on success, 0 on failure, any other value indicates invalid state. + +Output: +pIdentityOut: Receives the SecIdentityRef of the mated cert/key pair, when applicable. +pOSStatus: Receives the result of the last executed system call. +*/ +DLLEXPORT int32_t AppleCryptoNative_X509MoveToKeychain(SecCertificateRef cert, + SecKeychainRef keychain, + SecKeyRef privateKey, + SecIdentityRef* pIdentityOut, + int32_t* pOSStatus); diff --git a/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Asn.cs b/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Asn.cs index 778ff54a405a..0c4bcdbc9065 100644 --- a/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Asn.cs +++ b/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Asn.cs @@ -3,10 +3,9 @@ // See the LICENSE file in the project root for more information. using System; -using System.Diagnostics; using System.Security.Cryptography; using System.Security.Cryptography.Asn1; -using System.Security.Cryptography.Pkcs.Asn1; +using System.Security.Cryptography.Asn1.Pkcs7; namespace Internal.Cryptography.Pal.AnyOS { diff --git a/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Decode.cs b/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Decode.cs index b08ead2c1991..a8a0dacfc56b 100644 --- a/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Decode.cs +++ b/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/Pal/AnyOS/ManagedPal.Decode.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Security.Cryptography; using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs7; using System.Security.Cryptography.Pkcs; using System.Security.Cryptography.Pkcs.Asn1; using System.Security.Cryptography.X509Certificates; diff --git a/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/PkcsHelpers.cs b/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/PkcsHelpers.cs index 1a1c24274a33..fd907baa395b 100644 --- a/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/PkcsHelpers.cs +++ b/src/System.Security.Cryptography.Pkcs/src/Internal/Cryptography/PkcsHelpers.cs @@ -11,8 +11,8 @@ using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs7; using System.Security.Cryptography.Pkcs; -using System.Security.Cryptography.Pkcs.Asn1; using System.Security.Cryptography.X509Certificates; using X509IssuerSerial = System.Security.Cryptography.Xml.X509IssuerSerial; diff --git a/src/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj b/src/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj index d019d4d1ca66..388255081361 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj +++ b/src/System.Security.Cryptography.Pkcs/src/System.Security.Cryptography.Pkcs.csproj @@ -60,6 +60,20 @@ + + 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 + @@ -69,10 +83,6 @@ - - - System\Security\Cryptography\Pkcs\Asn1\EncryptedContentInfoAsn.xml - System\Security\Cryptography\Pkcs\Asn1\EnvelopedDataAsn.xml @@ -557,10 +567,6 @@ System\Security\Cryptography\Pkcs\Asn1\CertificateChoiceAsn.xml - - - System\Security\Cryptography\Pkcs\Asn1\ContentInfoAsn.xml - System\Security\Cryptography\Pkcs\Asn1\EncapsulatedContentInfoAsn.xml @@ -650,35 +656,57 @@ Common\System\Security\Cryptography\KeyFormatHelper.cs - - Common\System\Security\Cryptography\PasswordBasedEncryption.cs + + 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\Pkcs12Kdf.cs + + 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 - - - System\Security\Cryptography\Pkcs\Asn1\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 - - - System\Security\Cryptography\Pkcs\Asn1\DigestInfoAsn.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 - - - System\Security\Cryptography\Pkcs\Asn1\EncryptedDataAsn.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 - - - System\Security\Cryptography\Pkcs\Asn1\MacData.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 - - - System\Security\Cryptography\Pkcs\Asn1\PfxAsn.xml + + Common\System\Security\Cryptography\PasswordBasedEncryption.cs - - - System\Security\Cryptography\Pkcs\Asn1\SafeBagAsn.xml + + Common\System\Security\Cryptography\Pkcs12Kdf.cs diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EnvelopedDataAsn.xml b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EnvelopedDataAsn.xml index 905ecb117879..54508f8c1ea4 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EnvelopedDataAsn.xml +++ b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EnvelopedDataAsn.xml @@ -22,7 +22,7 @@ - + diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EnvelopedDataAsn.xml.cs b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EnvelopedDataAsn.xml.cs index c3d4f0ba3068..e1dcb2c685c0 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EnvelopedDataAsn.xml.cs +++ b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1/EnvelopedDataAsn.xml.cs @@ -17,7 +17,7 @@ internal partial struct EnvelopedDataAsn internal int Version; internal System.Security.Cryptography.Pkcs.Asn1.OriginatorInfoAsn? OriginatorInfo; internal System.Security.Cryptography.Pkcs.Asn1.RecipientInfoAsn[] RecipientInfos; - internal System.Security.Cryptography.Pkcs.Asn1.EncryptedContentInfoAsn EncryptedContentInfo; + internal System.Security.Cryptography.Asn1.Pkcs7.EncryptedContentInfoAsn EncryptedContentInfo; internal System.Security.Cryptography.Asn1.AttributeAsn[] UnprotectedAttributes; internal void Encode(AsnWriter writer) @@ -123,7 +123,7 @@ internal static void Decode(AsnReader reader, Asn1Tag expectedTag, out Enveloped decoded.RecipientInfos = tmpList.ToArray(); } - System.Security.Cryptography.Pkcs.Asn1.EncryptedContentInfoAsn.Decode(sequenceReader, out decoded.EncryptedContentInfo); + System.Security.Cryptography.Asn1.Pkcs7.EncryptedContentInfoAsn.Decode(sequenceReader, out decoded.EncryptedContentInfo); if (sequenceReader.HasData && sequenceReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 1))) { diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12Builder.cs b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12Builder.cs index de795116e9d1..6cc9858d267f 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12Builder.cs +++ b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12Builder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs7; using System.Security.Cryptography.Pkcs.Asn1; using Internal.Cryptography; diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12CertBag.cs b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12CertBag.cs index b0d287cace88..b9967b57bd0c 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12CertBag.cs +++ b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12CertBag.cs @@ -3,7 +3,7 @@ // See the LICENSE file in the project root for more information. using System.Security.Cryptography.Asn1; -using System.Security.Cryptography.Pkcs.Asn1; +using System.Security.Cryptography.Asn1.Pkcs12; using System.Security.Cryptography.X509Certificates; using Internal.Cryptography; diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12Info.cs b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12Info.cs index 1455b5260b11..0422a6f9bd82 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12Info.cs +++ b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12Info.cs @@ -4,8 +4,9 @@ using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Diagnostics; using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs12; +using System.Security.Cryptography.Asn1.Pkcs7; using System.Security.Cryptography.Pkcs.Asn1; using Internal.Cryptography; @@ -40,72 +41,7 @@ public bool VerifyMac(ReadOnlySpan password) IntegrityMode)); } - Debug.Assert(_decoded.MacData.HasValue); - - HashAlgorithmName hashAlgorithm; - int expectedOutputSize; - - string algorithmValue = _decoded.MacData.Value.Mac.DigestAlgorithm.Algorithm.Value; - - switch (algorithmValue) - { - case Oids.Md5: - expectedOutputSize = 128 >> 3; - hashAlgorithm = HashAlgorithmName.MD5; - break; - case Oids.Sha1: - expectedOutputSize = 160 >> 3; - hashAlgorithm = HashAlgorithmName.SHA1; - break; - case Oids.Sha256: - expectedOutputSize = 256 >> 3; - hashAlgorithm = HashAlgorithmName.SHA256; - break; - case Oids.Sha384: - expectedOutputSize = 384 >> 3; - hashAlgorithm = HashAlgorithmName.SHA384; - break; - case Oids.Sha512: - expectedOutputSize = 512 >> 3; - hashAlgorithm = HashAlgorithmName.SHA512; - break; - default: - throw new CryptographicException( - SR.Format(SR.Cryptography_UnknownHashAlgorithm, algorithmValue)); - } - - if (_decoded.MacData.Value.Mac.Digest.Length != expectedOutputSize) - { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); - } - - // Cannot use the ArrayPool or stackalloc here because CreateHMAC needs a properly bounded array. - byte[] derived = new byte[expectedOutputSize]; - - int iterationCount = - PasswordBasedEncryption.NormalizeIterationCount(_decoded.MacData.Value.IterationCount); - - Pkcs12Kdf.DeriveMacKey( - password, - hashAlgorithm, - iterationCount, - _decoded.MacData.Value.MacSalt.Span, - derived); - - using (IncrementalHash hmac = IncrementalHash.CreateHMAC(hashAlgorithm, derived)) - { - hmac.AppendData(_authSafeContents.Span); - - if (!hmac.TryGetHashAndReset(derived, out int bytesWritten) || bytesWritten != expectedOutputSize) - { - Debug.Fail($"TryGetHashAndReset wrote {bytesWritten} bytes when {expectedOutputSize} was expected"); - throw new CryptographicException(); - } - - return CryptographicOperations.FixedTimeEquals( - derived, - _decoded.MacData.Value.Mac.Digest.Span); - } + return _decoded.VerifyMac(password, _authSafeContents.Span); } public static Pkcs12Info Decode( diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12SafeContents.cs b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12SafeContents.cs index cb90430d4d7b..3e8c8a66e5d2 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12SafeContents.cs +++ b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Pkcs12SafeContents.cs @@ -2,12 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Security.Cryptography.Asn1; -using System.Security.Cryptography.Pkcs.Asn1; +using System.Security.Cryptography.Asn1.Pkcs12; +using System.Security.Cryptography.Asn1.Pkcs7; using System.Security.Cryptography.X509Certificates; using Internal.Cryptography; diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampToken.cs b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampToken.cs index c224915fc827..d6c820bcf589 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampToken.cs +++ b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Rfc3161TimestampToken.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.Linq; using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs7; using System.Security.Cryptography.Pkcs.Asn1; using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.Xml; diff --git a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs index 4942f526ce64..52c633a52267 100644 --- a/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs +++ b/src/System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/SignedCms.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs7; using System.Security.Cryptography.Pkcs.Asn1; using System.Security.Cryptography.X509Certificates; using Internal.Cryptography; diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Helpers.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Helpers.cs index 09f5ebfb78a7..19538aed2634 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Helpers.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Helpers.cs @@ -21,7 +21,7 @@ public static char[] ToHexArrayUpper(this byte[] bytes) return chars; } - private static void ToHexArrayUpper(byte[] bytes, Span chars) + private static void ToHexArrayUpper(ReadOnlySpan bytes, Span chars) { Debug.Assert(chars.Length >= bytes.Length * 2); int i = 0; @@ -208,6 +208,35 @@ public static void ValidateDer(ReadOnlyMemory encodedValue) reader.ReadEncodedValue(); } } + + public static ReadOnlyMemory DecodeOctetStringAsMemory(ReadOnlyMemory encodedOctetString) + { + AsnReader reader = new AsnReader(encodedOctetString, AsnEncodingRules.BER); + + if (reader.PeekEncodedValue().Length != encodedOctetString.Length) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + // Almost everything in X.509 is DER-encoded, which means Octet String values are + // encoded as a primitive (non-segmented) + // + // Even in BER Octet Strings are usually encoded as a primitive. + if (reader.TryReadPrimitiveOctetStringBytes(out ReadOnlyMemory primitiveContents)) + { + return primitiveContents; + } + + byte[] tooBig = new byte[encodedOctetString.Length]; + + if (reader.TryCopyOctetStringBytes(tooBig, out int bytesWritten)) + { + return tooBig.AsMemory(0, bytesWritten); + } + + Debug.Fail("TryCopyOctetStringBytes failed with an over-allocated array"); + throw new CryptographicException(); + } } internal static class DictionaryStringHelper diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.Pkcs12.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.Pkcs12.cs new file mode 100644 index 000000000000..2074fb43aad9 --- /dev/null +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.Pkcs12.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Security.Cryptography.Apple; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Win32.SafeHandles; + +namespace Internal.Cryptography.Pal +{ + internal sealed partial class AppleCertificatePal : ICertificatePal + { + private static ICertificatePal ImportPkcs12( + byte[] rawData, + SafePasswordHandle password, + bool exportable, + SafeKeychainHandle keychain) + { + using (ApplePkcs12Reader reader = new ApplePkcs12Reader(rawData)) + { + reader.Decrypt(password); + + 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 ICertificatePal ImportPkcs12NonExportable( + AppleCertificatePal cert, + SafeSecKeyRefHandle privateKey, + SafePasswordHandle password, + SafeKeychainHandle keychain) + { + Pkcs12SmallExport exporter = new Pkcs12SmallExport(new TempExportPal(cert), privateKey); + byte[] smallPfx = exporter.Export(X509ContentType.Pkcs12, password); + + SafeSecIdentityHandle identityHandle; + SafeSecCertificateHandle certHandle = Interop.AppleCrypto.X509ImportCertificate( + smallPfx, + X509ContentType.Pkcs12, + password, + keychain, + exportable: false, + out identityHandle); + + // On Windows and Linux if a PFX uses a LocalKeyId to bind the wrong key to a cert, the + // nonsensical object of "this cert, that key" is returned. + // + // On macOS, because we can't forge CFIdentityRefs without the keychain, we're subject to + // Apple's more stringent matching of a consistent keypair. + if (identityHandle.IsInvalid) + { + identityHandle.Dispose(); + return new AppleCertificatePal(certHandle); + } + + certHandle.Dispose(); + return new AppleCertificatePal(identityHandle); + } + + private sealed class Pkcs12SmallExport : UnixExportProvider + { + private readonly SafeSecKeyRefHandle _privateKey; + + internal Pkcs12SmallExport(ICertificatePalCore cert, SafeSecKeyRefHandle privateKey) + : base(cert) + { + Debug.Assert(!privateKey.IsInvalid); + _privateKey = privateKey; + } + + protected override byte[] ExportPkcs7() => throw new NotImplementedException(); + + protected override byte[] ExportPkcs8(ICertificatePalCore certificatePal, ReadOnlySpan password) + { + return AppleCertificatePal.ExportPkcs8(_privateKey, password); + } + } + + private sealed class TempExportPal : ICertificatePalCore + { + private readonly ICertificatePal _realPal; + + internal TempExportPal(AppleCertificatePal realPal) + { + _realPal = realPal; + } + + public bool HasPrivateKey => true; + + public void Dispose() + { + // No-op. + } + + // Forwarders to make the interface compliant. + public IntPtr Handle => _realPal.Handle; + public string Issuer => _realPal.Issuer; + public string Subject => _realPal.Subject; + public string LegacyIssuer => _realPal.LegacyIssuer; + public string LegacySubject => _realPal.LegacySubject; + public byte[] Thumbprint => _realPal.Thumbprint; + public string KeyAlgorithm => _realPal.KeyAlgorithm; + public byte[] KeyAlgorithmParameters => _realPal.KeyAlgorithmParameters; + public byte[] PublicKeyValue => _realPal.PublicKeyValue; + public byte[] SerialNumber => _realPal.SerialNumber; + public string SignatureAlgorithm => _realPal.SignatureAlgorithm; + public DateTime NotAfter => _realPal.NotAfter; + public DateTime NotBefore => _realPal.NotBefore; + public byte[] RawData => _realPal.RawData; + public byte[] Export(X509ContentType contentType, SafePasswordHandle password) => + _realPal.Export(contentType, password); + } + } +} diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.cs index 8d68a6473ff9..bcf623a1358e 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.cs @@ -3,17 +3,19 @@ // See the LICENSE file in the project root for more information. using System; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Security.Cryptography; using System.Security.Cryptography.Apple; +using System.Security.Cryptography.Asn1; using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.Win32.SafeHandles; namespace Internal.Cryptography.Pal { - internal sealed class AppleCertificatePal : ICertificatePal + internal sealed partial class AppleCertificatePal : ICertificatePal { private SafeSecIdentityHandle _identityHandle; private SafeSecCertificateHandle _certHandle; @@ -90,10 +92,6 @@ public static ICertificatePal FromBlob( throw new CryptographicException(SR.Cryptography_X509_PKCS7_NoSigner); } - bool exportable = true; - - SafeKeychainHandle keychain; - if (contentType == X509ContentType.Pkcs12) { if ((keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) == X509KeyStorageFlags.EphemeralKeySet) @@ -101,51 +99,41 @@ public static ICertificatePal FromBlob( throw new PlatformNotSupportedException(SR.Cryptography_X509_NoEphemeralPfx); } - exportable = (keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable; + bool exportable = (keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable; bool persist = (keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet; - keychain = persist + SafeKeychainHandle keychain = persist ? Interop.AppleCrypto.SecKeychainCopyDefault() : Interop.AppleCrypto.CreateTemporaryKeychain(); - } - else - { - keychain = SafeTemporaryKeychainHandle.InvalidHandle; - password = SafePasswordHandle.InvalidHandle; - } - - using (keychain) - { - SafeSecIdentityHandle identityHandle; - SafeSecCertificateHandle certHandle = Interop.AppleCrypto.X509ImportCertificate( - rawData, - contentType, - password, - keychain, - exportable, - out identityHandle); - if (identityHandle.IsInvalid) + using (keychain) { - identityHandle.Dispose(); - return new AppleCertificatePal(certHandle); + return ImportPkcs12(rawData, password, exportable, keychain); } + } - if (contentType != X509ContentType.Pkcs12) - { - Debug.Fail("Non-PKCS12 import produced an identity handle"); - - identityHandle.Dispose(); - certHandle.Dispose(); - throw new CryptographicException(); - } + SafeSecIdentityHandle identityHandle; + SafeSecCertificateHandle certHandle = Interop.AppleCrypto.X509ImportCertificate( + rawData, + contentType, + SafePasswordHandle.InvalidHandle, + SafeTemporaryKeychainHandle.InvalidHandle, + exportable: true, + out identityHandle); - Debug.Assert(certHandle.IsInvalid); - certHandle.Dispose(); - return new AppleCertificatePal(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(); } public static ICertificatePal FromFile(string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags) @@ -359,6 +347,44 @@ public byte[] SubjectPublicKeyInfo } } + internal unsafe byte[] ExportPkcs8(ReadOnlySpan password) + { + Debug.Assert(_identityHandle != null); + + using (SafeSecKeyRefHandle key = Interop.AppleCrypto.X509GetPrivateKeyFromIdentity(_identityHandle)) + { + return ExportPkcs8(key, password); + } + } + + internal static unsafe byte[] ExportPkcs8(SafeSecKeyRefHandle key, ReadOnlySpan password) + { + using (SafeCFDataHandle data = Interop.AppleCrypto.SecKeyExportData(key, exportPrivate: true, password)) + { + ReadOnlySpan systemExport = Interop.CoreFoundation.CFDataDangerousGetSpan(data); + + fixed (byte* ptr = systemExport) + { + using (PointerMemoryManager manager = new PointerMemoryManager(ptr, systemExport.Length)) + { + // Apple's PKCS8 export exports using PBES2, which Win7, Win8.1, and Apple all fail to + // understand in their PKCS12 readers, so re-encrypt using the Win7 PKCS12-PBE parameters. + // + // Since Apple only reliably exports keys with encrypted PKCS#8 there's not a + // "so export it plaintext and only encrypt it once" option. + using (AsnWriter writer = KeyFormatHelper.ReencryptPkcs8( + password, + manager.Memory, + password, + UnixExportProvider.s_windowsPbe)) + { + return writer.Encode(); + } + } + } + } + } + public RSA GetRSAPrivateKey() { if (_identityHandle == null) @@ -465,6 +491,21 @@ public ICertificatePal CopyWithPrivateKey(RSA privateKey) } } + internal AppleCertificatePal MoveToKeychain(SafeKeychainHandle keychain, SafeSecKeyRefHandle privateKey) + { + SafeSecIdentityHandle identity = Interop.AppleCrypto.X509MoveToKeychain( + _certHandle, + keychain, + privateKey); + + if (identity != null) + { + return new AppleCertificatePal(identity); + } + + return null; + } + private ICertificatePal CopyWithPrivateKey(SecKeyPair keyPair) { if (keyPair.PrivateKey == null) diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/ApplePkcs12Reader.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/ApplePkcs12Reader.cs new file mode 100644 index 000000000000..3d0bb4df5618 --- /dev/null +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/ApplePkcs12Reader.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. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics; +using System.Security.Cryptography; +using System.Security.Cryptography.Apple; +using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Win32.SafeHandles; + +namespace Internal.Cryptography.Pal +{ + internal sealed class ApplePkcs12Reader : UnixPkcs12Reader + { + internal ApplePkcs12Reader(byte[] data) + { + ParsePkcs12(data); + } + + protected override ICertificatePalCore ReadX509Der(ReadOnlyMemory data) + { + SafeSecCertificateHandle certHandle = Interop.AppleCrypto.X509ImportCertificate( + data.ToArray(), + 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.Value) + { + 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.Value); + } + + 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) + { + return rsa.GetKeys().PrivateKey; + } + + if (key is DSAImplementation.DSASecurityTransforms dsa) + { + return dsa.GetKeys().PrivateKey; + } + + return ((ECDsaImplementation.ECDsaSecurityTransforms)key).GetKeys().PrivateKey; + } + } +} diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.ExportPal.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.ExportPal.cs index ba5dae2c87af..d28d9a7bb25b 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.ExportPal.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.ExportPal.cs @@ -12,90 +12,19 @@ namespace Internal.Cryptography.Pal { internal sealed partial class StorePal { - private sealed class AppleCertificateExporter : IExportPal + private sealed class AppleCertificateExporter : UnixExportProvider { - private X509Certificate2Collection _certs; - private ICertificatePalCore _singleCertPal; - public AppleCertificateExporter(ICertificatePalCore cert) + : base(cert) { - _singleCertPal = cert; } public AppleCertificateExporter(X509Certificate2Collection certs) + : base(certs) { - _certs = certs; - } - - public void Dispose() - { - // Don't dispose any of the resources, they're still owned by the caller. - _singleCertPal = null; - _certs = null; - } - - public byte[] Export(X509ContentType contentType, SafePasswordHandle password) - { - Debug.Assert(password != null); - switch (contentType) - { - case X509ContentType.Cert: - return ExportX509Der(); - case X509ContentType.Pkcs12: - return ExportPkcs12(password); - case X509ContentType.Pkcs7: - return ExportPkcs7(); - case X509ContentType.SerializedCert: - case X509ContentType.SerializedStore: - throw new PlatformNotSupportedException(SR.Cryptography_Unix_X509_SerializedExport); - default: - throw new CryptographicException(SR.Cryptography_X509_InvalidContentType); - } - } - - private byte[] ExportX509Der() - { - if (_singleCertPal != null) - { - return _singleCertPal.RawData; - } - - // Windows/Desktop compatibility: Exporting a collection (or store) as - // X509ContentType.Cert returns the equivalent of FirstOrDefault(), - // so anything past _certs[0] is ignored, and an empty collection is - // null (not an Exception) - if (_certs.Count == 0) - { - return null; - } - - return _certs[0].RawData; } - private byte[] ExportPkcs12(SafePasswordHandle password) - { - IntPtr[] certHandles; - - if (_singleCertPal != null) - { - certHandles = new[] { _singleCertPal.Handle }; - } - else - { - certHandles = new IntPtr[_certs.Count]; - - for (int i = 0; i < _certs.Count; i++) - { - certHandles[i] = _certs[i].Handle; - } - } - - byte[] exported = Interop.AppleCrypto.X509ExportPfx(certHandles, password); - GC.KeepAlive(_certs); // ensure certs' safe handles aren't finalized while raw handles are in use - return exported; - } - - private byte[] ExportPkcs7() + protected override byte[] ExportPkcs7() { IntPtr[] certHandles; @@ -116,6 +45,12 @@ private byte[] ExportPkcs7() return Interop.AppleCrypto.X509ExportPkcs7(certHandles); } + + protected override byte[] ExportPkcs8(ICertificatePalCore certificatePal, ReadOnlySpan password) + { + AppleCertificatePal pal = (AppleCertificatePal)certificatePal; + return pal.ExportPkcs8(password); + } } } -} \ No newline at end of file +} diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.LoaderPal.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.LoaderPal.cs index 0657ce677aa9..73bf30d36b0c 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.LoaderPal.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.LoaderPal.cs @@ -6,6 +6,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.Apple; using System.Security.Cryptography.X509Certificates; +using System.Threading; using Microsoft.Win32.SafeHandles; namespace Internal.Cryptography.Pal @@ -56,5 +57,75 @@ 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) + { + ICertificatePal 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); + } + } + } + } } -} \ No newline at end of file +} diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.cs index d572c42c01c2..2fcb404c38bc 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/StorePal.cs @@ -32,9 +32,6 @@ public static ILoaderPal FromBlob(byte[] rawData, SafePasswordHandle password, X X509ContentType contentType = X509Certificate2.GetCertContentType(rawData); - SafeKeychainHandle keychain; - bool exportable = true; - if (contentType == X509ContentType.Pkcs12) { if ((keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) == X509KeyStorageFlags.EphemeralKeySet) @@ -42,36 +39,44 @@ public static ILoaderPal FromBlob(byte[] rawData, SafePasswordHandle password, X throw new PlatformNotSupportedException(SR.Cryptography_X509_NoEphemeralPfx); } - exportable = (keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable; + bool exportable = (keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable; bool persist = (keyStorageFlags & X509KeyStorageFlags.PersistKeySet) == X509KeyStorageFlags.PersistKeySet; - keychain = persist + SafeKeychainHandle keychain = persist ? Interop.AppleCrypto.SecKeychainCopyDefault() : Interop.AppleCrypto.CreateTemporaryKeychain(); + + return ImportPkcs12(rawData, password, exportable, keychain); } - else - { - keychain = SafeTemporaryKeychainHandle.InvalidHandle; - password = SafePasswordHandle.InvalidHandle; - } - // Only dispose tmpKeychain on the exception path, otherwise it's managed by AppleCertLoader. + SafeCFArrayHandle certs = Interop.AppleCrypto.X509ImportCollection( + rawData, + contentType, + password, + SafeTemporaryKeychainHandle.InvalidHandle, + exportable: true); + + return new AppleCertLoader(certs, null); + } + + private static ILoaderPal ImportPkcs12( + byte[] rawData, + SafePasswordHandle password, + bool exportable, + SafeKeychainHandle keychain) + { + ApplePkcs12Reader reader = new ApplePkcs12Reader(rawData); + try { - SafeCFArrayHandle certs = Interop.AppleCrypto.X509ImportCollection( - rawData, - contentType, - password, - keychain, - exportable); - - // If the default keychain was used, null will be passed to the loader. - return new AppleCertLoader(certs, keychain as SafeTemporaryKeychainHandle); + reader.Decrypt(password); + return new ApplePkcs12CertLoader(reader, keychain, password, exportable); } catch { + reader.Dispose(); keychain.Dispose(); throw; } diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/X509Pal.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/X509Pal.cs index 793de73802f6..5c8ac365f613 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/X509Pal.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/X509Pal.cs @@ -8,6 +8,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.Apple; using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs12; using System.Security.Cryptography.X509Certificates; namespace Internal.Cryptography.Pal @@ -152,6 +153,19 @@ public X509ContentType GetCertContentType(byte[] rawData) X509ContentType contentType = Interop.AppleCrypto.X509GetContentType(rawData, rawData.Length); + // Apple doesn't seem to recognize PFX files with no MAC, so try a quick maybe-it's-a-PFX test + if (contentType == X509ContentType.Unknown) + { + try + { + PfxAsn.Decode(rawData, AsnEncodingRules.BER); + contentType = X509ContentType.Pkcs12; + } + catch (CryptographicException) + { + } + } + if (contentType == X509ContentType.Unknown) { // Throw to match Windows and Unix behavior. diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/ExportProvider.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/ExportProvider.cs index f8c78c3b290b..4ce2fc4585ef 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/ExportProvider.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/ExportProvider.cs @@ -10,148 +10,57 @@ namespace Internal.Cryptography.Pal { - internal sealed class ExportProvider : IExportPal + internal sealed class ExportProvider : UnixExportProvider { - private static readonly SafeEvpPKeyHandle InvalidPKeyHandle = new SafeEvpPKeyHandle(IntPtr.Zero, false); - - private ICertificatePalCore _singleCertPal; - private X509Certificate2Collection _certs; - internal ExportProvider(ICertificatePalCore singleCertPal) + : base(singleCertPal) { - _singleCertPal = singleCertPal; } internal ExportProvider(X509Certificate2Collection certs) + : base(certs) { - _certs = certs; } - public void Dispose() + protected override byte[] ExportPkcs8( + ICertificatePalCore certificatePal, + ReadOnlySpan password) { - // Don't dispose any of the resources, they're still owned by the caller. - _singleCertPal = null; - _certs = null; - } + AsymmetricAlgorithm alg = null; + SafeEvpPKeyHandle privateKey = ((OpenSslX509CertificateReader)certificatePal).PrivateKeyHandle; - public byte[] Export(X509ContentType contentType, SafePasswordHandle password) - { - Debug.Assert(password != null); - switch (contentType) + try { - case X509ContentType.Cert: - return ExportX509Der(); - case X509ContentType.Pfx: - return ExportPfx(password); - case X509ContentType.Pkcs7: - return ExportPkcs7(); - case X509ContentType.SerializedCert: - case X509ContentType.SerializedStore: - throw new PlatformNotSupportedException(SR.Cryptography_Unix_X509_SerializedExport); - default: - throw new CryptographicException(SR.Cryptography_X509_InvalidContentType); + alg = new RSAOpenSsl(privateKey); } - } - - private byte[] ExportX509Der() - { - if (_singleCertPal != null) + catch (CryptographicException) { - return _singleCertPal.RawData; } - // Windows/Desktop compatibility: Exporting a collection (or store) as - // X509ContentType.Cert returns the equivalent of FirstOrDefault(), - // so anything past _certs[0] is ignored, and an empty collection is - // null (not an Exception) - if (_certs.Count == 0) + if (alg == null) { - return null; - } - - return _certs[0].RawData; - } - - private byte[] ExportPfx(SafePasswordHandle password) - { - using (SafeX509StackHandle publicCerts = Interop.Crypto.NewX509Stack()) - { - SafeX509Handle privateCertHandle = SafeX509Handle.InvalidHandle; - SafeEvpPKeyHandle privateCertKeyHandle = InvalidPKeyHandle; - - if (_singleCertPal != null) + try { - var certPal = (OpenSslX509CertificateReader)_singleCertPal; - - if (_singleCertPal.HasPrivateKey) - { - privateCertHandle = certPal.SafeHandle; - privateCertKeyHandle = certPal.PrivateKeyHandle; - } - else - { - PushHandle(certPal.Handle, publicCerts); - } - - GC.KeepAlive(certPal); // ensure reader's safe handle isn't finalized while raw handle is in use + alg = new ECDsaOpenSsl(privateKey); } - else + catch (CryptographicException) { - X509Certificate2 privateCert = null; - - // Walk the collection backwards, because we're pushing onto a stack. - // This will cause the read order later to be the same as it was now. - for (int i = _certs.Count - 1; i >= 0; --i) - { - X509Certificate2 cert = _certs[i]; - - if (cert.HasPrivateKey) - { - if (privateCert != null) - { - // OpenSSL's PKCS12 accelerator (PKCS12_create) only supports one - // private key. The data structure supports more than one, but - // being able to use that functionality requires a lot more code for - // a low-usage scenario. - throw new PlatformNotSupportedException(SR.NotSupported_Export_MultiplePrivateCerts); - } - - privateCert = cert; - var certPal = (OpenSslX509CertificateReader)cert.Pal; - privateCertHandle = certPal.SafeHandle; - privateCertKeyHandle = certPal.PrivateKeyHandle; - } - else - { - PushHandle(cert.Handle, publicCerts); - } - - } } + } - using (SafePkcs12Handle pkcs12 = Interop.Crypto.Pkcs12Create( - password, - privateCertKeyHandle, - privateCertHandle, - publicCerts)) + if (alg == null) + { + try + { + alg = new DSAOpenSsl(privateKey); + } + catch (CryptographicException) { - if (pkcs12.IsInvalid) - { - throw Interop.Crypto.CreateOpenSslCryptographicException(); - } - - byte[] result = Interop.Crypto.OpenSslEncode( - Interop.Crypto.GetPkcs12DerSize, - Interop.Crypto.EncodePkcs12, - pkcs12); - - // ensure cert handles aren't finalized while the raw handles are in use - GC.KeepAlive(_certs); - return result; } - - } + + Debug.Assert(alg != null); + return alg.ExportEncryptedPkcs8PrivateKey(password, s_windowsPbe); } private static void PushHandle(IntPtr certPtr, SafeX509StackHandle publicCerts) @@ -168,7 +77,7 @@ private static void PushHandle(IntPtr certPtr, SafeX509StackHandle publicCerts) } } - private byte[] ExportPkcs7() + protected override byte[] ExportPkcs7() { // Pack all of the certificates into a new PKCS7*, export it to a byte[], // then free the PKCS7*, since we don't need it any more. diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslPkcs12Reader.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslPkcs12Reader.cs index b754979071d6..fb5032ae56a3 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslPkcs12Reader.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslPkcs12Reader.cs @@ -2,24 +2,27 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Win32.SafeHandles; using System; -using System.Collections.Generic; using System.Security.Cryptography; -using System.Runtime.InteropServices; +using System.Security.Cryptography.Asn1; namespace Internal.Cryptography.Pal { - internal sealed class OpenSslPkcs12Reader : IDisposable + internal sealed class OpenSslPkcs12Reader : UnixPkcs12Reader { - private readonly SafePkcs12Handle _pkcs12Handle; - private SafeEvpPKeyHandle _evpPkeyHandle; - private SafeX509Handle _x509Handle; - private SafeX509StackHandle _caStackHandle; + private OpenSslPkcs12Reader(byte[] data) + { + ParsePkcs12(data); + } - private OpenSslPkcs12Reader(SafePkcs12Handle pkcs12Handle) + protected override ICertificatePalCore ReadX509Der(ReadOnlyMemory data) { - _pkcs12Handle = pkcs12Handle; + if (OpenSslX509CertificateReader.TryReadX509Der(data.Span, out ICertificatePal ret)) + { + return ret; + } + + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } public static bool TryRead(byte[] data, out OpenSslPkcs12Reader pkcs12Reader) => @@ -28,141 +31,77 @@ public static bool TryRead(byte[] data, out OpenSslPkcs12Reader pkcs12Reader) => public static bool TryRead(byte[] data, out OpenSslPkcs12Reader pkcs12Reader, out Exception openSslException) => TryRead(data, out pkcs12Reader, out openSslException, captureException: true); - public static bool TryRead(SafeBioHandle fileBio, out OpenSslPkcs12Reader pkcs12Reader) => - TryRead(fileBio, out pkcs12Reader, out _, captureException: false); - - public static bool TryRead(SafeBioHandle fileBio, out OpenSslPkcs12Reader pkcs12Reader, out Exception openSslException) => - TryRead(fileBio, out pkcs12Reader, out openSslException, captureException: true); - - public void Dispose() + protected override AsymmetricAlgorithm LoadKey(ReadOnlyMemory pkcs8) { - if (_caStackHandle != null) - { - _caStackHandle.Dispose(); - _caStackHandle = null; - } + PrivateKeyInfoAsn privateKeyInfo = PrivateKeyInfoAsn.Decode(pkcs8, AsnEncodingRules.BER); + AsymmetricAlgorithm key; - if (_x509Handle != null) + switch (privateKeyInfo.PrivateKeyAlgorithm.Algorithm.Value) { - _x509Handle.Dispose(); - _x509Handle = null; + 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.Value); } - if (_evpPkeyHandle != null) - { - _evpPkeyHandle.Dispose(); - _evpPkeyHandle = null; - } + key.ImportPkcs8PrivateKey(pkcs8.Span, out int bytesRead); - if (_pkcs12Handle != null) + if (bytesRead != pkcs8.Length) { - _pkcs12Handle.Dispose(); + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); } - } - public void Decrypt(SafePasswordHandle password) - { - bool parsed = Interop.Crypto.Pkcs12Parse( - _pkcs12Handle, - password, - out _evpPkeyHandle, - out _x509Handle, - out _caStackHandle); - - if (!parsed) - { - throw Interop.Crypto.CreateOpenSslCryptographicException(); - } + return key; } - public List ReadCertificates() + internal static SafeEvpPKeyHandle GetPrivateKey(AsymmetricAlgorithm key) { - var certs = new List(); - - if (_caStackHandle != null && !_caStackHandle.IsInvalid) + if (key is RSAOpenSsl rsa) { - int caCertCount = Interop.Crypto.GetX509StackFieldCount(_caStackHandle); - - for (int i = 0; i < caCertCount; i++) - { - IntPtr certPtr = Interop.Crypto.GetX509StackField(_caStackHandle, i); - - if (certPtr != IntPtr.Zero) - { - // The STACK_OF(X509) still needs to be cleaned up, so upref the handle out of it. - certs.Add(new OpenSslX509CertificateReader(Interop.Crypto.X509UpRef(certPtr))); - } - } + return rsa.DuplicateKeyHandle(); } - if (_x509Handle != null && !_x509Handle.IsInvalid) + if (key is DSAOpenSsl dsa) { - // The certificate and (if applicable) private key handles will be given over - // to the OpenSslX509CertificateReader, and the fields here are thus nulled out to - // prevent double-Dispose. - OpenSslX509CertificateReader reader = new OpenSslX509CertificateReader(_x509Handle); - _x509Handle = null; - - if (_evpPkeyHandle != null && !_evpPkeyHandle.IsInvalid) - { - reader.SetPrivateKey(_evpPkeyHandle); - _evpPkeyHandle = null; - } - - certs.Add(reader); + return dsa.DuplicateKeyHandle(); } - return certs; + return ((ECDiffieHellmanOpenSsl)key).DuplicateKeyHandle(); } - private static bool TryRead(byte[] data, out OpenSslPkcs12Reader pkcs12Reader, out Exception openSslException, bool captureException) + private static bool TryRead( + byte[] data, + out OpenSslPkcs12Reader pkcs12Reader, + out Exception openSslException, + bool captureException) { - SafePkcs12Handle handle = Interop.Crypto.DecodePkcs12(data, data.Length); openSslException = null; - if (!handle.IsInvalid) + try { - pkcs12Reader = new OpenSslPkcs12Reader(handle); + pkcs12Reader = new OpenSslPkcs12Reader(data); return true; } - - handle.Dispose(); - pkcs12Reader = null; - if (captureException) - { - openSslException = Interop.Crypto.CreateOpenSslCryptographicException(); - } - else + catch (CryptographicException e) { - Interop.Crypto.ErrClearError(); - } - - return false; - } - - private static bool TryRead(SafeBioHandle fileBio, out OpenSslPkcs12Reader pkcs12Reader, out Exception openSslException, bool captureException) - { - SafePkcs12Handle p12 = Interop.Crypto.DecodePkcs12FromBio(fileBio); - openSslException = null; - - if (!p12.IsInvalid) - { - pkcs12Reader = new OpenSslPkcs12Reader(p12); - return true; - } + if (captureException) + { + openSslException = e; + } - p12.Dispose(); - pkcs12Reader = null; - if (captureException) - { - openSslException = Interop.Crypto.CreateOpenSslCryptographicException(); + pkcs12Reader = null; + return false; } - else - { - Interop.Crypto.ErrClearError(); - } - - return false; } } } diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs index f5f28cc663b9..a14fc38babd9 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509CertificateReader.cs @@ -6,6 +6,8 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; @@ -69,16 +71,36 @@ public static ICertificatePal FromBlob(byte[] rawData, SafePasswordHandle passwo public static ICertificatePal FromFile(string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags) { + ICertificatePal pal; + // If we can't open the file, fail right away. using (SafeBioHandle fileBio = Interop.Crypto.BioNewFile(fileName, "rb")) { Interop.Crypto.CheckValidOpenSslHandle(fileBio); - return FromBio(fileBio, password); + pal = FromBio(fileBio); + } + + if (pal == null) + { + PkcsFormatReader.TryReadPkcs12( + File.ReadAllBytes(fileName), + password, + out pal, + out Exception exception); + + if (exception != null) + { + throw exception; + } + + Debug.Assert(pal != null); } + + return pal; } - private static ICertificatePal FromBio(SafeBioHandle bio, SafePasswordHandle password) + private static ICertificatePal FromBio(SafeBioHandle bio) { int bioPosition = Interop.Crypto.BioTell(bio); @@ -114,28 +136,7 @@ private static ICertificatePal FromBio(SafeBioHandle bio, SafePasswordHandle pas return certPal; } - // Rewind, try again. - RewindBio(bio, bioPosition); - - // Capture the exception so in case of failure, the call to BioSeek does not override it. - Exception openSslException; - if (PkcsFormatReader.TryReadPkcs12(bio, password, out certPal, out openSslException)) - { - return certPal; - } - - // 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) - { - Interop.Crypto.ErrClearError(); - } - - Debug.Assert(openSslException != null); - throw openSslException; + return null; } internal static void RewindBio(SafeBioHandle bio, int bioPosition) @@ -148,9 +149,11 @@ internal static void RewindBio(SafeBioHandle bio, int bioPosition) } } - internal static bool TryReadX509Der(byte[] rawData, out ICertificatePal certPal) + internal static bool TryReadX509Der(ReadOnlySpan rawData, out ICertificatePal certPal) { - SafeX509Handle certHandle = Interop.Crypto.DecodeX509(rawData, rawData.Length); + SafeX509Handle certHandle = Interop.Crypto.DecodeX509( + ref MemoryMarshal.GetReference(rawData), + rawData.Length); if (certHandle.IsInvalid) { diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509Encoder.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509Encoder.cs index 5d4ccda8f9cf..40eef919f81b 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509Encoder.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/OpenSslX509Encoder.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; +using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.Asn1; using System.Security.Cryptography.X509Certificates; @@ -134,19 +135,17 @@ public X509ContentType GetCertContentType(string fileName) OpenSslX509CertificateReader.RewindBio(fileBio, bioPosition); } + } - // X509ContentType.Pkcs12 (aka PFX) - { - OpenSslPkcs12Reader pkcs12Reader; - - if (OpenSslPkcs12Reader.TryRead(fileBio, out pkcs12Reader)) - { - pkcs12Reader.Dispose(); + // X509ContentType.Pkcs12 (aka PFX) + { + OpenSslPkcs12Reader pkcs12Reader; - return X509ContentType.Pkcs12; - } + if (OpenSslPkcs12Reader.TryRead(File.ReadAllBytes(fileName), out pkcs12Reader)) + { + pkcs12Reader.Dispose(); - OpenSslX509CertificateReader.RewindBio(fileBio, bioPosition); + return X509ContentType.Pkcs12; } } diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/PkcsFormatReader.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/PkcsFormatReader.cs index 3502af8fc5ab..e864d4f165c2 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/PkcsFormatReader.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/PkcsFormatReader.cs @@ -257,13 +257,6 @@ internal static bool TryReadPkcs12(byte[] rawData, SafePasswordHandle password, return TryReadPkcs12(rawData, password, true, out certPal, out ignored, out openSslException); } - internal static bool TryReadPkcs12(SafeBioHandle bio, SafePasswordHandle password, out ICertificatePal certPal, out Exception openSslException) - { - List ignored; - - return TryReadPkcs12(bio, password, true, out certPal, out ignored, out openSslException); - } - internal static bool TryReadPkcs12(byte[] rawData, SafePasswordHandle password, out List certPals, out Exception openSslException) { ICertificatePal ignored; @@ -271,13 +264,6 @@ internal static bool TryReadPkcs12(byte[] rawData, SafePasswordHandle password, return TryReadPkcs12(rawData, password, false, out ignored, out certPals, out openSslException); } - internal static bool TryReadPkcs12(SafeBioHandle bio, SafePasswordHandle password, out List certPals, out Exception openSslException) - { - ICertificatePal ignored; - - return TryReadPkcs12(bio, password, false, out ignored, out certPals, out openSslException); - } - private static bool TryReadPkcs12( byte[] rawData, SafePasswordHandle password, @@ -302,30 +288,6 @@ private static bool TryReadPkcs12( } } - private static bool TryReadPkcs12( - SafeBioHandle bio, - SafePasswordHandle password, - bool single, - out ICertificatePal readPal, - out List readCerts, - out Exception openSslException) - { - // DER-PKCS12 - OpenSslPkcs12Reader pfx; - - if (!OpenSslPkcs12Reader.TryRead(bio, out pfx, out openSslException)) - { - readPal = null; - readCerts = null; - return false; - } - - using (pfx) - { - return TryReadPkcs12(pfx, password, single, out readPal, out readCerts); - } - } - private static bool TryReadPkcs12( OpenSslPkcs12Reader pfx, SafePasswordHandle password, @@ -335,42 +297,36 @@ private static bool TryReadPkcs12( { pfx.Decrypt(password); - ICertificatePal first = null; - List certs = null; - - if (!single) + if (single) { - certs = new List(); + 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; } - foreach (OpenSslX509CertificateReader certPal in pfx.ReadCertificates()) + readPal = null; + List certs = new List(pfx.GetCertCount()); + + foreach (UnixPkcs12Reader.CertAndKey certAndKey in pfx.EnumerateAll()) { - if (single) - { - // When requesting an X509Certificate2 from a PFX only the first entry is - // returned. Other entries should be disposed. + OpenSslX509CertificateReader pal = (OpenSslX509CertificateReader)certAndKey.Cert; - if (first == null) - { - first = certPal; - } - else if (certPal.HasPrivateKey && !first.HasPrivateKey) - { - first.Dispose(); - first = certPal; - } - else - { - certPal.Dispose(); - } - } - else + if (certAndKey.Key != null) { - certs.Add(certPal); + pal.SetPrivateKey(OpenSslPkcs12Reader.GetPrivateKey(certAndKey.Key)); } + + certs.Add(pal); } - readPal = first; readCerts = certs; return true; } diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs index ad3d5b074b5f..013c0d2bbf69 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.Win32.SafeHandles; @@ -56,11 +57,11 @@ public static ILoaderPal FromFile(string fileName, SafePasswordHandle password, { Interop.Crypto.CheckValidOpenSslHandle(bio); - return FromBio(bio, password); + return FromBio(fileName, bio, password); } } - private static ILoaderPal FromBio(SafeBioHandle bio, SafePasswordHandle password) + private static ILoaderPal FromBio(string fileName, SafeBioHandle bio, SafePasswordHandle password) { int bioPosition = Interop.Crypto.BioTell(bio); Debug.Assert(bioPosition >= 0); @@ -103,7 +104,8 @@ private static ILoaderPal FromBio(SafeBioHandle bio, SafePasswordHandle password // Capture the exception so in case of failure, the call to BioSeek does not override it. Exception openSslException; - if (PkcsFormatReader.TryReadPkcs12(bio, password, out certPals, out openSslException)) + byte[] data = File.ReadAllBytes(fileName); + if (PkcsFormatReader.TryReadPkcs12(data, password, out certPals, out openSslException)) { return ListToLoaderPal(certPals); } diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixExportProvider.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixExportProvider.cs new file mode 100644 index 000000000000..2f7a4b7d5e57 --- /dev/null +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixExportProvider.cs @@ -0,0 +1,565 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Win32.SafeHandles; +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs12; +using System.Security.Cryptography.Pkcs; +using System.Security.Cryptography.X509Certificates; + +namespace Internal.Cryptography.Pal +{ + internal abstract class UnixExportProvider : IExportPal + { + private static readonly Asn1Tag s_contextSpecific0 = + new Asn1Tag(TagClass.ContextSpecific, 0, isConstructed: true); + + internal static readonly PbeParameters s_windowsPbe = + new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 2000); + + protected ICertificatePalCore _singleCertPal; + protected X509Certificate2Collection _certs; + + internal UnixExportProvider(ICertificatePalCore singleCertPal) + { + _singleCertPal = singleCertPal; + } + + internal UnixExportProvider(X509Certificate2Collection certs) + { + _certs = certs; + } + + public void Dispose() + { + // Don't dispose any of the resources, they're still owned by the caller. + _singleCertPal = null; + _certs = null; + } + + protected abstract byte[] ExportPkcs7(); + + protected abstract byte[] ExportPkcs8(ICertificatePalCore certificatePal, ReadOnlySpan password); + + public byte[] Export(X509ContentType contentType, SafePasswordHandle password) + { + Debug.Assert(password != null); + switch (contentType) + { + case X509ContentType.Cert: + return ExportX509Der(); + case X509ContentType.Pfx: + return ExportPfx(password); + case X509ContentType.Pkcs7: + return ExportPkcs7(); + case X509ContentType.SerializedCert: + case X509ContentType.SerializedStore: + throw new PlatformNotSupportedException(SR.Cryptography_Unix_X509_SerializedExport); + default: + throw new CryptographicException(SR.Cryptography_X509_InvalidContentType); + } + } + + private byte[] ExportX509Der() + { + if (_singleCertPal != null) + { + return _singleCertPal.RawData; + } + + // Windows/Desktop compatibility: Exporting a collection (or store) as + // X509ContentType.Cert returns the equivalent of FirstOrDefault(), + // so anything past _certs[0] is ignored, and an empty collection is + // null (not an Exception) + if (_certs.Count == 0) + { + return null; + } + + return _certs[0].RawData; + } + + private byte[] ExportPfx(SafePasswordHandle password) + { + int certCount = 1; + + if (_singleCertPal == null) + { + Debug.Assert(_certs != null); + certCount = _certs.Count; + } + + CertBagAsn[] certBags = ArrayPool.Shared.Rent(certCount); + SafeBagAsn[] keyBags = ArrayPool.Shared.Rent(certCount); + AttributeAsn[] certAttrs = ArrayPool.Shared.Rent(certCount); + certAttrs.AsSpan(0, certCount).Clear(); + + AsnWriter tmpWriter = new AsnWriter(AsnEncodingRules.DER); + ArraySegment encodedAuthSafe = default; + + bool gotRef = false; + password.DangerousAddRef(ref gotRef); + + try + { + ReadOnlySpan passwordSpan = password.DangerousGetSpan(); + + int keyIdx = 0; + int certIdx = 0; + + if (_singleCertPal != null) + { + BuildBags( + _singleCertPal, + passwordSpan, + tmpWriter, + certBags, + certAttrs, + keyBags, + ref certIdx, + ref keyIdx); + } + else + { + foreach (X509Certificate2 cert in _certs) + { + BuildBags( + cert.Pal, + passwordSpan, + tmpWriter, + certBags, + certAttrs, + keyBags, + ref certIdx, + ref keyIdx); + } + } + + encodedAuthSafe = EncodeAuthSafe( + tmpWriter, + keyBags, + keyIdx, + certBags, + certAttrs, + certIdx, + passwordSpan); + + return MacAndEncode(tmpWriter, encodedAuthSafe, passwordSpan); + } + finally + { + password.DangerousRelease(); + tmpWriter.Dispose(); + certAttrs.AsSpan(0, certCount).Clear(); + certBags.AsSpan(0, certCount).Clear(); + keyBags.AsSpan(0, certCount).Clear(); + ArrayPool.Shared.Return(certAttrs); + ArrayPool.Shared.Return(certBags); + ArrayPool.Shared.Return(keyBags); + + if (encodedAuthSafe.Array != null) + { + CryptoPool.Return(encodedAuthSafe); + } + } + } + + private void BuildBags( + ICertificatePalCore certPal, + ReadOnlySpan passwordSpan, + AsnWriter tmpWriter, + CertBagAsn[] certBags, + AttributeAsn[] certAttrs, + SafeBagAsn[] keyBags, + ref int certIdx, + ref int keyIdx) + { + tmpWriter.WriteOctetString(certPal.RawData); + + certBags[certIdx] = new CertBagAsn + { + CertId = Oids.Pkcs12X509CertBagType, + CertValue = tmpWriter.Encode(), + }; + + tmpWriter.Reset(); + + if (certPal.HasPrivateKey) + { + byte[] attrBytes = new byte[6]; + attrBytes[0] = (byte)UniversalTagNumber.OctetString; + attrBytes[1] = sizeof(int); + MemoryMarshal.Write(attrBytes.AsSpan(2), ref keyIdx); + + keyBags[keyIdx] = new SafeBagAsn + { + BagId = Oids.Pkcs12ShroudedKeyBag, + BagValue = ExportPkcs8(certPal, passwordSpan), + BagAttributes = new[] + { + new AttributeAsn + { + AttrType = new Oid(Oids.LocalKeyId, null), + AttrValues = new ReadOnlyMemory[] + { + attrBytes, + } + } + } + }; + + // Reuse the attribute between the cert and the key. + certAttrs[certIdx] = keyBags[keyIdx].BagAttributes[0]; + keyIdx++; + } + + certIdx++; + } + + private static unsafe ArraySegment EncodeAuthSafe( + AsnWriter tmpWriter, + SafeBagAsn[] keyBags, + int keyCount, + CertBagAsn[] certBags, + AttributeAsn[] certAttrs, + int certIdx, + ReadOnlySpan passwordSpan) + { + string encryptionAlgorithmOid = null; + bool certsIsPkcs12Encryption = false; + string certsHmacOid = null; + + ArraySegment encodedKeyContents = default; + ArraySegment encodedCertContents = default; + + try + { + if (keyCount > 0) + { + encodedKeyContents = EncodeKeys(tmpWriter, keyBags, keyCount); + } + + Span salt = stackalloc byte[16]; + RandomNumberGenerator.Fill(salt); + Span certContentsIv = stackalloc byte[8]; + + if (certIdx > 0) + { + encodedCertContents = EncodeCerts( + tmpWriter, + certBags, + certAttrs, + certIdx, + salt, + passwordSpan, + certContentsIv, + out certsHmacOid, + out encryptionAlgorithmOid, + out certsIsPkcs12Encryption); + } + + return EncodeAuthSafe( + tmpWriter, + encodedKeyContents, + encodedCertContents, + certsIsPkcs12Encryption, + certsHmacOid, + encryptionAlgorithmOid, + salt, + certContentsIv); + } + finally + { + if (encodedCertContents.Array != null) + { + CryptoPool.Return(encodedCertContents); + } + + if (encodedKeyContents.Array != null) + { + CryptoPool.Return(encodedKeyContents); + } + } + } + + private static ArraySegment EncodeKeys(AsnWriter tmpWriter, SafeBagAsn[] keyBags, int keyCount) + { + Debug.Assert(tmpWriter.GetEncodedLength() == 0); + tmpWriter.PushSequence(); + + for (int i = 0; i < keyCount; i++) + { + keyBags[i].Encode(tmpWriter); + } + + tmpWriter.PopSequence(); + ReadOnlySpan encodedKeys = tmpWriter.EncodeAsSpan(); + int length = encodedKeys.Length; + byte[] keyBuf = CryptoPool.Rent(length); + encodedKeys.CopyTo(keyBuf); + tmpWriter.Reset(); + + return new ArraySegment(keyBuf, 0, length); + } + + private static ArraySegment EncodeCerts( + AsnWriter tmpWriter, + CertBagAsn[] certBags, + AttributeAsn[] certAttrs, + int certCount, + Span salt, + ReadOnlySpan passwordSpan, + Span certContentsIv, + out string hmacOid, + out string encryptionAlgorithmOid, + out bool isPkcs12) + { + Debug.Assert(tmpWriter.GetEncodedLength() == 0); + tmpWriter.PushSequence(); + + PasswordBasedEncryption.InitiateEncryption( + s_windowsPbe, + out SymmetricAlgorithm cipher, + out hmacOid, + out encryptionAlgorithmOid, + out isPkcs12); + + using (cipher) + { + Debug.Assert(certContentsIv.Length * 8 == cipher.BlockSize); + + for (int i = certCount - 1; i >= 0; --i) + { + // Manually write the SafeBagAsn + // https://tools.ietf.org/html/rfc7292#section-4.2 + // + // SafeBag ::= SEQUENCE { + // bagId BAG-TYPE.&id ({PKCS12BagSet}) + // bagValue [0] EXPLICIT BAG-TYPE.&Type({PKCS12BagSet}{@bagId}), + // bagAttributes SET OF PKCS12Attribute OPTIONAL + // } + tmpWriter.PushSequence(); + + tmpWriter.WriteObjectIdentifier(Oids.Pkcs12CertBag); + + tmpWriter.PushSequence(s_contextSpecific0); + certBags[i].Encode(tmpWriter); + tmpWriter.PopSequence(s_contextSpecific0); + + if (certAttrs[i].AttrType != null) + { + tmpWriter.PushSetOf(); + certAttrs[i].Encode(tmpWriter); + tmpWriter.PopSetOf(); + } + + tmpWriter.PopSequence(); + } + + tmpWriter.PopSequence(); + ReadOnlySpan contentsSpan = tmpWriter.EncodeAsSpan(); + + // The padding applied will add at most a block to the output, + // so ask for contentsSpan.Length + the number of bytes in a cipher block. + int cipherBlockBytes = cipher.BlockSize >> 3; + int requestedSize = checked(contentsSpan.Length + cipherBlockBytes); + byte[] certContents = CryptoPool.Rent(requestedSize); + + int encryptedLength = PasswordBasedEncryption.Encrypt( + passwordSpan, + ReadOnlySpan.Empty, + cipher, + isPkcs12, + contentsSpan, + s_windowsPbe, + salt, + certContents, + certContentsIv); + + Debug.Assert(encryptedLength <= requestedSize); + tmpWriter.Reset(); + + return new ArraySegment(certContents, 0, encryptedLength); + } + } + + private static ArraySegment EncodeAuthSafe( + AsnWriter tmpWriter, + ReadOnlyMemory encodedKeyContents, + ReadOnlyMemory encodedCertContents, + bool isPkcs12, + string hmacOid, + string encryptionAlgorithmOid, + Span salt, + Span certContentsIv) + { + Debug.Assert(tmpWriter.GetEncodedLength() == 0); + + tmpWriter.PushSequence(); + + if (!encodedKeyContents.IsEmpty) + { + tmpWriter.PushSequence(); + tmpWriter.WriteObjectIdentifier(Oids.Pkcs7Data); + tmpWriter.PushSequence(s_contextSpecific0); + + ReadOnlySpan keyContents = encodedKeyContents.Span; + tmpWriter.WriteOctetString(keyContents); + + tmpWriter.PopSequence(s_contextSpecific0); + tmpWriter.PopSequence(); + } + + if (!encodedCertContents.IsEmpty) + { + tmpWriter.PushSequence(); + + { + tmpWriter.WriteObjectIdentifier(Oids.Pkcs7Encrypted); + + tmpWriter.PushSequence(s_contextSpecific0); + tmpWriter.PushSequence(); + + { + // No unprotected attributes: version 0 data + tmpWriter.WriteInteger(0); + + tmpWriter.PushSequence(); + + { + tmpWriter.WriteObjectIdentifier(Oids.Pkcs7Data); + + PasswordBasedEncryption.WritePbeAlgorithmIdentifier( + tmpWriter, + isPkcs12, + encryptionAlgorithmOid, + salt, + s_windowsPbe.IterationCount, + hmacOid, + certContentsIv); + + tmpWriter.WriteOctetString(s_contextSpecific0, encodedCertContents.Span); + tmpWriter.PopSequence(); + } + + tmpWriter.PopSequence(); + tmpWriter.PopSequence(s_contextSpecific0); + } + + tmpWriter.PopSequence(); + } + } + + tmpWriter.PopSequence(); + + ReadOnlySpan authSafeSpan = tmpWriter.EncodeAsSpan(); + byte[] authSafe = CryptoPool.Rent(authSafeSpan.Length); + authSafeSpan.CopyTo(authSafe); + tmpWriter.Reset(); + + return new ArraySegment(authSafe, 0, authSafeSpan.Length); + } + + private static unsafe byte[] MacAndEncode( + AsnWriter tmpWriter, + ReadOnlyMemory encodedAuthSafe, + ReadOnlySpan passwordSpan) + { + // Windows/macOS compatibility: Use HMAC-SHA-1, + // other algorithms may not be understood + byte[] macKey = new byte[20]; + Span macSalt = stackalloc byte[20]; + Span macSpan = stackalloc byte[20]; + HashAlgorithmName hashAlgorithm = HashAlgorithmName.SHA1; + RandomNumberGenerator.Fill(macSalt); + + fixed (byte* macKeyPtr = macKey) + { + Span macKeySpan = macKey; + + Pkcs12Kdf.DeriveMacKey( + passwordSpan, + hashAlgorithm, + s_windowsPbe.IterationCount, + macSalt, + macKeySpan); + + using (IncrementalHash mac = IncrementalHash.CreateHMAC(hashAlgorithm, macKey)) + { + mac.AppendData(encodedAuthSafe.Span); + + if (!mac.TryGetHashAndReset(macSpan, out int bytesWritten) || bytesWritten != macSpan.Length) + { + Debug.Fail($"TryGetHashAndReset wrote {bytesWritten} of {macSpan.Length} bytes"); + throw new CryptographicException(); + } + } + + CryptographicOperations.ZeroMemory(macKeySpan); + } + + // https://tools.ietf.org/html/rfc7292#section-4 + // + // PFX ::= SEQUENCE { + // version INTEGER {v3(3)}(v3,...), + // authSafe ContentInfo, + // macData MacData OPTIONAL + // } + Debug.Assert(tmpWriter.GetEncodedLength() == 0); + tmpWriter.PushSequence(); + + tmpWriter.WriteInteger(3); + + tmpWriter.PushSequence(); + { + tmpWriter.WriteObjectIdentifier(Oids.Pkcs7Data); + + tmpWriter.PushSequence(s_contextSpecific0); + { + tmpWriter.WriteOctetString(encodedAuthSafe.Span); + tmpWriter.PopSequence(s_contextSpecific0); + } + + tmpWriter.PopSequence(); + } + + // 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. + // } + tmpWriter.PushSequence(); + { + tmpWriter.PushSequence(); + { + tmpWriter.PushSequence(); + { + tmpWriter.WriteObjectIdentifier(Oids.Sha1); + tmpWriter.PopSequence(); + } + + tmpWriter.WriteOctetString(macSpan); + tmpWriter.PopSequence(); + } + + tmpWriter.WriteOctetString(macSalt); + tmpWriter.WriteInteger(s_windowsPbe.IterationCount); + + tmpWriter.PopSequence(); + } + + tmpWriter.PopSequence(); + return tmpWriter.Encode(); + } + } +} diff --git a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixPkcs12Reader.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixPkcs12Reader.cs new file mode 100644 index 000000000000..12a6826c99f5 --- /dev/null +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixPkcs12Reader.cs @@ -0,0 +1,772 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.Asn1; +using System.Security.Cryptography.Asn1.Pkcs12; +using System.Security.Cryptography.Asn1.Pkcs7; +using System.Threading; +using Microsoft.Win32.SafeHandles; + +namespace Internal.Cryptography.Pal +{ + internal abstract class UnixPkcs12Reader : IDisposable + { + private const string DecryptedSentinel = nameof(UnixPkcs12Reader); + + private PfxAsn _pfxAsn; + private ContentInfoAsn[] _safeContentsValues; + private CertAndKey[] _certs; + private int _certCount; + + protected abstract ICertificatePalCore ReadX509Der(ReadOnlyMemory data); + protected abstract AsymmetricAlgorithm LoadKey(ReadOnlyMemory safeBagBagValue); + + protected void ParsePkcs12(byte[] data) + { + // RFC7292 specifies BER instead of DER + AsnReader reader = new AsnReader(data, AsnEncodingRules.BER); + ReadOnlyMemory encodedData = reader.PeekEncodedValue(); + + // Windows compatibility: Ignore trailing data. + if (encodedData.Length != data.Length) + { + reader = new AsnReader(encodedData, AsnEncodingRules.BER); + } + + PfxAsn.Decode(reader, out PfxAsn pfxAsn); + + if (pfxAsn.AuthSafe.ContentType != Oids.Pkcs7Data) + { + throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + } + + _pfxAsn = pfxAsn; + } + + 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() + { + ContentInfoAsn[] rentedContents = Interlocked.Exchange(ref _safeContentsValues, null); + CertAndKey[] rentedCerts = Interlocked.Exchange(ref _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; + + if (!MemoryMarshal.TryGetArray(content, out ArraySegment segment)) + { + Debug.Fail("Couldn't unpack decrypted buffer."); + } + + CryptoPool.Return(segment); + rentedContents[0].Content = default; + } + } + + ArrayPool.Shared.Return(rentedContents, clearArray: true); + } + + public void Decrypt(SafePasswordHandle password) + { + ReadOnlyMemory authSafeContents = + Helpers.DecodeOctetStringAsMemory(_pfxAsn.AuthSafe.Content); + + bool hasRef = false; + password.DangerousAddRef(ref hasRef); + + try + { + 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); + } + finally + { + 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); + } + + private void Decrypt(ReadOnlySpan password, ReadOnlyMemory authSafeContents) + { + if (_safeContentsValues == null) + { + _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( + 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); + + AsnReader outer = new AsnReader(authSafeContents, AsnEncodingRules.BER); + AsnReader reader = outer.ReadSequence(); + outer.ThrowIfNotEmpty(); + int i = 0; + + while (reader.HasData) + { + GrowIfNeeded(ref rented, i); + ContentInfoAsn.Decode(reader, out rented[i]); + i++; + } + + rented.AsSpan(i).Clear(); + return rented; + } + + 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( + 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.Value == 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) + { + if (keys[matchingKeyIdx] == null) + { + throw new CryptographicException(SR.Cryptography_Pfx_BadKeyReference); + } + + 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.Value) + { + 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.Value) + { + 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.Value) + { + 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) + { + if (attr.AttrType.Value == 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); + } + + AsnReader outer = new AsnReader(contentData, AsnEncodingRules.BER); + AsnReader reader = outer.ReadSequence(); + outer.ThrowIfNotEmpty(); + + while (reader.HasData) + { + SafeBagAsn.Decode(reader, 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++; + } + } + } + + 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.Array, clearSize: decrypted.Count); + } + } + + 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/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Windows/CertificatePal.Import.cs b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Windows/CertificatePal.Import.cs index c5aa53f58378..267d6466b077 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Windows/CertificatePal.Import.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Windows/CertificatePal.Import.cs @@ -8,6 +8,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Security; +using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Internal.Cryptography.Pal.Native; @@ -185,14 +186,16 @@ private static SafeCertContextHandle FilterPFXStore(byte[] rawData, SafePassword else { if (pCertContext.IsInvalid) - pCertContext = pEnumContext.Duplicate(); // Doesn't have a private key but hang on to it anyway in case we don't find any certs with a private key. + { + // 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 = pEnumContext.Duplicate(); + } } } if (pCertContext.IsInvalid) { - // For compat, setting "hr" to ERROR_INVALID_PARAMETER even though ERROR_INVALID_PARAMETER is not actually an HRESULT. - throw ErrorCode.ERROR_INVALID_PARAMETER.ToCryptographicException(); + throw new CryptographicException(SR.Cryptography_Pfx_NoCertificates); } return pCertContext; diff --git a/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.Unix.cs b/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.Unix.cs deleted file mode 100644 index bb4169132208..000000000000 --- a/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.Unix.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Runtime.InteropServices; -using System.Security; - -namespace Microsoft.Win32.SafeHandles -{ - internal partial class SafePasswordHandle - { - private IntPtr CreateHandle(string password) - { - return Marshal.StringToHGlobalAnsi(password); - } - - private IntPtr CreateHandle(SecureString password) - { - return Marshal.SecureStringToGlobalAllocAnsi(password); - } - - private void FreeHandle() - { - Marshal.ZeroFreeGlobalAllocAnsi(handle); - } - } -} diff --git a/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.Windows.cs b/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.Windows.cs deleted file mode 100644 index 93dce2ab136b..000000000000 --- a/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.Windows.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Runtime.InteropServices; -using System.Security; - -namespace Microsoft.Win32.SafeHandles -{ - internal partial class SafePasswordHandle - { - private IntPtr CreateHandle(string password) - { - return Marshal.StringToHGlobalUni(password); - } - - private IntPtr CreateHandle(SecureString password) - { - return Marshal.SecureStringToGlobalAllocUnicode(password); - } - - private void FreeHandle() - { - Marshal.ZeroFreeGlobalAllocUnicode(handle); - } - } -} diff --git a/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.cs b/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.cs index f9235193b43d..2d1ca236fb40 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.cs @@ -11,33 +11,35 @@ 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 : SafeHandle + internal sealed partial class SafePasswordHandle : SafeHandleZeroOrMinusOneIsInvalid { + internal int Length { get; private set; } + public SafePasswordHandle(string password) - : base(IntPtr.Zero, ownsHandle: true) + : base(ownsHandle: true) { if (password != null) { - SetHandle(CreateHandle(password)); + handle = Marshal.StringToHGlobalUni(password); + Length = password.Length; } } public SafePasswordHandle(SecureString password) - : base(IntPtr.Zero, ownsHandle: true) + : base(ownsHandle: true) { if (password != null) { - SetHandle(CreateHandle(password)); + handle = Marshal.SecureStringToGlobalAllocUnicode(password); + Length = password.Length; } } protected override bool ReleaseHandle() { - if (handle != IntPtr.Zero) - { - FreeHandle(); - } + Marshal.ZeroFreeGlobalAllocUnicode(handle); SetHandle((IntPtr)(-1)); + Length = 0; return true; } @@ -51,7 +53,18 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } - public override bool IsInvalid => handle == (IntPtr)(-1); + internal ReadOnlySpan DangerousGetSpan() + { + if (IsInvalid) + { + return default; + } + + unsafe + { + return new ReadOnlySpan((char*)handle, Length); + } + } public static SafePasswordHandle InvalidHandle => SafeHandleCache.GetInvalidHandle( diff --git a/src/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx b/src/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx index bdf2a6af9a6d..37fc7642786b 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx +++ b/src/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx @@ -201,6 +201,15 @@ Cannot open an invalid handle. + + A certificate referenced a private key which was already referenced, or could not be loaded. + + + The certificate data cannot be read with the provided password, the password may be incorrect. + + + The provided PFX data contains no certificates. + The provided key does not match the public key for this certificate. diff --git a/src/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj b/src/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj index 5eb3738d3b63..e3238a5986bc 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj +++ b/src/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj @@ -246,7 +246,6 @@ - Common\Interop\Windows\Crypt32\Interop.CertCloseStore.cs @@ -296,9 +295,6 @@ Common\Microsoft\Win32\SafeHandles\SafeBCryptKeyHandle.cs - - - @@ -308,6 +304,15 @@ System\Security\Cryptography\X509Certificates\Asn1\DistributionPointNameAsn.xml + + Common\System\Security\Cryptography\KeyFormatHelper.cs + + + Common\System\Security\Cryptography\PasswordBasedEncryption.cs + + + Common\System\Security\Cryptography\Pkcs12Kdf.cs + @@ -578,6 +583,56 @@ Common\System\Security\Cryptography\Asn1\ECPrivateKey.xml.cs Common\System\Security\Cryptography\Asn1\ECPrivateKey.xml + + Common\System\Security\Cryptography\Asn1\FieldID.xml + + + Common\System\Security\Cryptography\Asn1\FieldID.xml.cs + Common\System\Security\Cryptography\Asn1\FieldID.xml + + + Common\System\Security\Cryptography\Asn1\RSAPrivateKeyAsn.xml + + + Common\System\Security\Cryptography\Asn1\RSAPrivateKeyAsn.xml.cs + Common\System\Security\Cryptography\Asn1\RSAPrivateKeyAsn.xml + + + Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml + + + Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml.cs + Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml + + + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml + + + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml.cs + Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.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 @@ -585,19 +640,44 @@ Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml.cs Common\System\Security\Cryptography\Asn1\EncryptedPrivateKeyInfoAsn.xml - - Common\System\Security\Cryptography\Asn1\FieldID.xml + + Common\System\Security\Cryptography\Asn1\Pkcs12\CertBagAsn.xml - - Common\System\Security\Cryptography\Asn1\FieldID.xml.cs - Common\System\Security\Cryptography\Asn1\FieldID.xml + + Common\System\Security\Cryptography\Asn1\Pkcs12\CertBagAsn.xml.cs + Common\System\Security\Cryptography\Asn1\Pkcs12\CertBagAsn.xml - - Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml + + Common\System\Security\Cryptography\Asn1\Pkcs12\MacData.xml - - Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.xml.cs - Common\System\Security\Cryptography\Asn1\PrivateKeyInfoAsn.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\PBEParameter.xml @@ -627,6 +707,27 @@ Common\System\Security\Cryptography\Asn1\Pbkdf2SaltChoice.xml.cs Common\System\Security\Cryptography\Asn1\Pbkdf2SaltChoice.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 + + + 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 @@ -638,44 +739,12 @@ Common\System\Security\Cryptography\Asn1\Rc2CbcParameters.manual.cs Common\System\Security\Cryptography\Asn1\Rc2CbcParameters.xml - - Common\System\Security\Cryptography\Asn1\RSAPrivateKeyAsn.xml - - - Common\System\Security\Cryptography\Asn1\RSAPrivateKeyAsn.xml.cs - Common\System\Security\Cryptography\Asn1\RSAPrivateKeyAsn.xml - - - Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml - - - Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml.cs - Common\System\Security\Cryptography\Asn1\RSAPublicKeyAsn.xml - - - Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml - - - Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml.cs - Common\System\Security\Cryptography\Asn1\SpecifiedECDomain.xml - - - - - - - - - - - - - - + + diff --git a/src/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/Asn1/DistributionPointAsn.xml.cs b/src/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/Asn1/DistributionPointAsn.xml.cs index 699659ea9c32..a66cde66ba55 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/Asn1/DistributionPointAsn.xml.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/Asn1/DistributionPointAsn.xml.cs @@ -1,7 +1,8 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable SA1028 // ignore whitespace warnings for generated code using System; using System.Collections.Generic; using System.Runtime.InteropServices; @@ -16,16 +17,16 @@ internal partial struct DistributionPointAsn internal System.Security.Cryptography.X509Certificates.Asn1.DistributionPointNameAsn? DistributionPoint; internal System.Security.Cryptography.X509Certificates.Asn1.ReasonFlagsAsn? Reasons; internal System.Security.Cryptography.Asn1.GeneralNameAsn[] CRLIssuer; - + internal void Encode(AsnWriter writer) { Encode(writer, Asn1Tag.Sequence); } - + internal void Encode(AsnWriter writer, Asn1Tag tag) { writer.PushSequence(tag); - + if (DistributionPoint.HasValue) { @@ -47,7 +48,7 @@ internal void Encode(AsnWriter writer, Asn1Tag tag) writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 2)); for (int i = 0; i < CRLIssuer.Length; i++) { - CRLIssuer[i].Encode(writer); + CRLIssuer[i].Encode(writer); } writer.PopSequence(new Asn1Tag(TagClass.ContextSpecific, 2)); @@ -60,11 +61,11 @@ internal static DistributionPointAsn Decode(ReadOnlyMemory encoded, AsnEnc { return Decode(Asn1Tag.Sequence, encoded, ruleSet); } - + internal static DistributionPointAsn Decode(Asn1Tag expectedTag, ReadOnlyMemory encoded, AsnEncodingRules ruleSet) { AsnReader reader = new AsnReader(encoded, ruleSet); - + Decode(reader, expectedTag, out DistributionPointAsn decoded); reader.ThrowIfNotEmpty(); return decoded; @@ -87,7 +88,7 @@ internal static void Decode(AsnReader reader, Asn1Tag expectedTag, out Distribut AsnReader sequenceReader = reader.ReadSequence(expectedTag); AsnReader explicitReader; AsnReader collectionReader; - + if (sequenceReader.HasData && sequenceReader.PeekTag().HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 0))) { @@ -117,7 +118,7 @@ internal static void Decode(AsnReader reader, Asn1Tag expectedTag, out Distribut while (collectionReader.HasData) { - System.Security.Cryptography.Asn1.GeneralNameAsn.Decode(collectionReader, out tmpItem); + System.Security.Cryptography.Asn1.GeneralNameAsn.Decode(collectionReader, out tmpItem); tmpList.Add(tmpItem); } diff --git a/src/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/Asn1/DistributionPointNameAsn.xml.cs b/src/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/Asn1/DistributionPointNameAsn.xml.cs index 599175c20978..e64481a73d39 100644 --- a/src/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/Asn1/DistributionPointNameAsn.xml.cs +++ b/src/System.Security.Cryptography.X509Certificates/src/System/Security/Cryptography/X509Certificates/Asn1/DistributionPointNameAsn.xml.cs @@ -1,7 +1,8 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +#pragma warning disable SA1028 // ignore whitespace warnings for generated code using System; using System.Collections.Generic; using System.Runtime.InteropServices; @@ -29,7 +30,7 @@ static DistributionPointNameAsn() usedTags.Add(tag, fieldName); }; - + ensureUniqueTag(new Asn1Tag(TagClass.ContextSpecific, 0), "FullName"); ensureUniqueTag(new Asn1Tag(TagClass.ContextSpecific, 1), "NameRelativeToCRLIssuer"); } @@ -37,18 +38,18 @@ static DistributionPointNameAsn() internal void Encode(AsnWriter writer) { - bool wroteValue = false; - + bool wroteValue = false; + if (FullName != null) { if (wroteValue) throw new CryptographicException(); - + writer.PushSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); for (int i = 0; i < FullName.Length; i++) { - FullName[i].Encode(writer); + FullName[i].Encode(writer); } writer.PopSequence(new Asn1Tag(TagClass.ContextSpecific, 0)); @@ -59,7 +60,7 @@ internal void Encode(AsnWriter writer) { if (wroteValue) throw new CryptographicException(); - + // Validator for tag constraint for NameRelativeToCRLIssuer { if (!Asn1Tag.TryDecode(NameRelativeToCRLIssuer.Value.Span, out Asn1Tag validateTag, out _) || @@ -82,7 +83,7 @@ internal void Encode(AsnWriter writer) internal static DistributionPointNameAsn Decode(ReadOnlyMemory encoded, AsnEncodingRules ruleSet) { AsnReader reader = new AsnReader(encoded, ruleSet); - + Decode(reader, out DistributionPointNameAsn decoded); reader.ThrowIfNotEmpty(); return decoded; @@ -96,7 +97,7 @@ internal static void Decode(AsnReader reader, out DistributionPointNameAsn decod decoded = default; Asn1Tag tag = reader.PeekTag(); AsnReader collectionReader; - + if (tag.HasSameClassAndValue(new Asn1Tag(TagClass.ContextSpecific, 0))) { @@ -108,7 +109,7 @@ internal static void Decode(AsnReader reader, out DistributionPointNameAsn decod while (collectionReader.HasData) { - System.Security.Cryptography.Asn1.GeneralNameAsn.Decode(collectionReader, out tmpItem); + System.Security.Cryptography.Asn1.GeneralNameAsn.Decode(collectionReader, out tmpItem); tmpList.Add(tmpItem); } diff --git a/src/System.Security.Cryptography.X509Certificates/tests/CertTests.cs b/src/System.Security.Cryptography.X509Certificates/tests/CertTests.cs index 20cfe52523c7..5770cd04b241 100644 --- a/src/System.Security.Cryptography.X509Certificates/tests/CertTests.cs +++ b/src/System.Security.Cryptography.X509Certificates/tests/CertTests.cs @@ -338,31 +338,16 @@ public static void ExportPublicKeyAsPkcs12() // Pre-condition: There's no private key Assert.False(publicOnly.HasPrivateKey); - // macOS 10.12 (Sierra) fails to create a PKCS#12 blob if it has no private keys within it. - bool shouldThrow = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + byte[] pkcs12Bytes = publicOnly.Export(X509ContentType.Pkcs12); - try + // Read it back as a collection, there should be only one cert, and it should + // be equal to the one we started with. + using (ImportedCollection ic = Cert.Import(pkcs12Bytes)) { - byte[] pkcs12Bytes = publicOnly.Export(X509ContentType.Pkcs12); + X509Certificate2Collection fromPfx = ic.Collection; - Assert.False(shouldThrow, "PKCS#12 export of a public-only certificate threw as expected"); - - // Read it back as a collection, there should be only one cert, and it should - // be equal to the one we started with. - using (ImportedCollection ic = Cert.Import(pkcs12Bytes)) - { - X509Certificate2Collection fromPfx = ic.Collection; - - Assert.Equal(1, fromPfx.Count); - Assert.Equal(publicOnly, fromPfx[0]); - } - } - catch (CryptographicException) - { - if (!shouldThrow) - { - throw; - } + Assert.Equal(1, fromPfx.Count); + Assert.Equal(publicOnly, fromPfx[0]); } } } diff --git a/src/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs b/src/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs index faa293b41f18..82604dc0e928 100644 --- a/src/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs +++ b/src/System.Security.Cryptography.X509Certificates/tests/CollectionTests.cs @@ -6,7 +6,6 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using System.Runtime.InteropServices; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests @@ -632,7 +631,6 @@ public static void ImportFromFileTests(X509KeyStorageFlags storageFlags) } [Fact] - [ActiveIssue(2745, TestPlatforms.AnyUnix)] public static void ImportMultiplePrivateKeysPfx() { using (ImportedCollection ic = Cert.Import(TestData.MultiPrivateKeyPfx)) @@ -722,7 +720,6 @@ public static void ExportEmpty_Cert() } [Fact] - [ActiveIssue(2746, TestPlatforms.AnyUnix)] public static void ExportEmpty_Pkcs12() { var collection = new X509Certificate2Collection(); @@ -733,7 +730,6 @@ public static void ExportEmpty_Pkcs12() } [Fact] - [ActiveIssue(16705, TestPlatforms.OSX)] public static void ExportUnrelatedPfx() { // Export multiple certificates which are not part of any kind of certificate chain. @@ -799,7 +795,6 @@ public static void MultipleImport() } [Fact] - [ActiveIssue(2743, TestPlatforms.AnyUnix & ~TestPlatforms.OSX)] public static void ExportMultiplePrivateKeys() { var collection = new X509Certificate2Collection(); @@ -813,37 +808,8 @@ public static void ExportMultiplePrivateKeys() int originalPrivateKeyCount = collection.OfType().Count(c => c.HasPrivateKey); Assert.Equal(2, originalPrivateKeyCount); - // Export, re-import. - byte[] exported; - - bool expectSuccess = - RuntimeInformation.IsOSPlatform(OSPlatform.Windows) || - RuntimeInformation.IsOSPlatform(OSPlatform.OSX); - - try - { - exported = collection.Export(X509ContentType.Pkcs12); - } - catch (PlatformNotSupportedException) - { - // [ActiveIssue(2743, TestPlatforms.AnyUnix)] - // Our Unix builds can't export more than one private key in a single PFX, so this is - // their exit point. - // - // If Windows gets here, or any exception other than PlatformNotSupportedException is raised, - // let that fail the test. - if (expectSuccess) - { - throw; - } - - return; - } - - // As the other half of issue 2743, if we make it this far we better be Windows (or remove the catch - // above) - Assert.True(expectSuccess, "Test is expected to fail on this platform"); - + byte[] exported = collection.Export(X509ContentType.Pkcs12); + using (ImportedCollection ic = Cert.Import(exported)) { X509Certificate2Collection importedCollection = ic.Collection; @@ -864,7 +830,6 @@ public static void ExportMultiplePrivateKeys() } [Fact] - [ActiveIssue(26397, TestPlatforms.OSX)] public static void CanAddMultipleCertsWithSinglePrivateKey() { using (var oneWithKey = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable | Cert.EphemeralIfPossible)) diff --git a/src/System.Security.Cryptography.X509Certificates/tests/CtorTests.cs b/src/System.Security.Cryptography.X509Certificates/tests/CtorTests.cs index 26d807a7ea9f..b88aa3ec091d 100644 --- a/src/System.Security.Cryptography.X509Certificates/tests/CtorTests.cs +++ b/src/System.Security.Cryptography.X509Certificates/tests/CtorTests.cs @@ -367,11 +367,7 @@ public static void InvalidCertificateBlob() } else // Any Unix { - // OpenSSL encodes the function name into the error code. However, the function name differs - // between versions (OpenSSL 1.0, OpenSSL 1.1 and BoringSSL) and it's subject to change in - // the future, so don't test for the exact match and mask out the function code away. The - // component number (high 8 bits) and error code (low 12 bits) should remain the same. - Assert.Equal(0x0D00003A, ex.HResult & 0xFF000FFF); + Assert.Equal(new CryptographicException("message").HResult, ex.HResult); } } diff --git a/src/System.Security.Cryptography.X509Certificates/tests/ExportTests.cs b/src/System.Security.Cryptography.X509Certificates/tests/ExportTests.cs index 24dfa156c5eb..c473d80895fa 100644 --- a/src/System.Security.Cryptography.X509Certificates/tests/ExportTests.cs +++ b/src/System.Security.Cryptography.X509Certificates/tests/ExportTests.cs @@ -48,7 +48,6 @@ public static void ExportAsSerializedCert_Unix() } [Fact] - [ActiveIssue(16705, TestPlatforms.OSX)] public static void ExportAsPfx() { using (X509Certificate2 c1 = new X509Certificate2(TestData.MsCertificate)) @@ -65,7 +64,6 @@ public static void ExportAsPfx() } [Fact] - [ActiveIssue(16705, TestPlatforms.OSX)] public static void ExportAsPfxWithPassword() { const string password = "Cotton"; @@ -84,7 +82,6 @@ public static void ExportAsPfxWithPassword() } [Fact] - [ActiveIssue(16705, TestPlatforms.OSX)] public static void ExportAsPfxVerifyPassword() { const string password = "Cotton"; diff --git a/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.SingleCertGenerator.cs b/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.SingleCertGenerator.cs new file mode 100644 index 000000000000..c4900d1aa13b --- /dev/null +++ b/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.SingleCertGenerator.cs @@ -0,0 +1,137 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Security.Cryptography.Pkcs; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public abstract partial class PfxFormatTests + { + [Flags] + public enum SingleCertOptions + { + Default = 0, + + SkipMac = 1 << 0, + + UnshroudedKey = 1 << 1, + + KeyAndCertInSameContents = 1 << 2, + KeyContentsLast = 1 << 3, + + PlaintextCertContents = 1 << 4, + + EncryptKeyContents = 1 << 5, + } + + public static IEnumerable AllSingleCertVariations + { + get + { + for (int skipMac = 0; skipMac < 2; skipMac++) + { + for (int unshroudedKey = 0; unshroudedKey < 2; unshroudedKey++) + { + // 3, not 4. Don't do SameContents | KeyLast, it's the same as SameContents. + for (int keySplit = 0; keySplit < 3; keySplit++) + { + for (int plaintextCert = 0; plaintextCert < 2; plaintextCert++) + { + // Only toggle EncryptKey if SameContents isn't set. + int encryptKeyLimit = keySplit == 1 ? 1: 2; + + for (int encryptKey = 0; encryptKey < encryptKeyLimit; encryptKey++) + { + yield return new object[] { + (SingleCertOptions)( + skipMac * (int)SingleCertOptions.SkipMac | + unshroudedKey * (int)SingleCertOptions.UnshroudedKey | + keySplit * (int)SingleCertOptions.KeyAndCertInSameContents | + plaintextCert * (int)SingleCertOptions.PlaintextCertContents | + encryptKey * (int)SingleCertOptions.EncryptKeyContents), + }; + } + } + } + } + } + } + } + + [Theory] + [MemberData(nameof(AllSingleCertVariations))] + public void OneCertWithOneKey(SingleCertOptions options) + { + bool sameContainer = (options & SingleCertOptions.KeyAndCertInSameContents) != 0; + bool dontShroudKey = (options & SingleCertOptions.UnshroudedKey) != 0; + bool keyContainerLast = (options & SingleCertOptions.KeyContentsLast) != 0; + bool encryptCertSafeContents = (options & SingleCertOptions.PlaintextCertContents) == 0; + bool encryptKeySafeContents = (options & SingleCertOptions.EncryptKeyContents) != 0; + bool skipMac = (options & SingleCertOptions.SkipMac) != 0; + string password = options.ToString(); + + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (RSA key = cert.GetRSAPrivateKey()) + { + if (dontShroudKey && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + // CNG keys are only encrypted-exportable, so we need to export them encrypted. + // Then we can import it into a new, fully-exportable key. (Sigh.) + byte[] tmpPkcs8 = key.ExportEncryptedPkcs8PrivateKey(password, s_windowsPbe); + key.ImportEncryptedPkcs8PrivateKey(password, tmpPkcs8, out _); + } + + Pkcs12Builder builder = new Pkcs12Builder(); + + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeContents keyContents = sameContainer ? null : new Pkcs12SafeContents(); + Pkcs12SafeContents keyEffectiveContents = keyContents ?? certContents; + + Pkcs12SafeBag certBag = certContents.AddCertificate(cert); + Pkcs12SafeBag keyBag; + + if (dontShroudKey) + { + keyBag = keyEffectiveContents.AddKeyUnencrypted(key); + } + else + { + keyBag = keyEffectiveContents.AddShroudedKey(key, password, s_windowsPbe); + } + + certBag.Attributes.Add(s_keyIdOne); + keyBag.Attributes.Add(s_keyIdOne); + + if (sameContainer) + { + AddContents(certContents, builder, password, encryptCertSafeContents); + } + else if (keyContainerLast) + { + AddContents(certContents, builder, password, encryptCertSafeContents); + AddContents(keyContents, builder, password, encryptKeySafeContents); + } + else + { + AddContents(keyContents, builder, password, encryptKeySafeContents); + AddContents(certContents, builder, password, encryptCertSafeContents); + } + + if (skipMac) + { + builder.SealWithoutIntegrity(); + } + else + { + builder.SealWithMac(password, s_digestAlgorithm, MacCount); + } + + ReadPfx(builder.Encode(), password, cert); + } + } + } +} diff --git a/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs b/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs new file mode 100644 index 000000000000..10a1ea730a0f --- /dev/null +++ b/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs @@ -0,0 +1,988 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.Runtime.InteropServices; +using System.Security.Cryptography.Pkcs; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public abstract partial class PfxFormatTests + { + // Use a MAC count of 1 because we're not persisting things, and the password + // is with the test... just save some CPU cycles. + private const int MacCount = 1; + + // Use SHA-1 for Windows 7-8.1 support. + private static readonly HashAlgorithmName s_digestAlgorithm = HashAlgorithmName.SHA1; + + // The PBE parameters used by Windows 7 for private keys. + // Needs to stay 3DES/SHA1 for Windows 7 to read it. + private static readonly PbeParameters s_windowsPbe = + new PbeParameters(PbeEncryptionAlgorithm.TripleDes3KeyPkcs12, HashAlgorithmName.SHA1, 2000); + + protected static readonly X509KeyStorageFlags s_importFlags = + Cert.EphemeralIfPossible | X509KeyStorageFlags.UserKeySet; + + protected static readonly X509KeyStorageFlags s_exportableImportFlags = + s_importFlags | X509KeyStorageFlags.Exportable; + + private static readonly Pkcs9LocalKeyId s_keyIdOne = new Pkcs9LocalKeyId(new byte[] { 1 }); + + // Windows 10 (1803? 1809? 1903?) changed the PFX loader to only fail unloadable keys that were + // referenced by certs. + // So our Unix loader can do the same. + private static readonly bool s_loaderFailsKeysEarly = + RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + !PlatformDetection.IsWindows10Version1803OrGreater; + + protected abstract void ReadPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedCert, + Action otherWork = null); + + protected abstract void ReadMultiPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedSingleCert, + X509Certificate2[] expectedOrder, + Action perCertOtherWork = null); + + protected abstract void ReadEmptyPfx(byte[] pfxBytes, string correctPassword); + protected abstract void ReadWrongPassword(byte[] pfxBytes, string wrongPassword); + + protected abstract void ReadUnreadablePfx( + byte[] pfxBytes, + string bestPassword, + // NTE_FAIL + int win32Error = -2146893792, + int altWin32Error = 0); + + [Fact] + public void EmptyPfx_NoMac() + { + Pkcs12Builder builder = new Pkcs12Builder(); + builder.SealWithoutIntegrity(); + ReadEmptyPfx(builder.Encode(), correctPassword: null); + } + + [Fact] + public void EmptyPfx_NoMac_ArbitraryPassword() + { + Pkcs12Builder builder = new Pkcs12Builder(); + builder.SealWithoutIntegrity(); + byte[] pfxBytes = builder.Encode(); + + ReadEmptyPfx(pfxBytes, "arbitrary password"); + ReadEmptyPfx(pfxBytes, "other arbitrary password"); + } + + [Fact] + public void EmptyPfx_EmptyPassword() + { + Pkcs12Builder builder = new Pkcs12Builder(); + builder.SealWithMac(string.Empty, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadEmptyPfx(pfxBytes, correctPassword: null); + ReadEmptyPfx(pfxBytes, correctPassword: string.Empty); + } + + [Fact] + public void EmptyPfx_NullPassword() + { + Pkcs12Builder builder = new Pkcs12Builder(); + builder.SealWithMac(null, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadEmptyPfx(pfxBytes, correctPassword: null); + ReadEmptyPfx(pfxBytes, correctPassword: string.Empty); + } + + [Fact] + public void EmptyPfx_BadPassword() + { + Pkcs12Builder builder = new Pkcs12Builder(); + builder.SealWithMac("correct password", s_digestAlgorithm, MacCount); + ReadWrongPassword(builder.Encode(), "wrong password"); + ReadWrongPassword(builder.Encode(), string.Empty); + ReadWrongPassword(builder.Encode(), null); + } + + [Fact] + public void OneCert_NoKeys_EncryptedNullPassword_NoMac() + { + using (X509Certificate2 cert = new X509Certificate2(TestData.MsCertificate)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + certContents.AddCertificate(cert); + builder.AddSafeContentsEncrypted(certContents, (string)null, s_windowsPbe); + builder.SealWithoutIntegrity(); + byte[] pfxBytes = builder.Encode(); + + ReadPfx(pfxBytes, null, cert); + ReadPfx(pfxBytes, string.Empty, cert); + } + } + + [Fact] + public void OneCert_NoKeys_EncryptedEmptyPassword_NoMac() + { + using (X509Certificate2 cert = new X509Certificate2(TestData.MsCertificate)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + certContents.AddCertificate(cert); + builder.AddSafeContentsEncrypted(certContents, string.Empty, s_windowsPbe); + builder.SealWithoutIntegrity(); + byte[] pfxBytes = builder.Encode(); + + ReadPfx(pfxBytes, null, cert); + ReadPfx(pfxBytes, string.Empty, cert); + } + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void OneCert_EncryptedEmptyPassword_OneKey_EncryptedNullPassword_NoMac(bool encryptKeySafe, bool associateKey) + { + // This test shows that while a null or empty password will result in both + // types being tested, the PFX contents have to be the same throughout. + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (AsymmetricAlgorithm key = cert.GetRSAPrivateKey()) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(key, (string)null, s_windowsPbe); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeBag certBag = certContents.AddCertificate(cert); + + if (associateKey) + { + keyBag.Attributes.Add(s_keyIdOne); + certBag.Attributes.Add(s_keyIdOne); + } + + AddContents(keyContents, builder, null, encryptKeySafe); + AddContents(certContents, builder, string.Empty, encrypt: true); + builder.SealWithoutIntegrity(); + byte[] pfxBytes = builder.Encode(); + + 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); + } + else + { + using (var publicOnlyCert = new X509Certificate2(cert.RawData)) + { + ReadPfx(pfxBytes, string.Empty, publicOnlyCert); + } + } + } + } + + [Fact] + public void OneCert_MismatchedKey() + { + string pw = nameof(OneCert_MismatchedKey); + + // Build the PFX in the normal Windows style, except the private key doesn't match. + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (RSA realKey = cert.GetRSAPrivateKey()) + using (RSA key = RSA.Create(realKey.KeySize)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeBag certBag = certContents.AddCertificate(cert); + + keyBag.Attributes.Add(s_keyIdOne); + certBag.Attributes.Add(s_keyIdOne); + + builder.AddSafeContentsUnencrypted(keyContents); + builder.AddSafeContentsEncrypted(certContents, pw, s_windowsPbe); + builder.SealWithoutIntegrity(); + byte[] pfxBytes = builder.Encode(); + + // On macOS the cert will come back with HasPrivateKey being false. + if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + using (var publicCert = new X509Certificate2(cert.RawData)) + { + ReadPfx( + pfxBytes, + pw, + publicCert); + } + + return; + } + + ReadPfx( + pfxBytes, + pw, + cert, + CheckKeyConsistencyFails); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void OneCert_TwoKeys_FirstWins(bool correctKeyFirst) + { + string pw = nameof(OneCert_TwoKeys_FirstWins); + + // Build the PFX in the normal Windows style, except the private key doesn't match. + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (RSA key = cert.GetRSAPrivateKey()) + using (RSA unrelated = RSA.Create(key.KeySize)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeBag keyBag; + Pkcs12SafeBag keyBag2; + Pkcs12SafeBag certBag = certContents.AddCertificate(cert); + + if (correctKeyFirst) + { + keyBag = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + keyBag2 = keyContents.AddShroudedKey(unrelated, pw, s_windowsPbe); + } + else + { + keyBag = keyContents.AddShroudedKey(unrelated, pw, s_windowsPbe); + keyBag2 = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + } + + keyBag.Attributes.Add(s_keyIdOne); + keyBag2.Attributes.Add(s_keyIdOne); + certBag.Attributes.Add(s_keyIdOne); + + builder.AddSafeContentsUnencrypted(keyContents); + builder.AddSafeContentsEncrypted(certContents, pw, s_windowsPbe); + builder.SealWithoutIntegrity(); + byte[] pfxBytes = builder.Encode(); + + // On macOS the cert will come back with HasPrivateKey being false when the + // incorrect key comes first + if (!correctKeyFirst && RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + using (var publicCert = new X509Certificate2(cert.RawData)) + { + ReadPfx( + pfxBytes, + pw, + publicCert); + } + + return; + } + + // The RSA "self-test" should pass when the correct key is first, + // and fail when the unrelated key is first. + Action followup = CheckKeyConsistency; + + if (!correctKeyFirst) + { + followup = CheckKeyConsistencyFails; + } + + ReadPfx( + pfxBytes, + pw, + cert, + followup); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void TwoCerts_OneKey(bool certWithKeyFirst) + { + string pw = nameof(TwoCerts_OneKey); + + // Build the PFX in the normal Windows style, except the private key doesn't match. + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (var cert2 = new X509Certificate2(TestData.MsCertificate)) + using (RSA key = cert.GetRSAPrivateKey()) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + + Pkcs12SafeBag certBag; + Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + X509Certificate2[] expectedOrder; + + if (certWithKeyFirst) + { + certBag = certContents.AddCertificate(cert); + certContents.AddCertificate(cert2); + + expectedOrder = new[] { cert2, cert }; + } + else + { + certContents.AddCertificate(cert2); + certBag = certContents.AddCertificate(cert); + + expectedOrder = new[] { cert, cert2 }; + } + + certBag.Attributes.Add(s_keyIdOne); + keyBag.Attributes.Add(s_keyIdOne); + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + ReadMultiPfx(builder.Encode(), pw, cert, expectedOrder); + } + } + + [Fact] + public void OneCert_ExtraKeyWithUnknownAlgorithm() + { + string pw = nameof(OneCert_ExtraKeyWithUnknownAlgorithm); + + using (var cert = new X509Certificate2(TestData.MsCertificate)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + + Pkcs8PrivateKeyInfo pk8 = new Pkcs8PrivateKeyInfo( + // The Microsoft organization OID, not an algorithm. + new Oid("1.3.6.1.4.1.311", null), + null, + new byte[] { 0x05, 0x00 }); + + // Note that neither the cert nor the key have a LocalKeyId attribute. + // The existence of this unknown key is enough to abort the load on older Windows. + keyContents.AddSafeBag(new Pkcs12ShroudedKeyBag(pk8.Encrypt(pw, s_windowsPbe))); + certContents.AddCertificate(cert); + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + if (s_loaderFailsKeysEarly) + { + ReadUnreadablePfx( + pfxBytes, + pw, + //NTE_BAD_ALGID, + win32Error: -2146893816); + } + else + { + ReadPfx(pfxBytes, pw, cert); + } + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void OneCert_ExtraKeyBadEncoding(bool badTag) + { + string pw = nameof(OneCert_ExtraKeyBadEncoding); + + using (var cert = new X509Certificate2(TestData.MsCertificate)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + + // SEQUENCE { INTEGER(1) } is not a valid RSAPrivateKey, it should be + // SEQUENCE { INTEGER(N), INTEGER(E), ... (D, P, Q, DP, DQ, QInv) } + // So the conclusion is "unexpected end of data" + byte[] badKeyBytes = { 0x30, 0x03, 0x02, 0x01, 0x01 }; + + // In "badTag" we make the INTEGER be OCTET STRING, triggering a different + // "uh, I can't read this..." error. + if (badTag) + { + badKeyBytes[2] = 0x04; + } + + Pkcs8PrivateKeyInfo pk8 = new Pkcs8PrivateKeyInfo( + // The correct RSA OID. + new Oid("1.2.840.113549.1.1.1", null), + null, + badKeyBytes, + skipCopies: true); + + // Note that neither the cert nor the key have a LocalKeyId attribute. + // The existence of this unreadable key is enough to abort the load on older Windows + keyContents.AddSafeBag(new Pkcs12ShroudedKeyBag(pk8.Encrypt(pw, s_windowsPbe))); + certContents.AddCertificate(cert); + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + if (s_loaderFailsKeysEarly) + { + // CRYPT_E_ASN1_BADTAG or CRYPT_E_ASN1_EOD + int expectedWin32Error = badTag ? -2146881269 : -2146881278; + + ReadUnreadablePfx( + pfxBytes, + pw, + expectedWin32Error); + } + else + { + ReadPfx(pfxBytes, pw, cert); + } + } + } + + [Fact] + public void OneCert_NoKey_WithLocalKeyId() + { + string pw = nameof(OneCert_NoKey_WithLocalKeyId); + + using (var cert = new X509Certificate2(TestData.MsCertificate)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + + Pkcs12CertBag certBag = certContents.AddCertificate(cert); + certBag.Attributes.Add(s_keyIdOne); + + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadPfx(pfxBytes, pw, cert); + } + } + + [Fact] + public void OneCert_TwentyKeys_NoMatches() + { + string pw = nameof(OneCert_NoKey_WithLocalKeyId); + + using (var cert = new X509Certificate2(TestData.MsCertificate)) + using (RSA rsa = RSA.Create(TestData.RsaBigExponentParams)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + + Pkcs12CertBag certBag = certContents.AddCertificate(cert); + certBag.Attributes.Add(s_keyIdOne); + + byte[] keyExport = rsa.ExportEncryptedPkcs8PrivateKey(pw, s_windowsPbe); + + for (int i = 0; i < 20; i++) + { + Pkcs12SafeBag keyBag = new Pkcs12ShroudedKeyBag(keyExport, skipCopy: true); + keyContents.AddSafeBag(keyBag); + + // Even with i=1 this won't match, because { 0x01 } != { 0x01, 0x00, 0x00, 0x00 } and + // { 0x01 } != { 0x00, 0x00, 0x00, 0x01 } (binary comparison, not "equivalence" comparison). + keyBag.Attributes.Add(new Pkcs9LocalKeyId(BitConverter.GetBytes(i))); + } + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadPfx(pfxBytes, pw, cert); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void TwoCerts_TwentyKeys_NoMatches(bool msCertFirst) + { + string pw = nameof(OneCert_NoKey_WithLocalKeyId); + + using (var certWithKey = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword)) + using (var cert = new X509Certificate2(certWithKey.RawData)) + using (var cert2 = new X509Certificate2(TestData.MsCertificate)) + using (RSA rsa = RSA.Create(TestData.RsaBigExponentParams)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12CertBag certBag; + + if (msCertFirst) + { + certBag = certContents.AddCertificate(cert2); + certBag.Attributes.Add(s_keyIdOne); + } + + certBag = certContents.AddCertificate(cert); + certBag.Attributes.Add(new Pkcs9LocalKeyId(cert.GetCertHash())); + + if (!msCertFirst) + { + certBag = certContents.AddCertificate(cert2); + certBag.Attributes.Add(s_keyIdOne); + } + + byte[] keyExport = rsa.ExportEncryptedPkcs8PrivateKey(pw, s_windowsPbe); + + for (int i = 0; i < 20; i++) + { + Pkcs12SafeBag keyBag = new Pkcs12ShroudedKeyBag(keyExport, skipCopy: true); + keyContents.AddSafeBag(keyBag); + + // Even with i=1 this won't match, because { 0x01 } != { 0x01, 0x00, 0x00, 0x00 } and + // { 0x01 } != { 0x00, 0x00, 0x00, 0x01 } (binary comparison, not "equivalence" comparison). + keyBag.Attributes.Add(new Pkcs9LocalKeyId(BitConverter.GetBytes(i))); + } + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadMultiPfx( + pfxBytes, + pw, + msCertFirst ? cert : cert2, + msCertFirst ? new[] { cert, cert2 } : new[] { cert2, cert }); + } + } + + [Fact] + public void OneCorruptCert() + { + string pw = nameof(OneCorruptCert); + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents contents = new Pkcs12SafeContents(); + contents.AddSafeBag(new Pkcs12CertBag(new Oid("1.2.840.113549.1.9.22.1"), new byte[] { 0x05, 0x00 })); + AddContents(contents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadUnreadablePfx( + pfxBytes, + pw, + // CRYPT_E_BAD_ENCODE + -2146885630); + } + + [Fact] + public void CertAndKey_NoLocalKeyId() + { + string pw = nameof(CertAndKey_NoLocalKeyId); + + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (RSA key = cert.GetRSAPrivateKey()) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + + keyContents.AddShroudedKey(key, pw, s_windowsPbe); + certContents.AddCertificate(cert); + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadPfx(pfxBytes, pw, cert, CheckKeyConsistency); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void SameCertTwice_NoKeys(bool addLocalKeyId) + { + string pw = nameof(SameCertTwice_NoKeys); + + using (var cert = new X509Certificate2(TestData.MsCertificate)) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + + Pkcs12SafeBag certBag = certContents.AddCertificate(cert); + Pkcs12SafeBag certBag2 = certContents.AddCertificate(cert); + + if (addLocalKeyId) + { + certBag.Attributes.Add(s_keyIdOne); + certBag2.Attributes.Add(s_keyIdOne); + } + + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadMultiPfx( + pfxBytes, + pw, + cert, + new[] { cert, cert }); + } + } + + [Fact] + public void TwoCerts_CrossedKeys() + { + string pw = nameof(SameCertTwice_NoKeys); + + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (var cert2 = new X509Certificate2(TestData.MsCertificate)) + using (RSA key = cert.GetRSAPrivateKey()) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + + Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + Pkcs12SafeBag certBag = certContents.AddCertificate(cert); + Pkcs12SafeBag certBag2 = certContents.AddCertificate(cert2); + + keyBag.Attributes.Add(s_keyIdOne); + certBag2.Attributes.Add(s_keyIdOne); + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + // Windows seems to be applying both the implicit match and the LocalKeyId match, + // so it detects two hits against the same key and fails. + ReadUnreadablePfx( + pfxBytes, + pw, + // NTE_BAD_DATA + -2146893819); + } + } + + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void CertAndKeyTwice(bool addLocalKeyId, bool crossIdentifiers) + { + string pw = nameof(CertAndKeyTwice); + + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (RSA key = cert.GetRSAPrivateKey()) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + + Pkcs12SafeBag key1 = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + Pkcs12SafeBag key2 = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + Pkcs12SafeBag cert1 = certContents.AddCertificate(cert); + Pkcs12SafeBag cert2 = certContents.AddCertificate(cert); + + if (addLocalKeyId) + { + (crossIdentifiers ? key2 : key1).Attributes.Add(s_keyIdOne); + cert1.Attributes.Add(s_keyIdOne); + + Pkcs9LocalKeyId id2 = new Pkcs9LocalKeyId(cert.GetCertHash()); + (crossIdentifiers ? key1 : key2).Attributes.Add(id2); + cert2.Attributes.Add(id2); + } + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + if (addLocalKeyId) + { + ReadMultiPfx( + pfxBytes, + pw, + cert, + new[] { cert, cert }, + CheckKeyConsistency); + } + else + { + ReadUnreadablePfx( + pfxBytes, + pw, + // NTE_BAD_DATA + -2146893819); + } + } + } + + [Fact] + public void CertAndKeyTwice_KeysUntagged() + { + string pw = nameof(CertAndKeyTwice); + + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (RSA key = cert.GetRSAPrivateKey()) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + + Pkcs12SafeBag key1 = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + Pkcs12SafeBag key2 = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + Pkcs12SafeBag cert1 = certContents.AddCertificate(cert); + Pkcs12SafeBag cert2 = certContents.AddCertificate(cert); + + Pkcs9LocalKeyId id2 = new Pkcs9LocalKeyId(cert.GetCertHash()); + Pkcs9LocalKeyId id3 = new Pkcs9LocalKeyId(BitConverter.GetBytes(3)); + Pkcs9LocalKeyId id4 = new Pkcs9LocalKeyId(BitConverter.GetBytes(4)); + cert1.Attributes.Add(s_keyIdOne); + cert2.Attributes.Add(id2); + key1.Attributes.Add(id3); + key2.Attributes.Add(id4); + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadUnreadablePfx( + pfxBytes, + pw, + // NTE_BAD_DATA + -2146893819); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CertTwice_KeyOnce(bool addLocalKeyId) + { + string pw = nameof(CertTwice_KeyOnce); + + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, s_exportableImportFlags)) + using (RSA key = cert.GetRSAPrivateKey()) + { + Pkcs12Builder builder = new Pkcs12Builder(); + Pkcs12SafeContents keyContents = new Pkcs12SafeContents(); + Pkcs12SafeContents certContents = new Pkcs12SafeContents(); + + Pkcs12SafeBag keyBag = keyContents.AddShroudedKey(key, pw, s_windowsPbe); + Pkcs12SafeBag certBag = certContents.AddCertificate(cert); + Pkcs12SafeBag certBag2 = certContents.AddCertificate(cert); + + if (addLocalKeyId) + { + certBag.Attributes.Add(s_keyIdOne); + certBag2.Attributes.Add(s_keyIdOne); + keyBag.Attributes.Add(s_keyIdOne); + } + + AddContents(keyContents, builder, pw, encrypt: false); + AddContents(certContents, builder, pw, encrypt: true); + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + byte[] pfxBytes = builder.Encode(); + + ReadUnreadablePfx( + pfxBytes, + pw, + // NTE_BAD_DATA + -2146893819); + } + } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void TwoCerts_TwoKeys_ManySafeContentsValues(bool invertCertOrder, bool invertKeyOrder) + { + string pw = nameof(TwoCerts_TwoKeys_ManySafeContentsValues); + + 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: true); + AddContents(secondCertContents, builder, pw, encrypt: true); + AddContents(irrelevant, builder, pw, encrypt: false); + AddContents(firstKeyContents, builder, pw, encrypt: false); + AddContents(firstCertContents, builder, pw, encrypt: true); + AddContents(irrelevant, builder, pw, encrypt: false); + AddContents(secondKeyContents, builder, pw, encrypt: true); + AddContents(irrelevant, builder, pw, encrypt: true); + + builder.SealWithMac(pw, s_digestAlgorithm, MacCount); + 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 (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && + !PlatformDetection.IsWindows8xOrLater) + { + followup = null; + } + + ReadMultiPfx( + pfxBytes, + pw, + first, + expectedOrder, + followup); + } + } + } + + private static void CheckKeyConsistency(X509Certificate2 cert) + { + using (RSA priv = cert.GetRSAPrivateKey()) + using (RSA pub = cert.GetRSAPublicKey()) + { + byte[] data = { 2, 7, 4 }; + byte[] signature = priv.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + Assert.True( + pub.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), + "Cert public key verifies signature from cert private key"); + } + } + + private static void CheckKeyConsistencyFails(X509Certificate2 cert) + { + using (RSA priv = cert.GetRSAPrivateKey()) + using (RSA pub = cert.GetRSAPublicKey()) + { + byte[] data = { 2, 7, 4 }; + byte[] signature = priv.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + + Assert.False( + pub.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1), + "Cert public key verifies signature from cert private key"); + } + } + + private static void AddContents( + Pkcs12SafeContents contents, + Pkcs12Builder builder, + string password, + bool encrypt) + { + if (encrypt) + { + builder.AddSafeContentsEncrypted(contents, password, s_windowsPbe); + } + else + { + builder.AddSafeContentsUnencrypted(contents); + } + } + + protected static void AssertCertEquals(X509Certificate2 expectedCert, X509Certificate2 actual) + { + if (expectedCert.HasPrivateKey) + { + Assert.True(actual.HasPrivateKey, "actual.HasPrivateKey"); + } + else + { + Assert.False(actual.HasPrivateKey, "actual.HasPrivateKey"); + } + + Assert.Equal(expectedCert.RawData.ByteArrayToHex(), actual.RawData.ByteArrayToHex()); + } + + protected static void AssertMessageContains(string expectedSubstring, Exception ex) + { + if (CultureInfo.CurrentCulture.Name == "en-US") + { + Assert.Contains(expectedSubstring, ex.Message); + } + } + } +} diff --git a/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_Collection.cs b/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_Collection.cs new file mode 100644 index 000000000000..8cf7f45734e8 --- /dev/null +++ b/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_Collection.cs @@ -0,0 +1,113 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public sealed class PfxFormatTests_Collection : PfxFormatTests + { + protected override void ReadPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedCert, + Action otherWork) + { + ReadPfx(pfxBytes, correctPassword, expectedCert, null, otherWork, s_importFlags); + ReadPfx(pfxBytes, correctPassword, expectedCert, null, otherWork, s_exportableImportFlags); + } + + protected override void ReadMultiPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedSingleCert, + X509Certificate2[] expectedOrder, + Action perCertOtherWork) + { + ReadPfx( + pfxBytes, + correctPassword, + expectedSingleCert, + expectedOrder, + perCertOtherWork, + s_importFlags); + + ReadPfx( + pfxBytes, + correctPassword, + expectedSingleCert, + expectedOrder, + perCertOtherWork, + s_exportableImportFlags); + } + + private void ReadPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedCert, + X509Certificate2[] expectedOrder, + Action otherWork, + X509KeyStorageFlags flags) + { + using (ImportedCollection imported = Cert.Import(pfxBytes, correctPassword, flags)) + { + X509Certificate2Collection coll = imported.Collection; + Assert.Equal(expectedOrder?.Length ?? 1, coll.Count); + + Span testOrder = expectedOrder == null ? + MemoryMarshal.CreateSpan(ref expectedCert, 1) : + expectedOrder.AsSpan(); + + for (int i = 0; i < testOrder.Length; i++) + { + X509Certificate2 actual = coll[i]; + AssertCertEquals(testOrder[i], actual); + otherWork?.Invoke(actual); + } + } + } + + protected override void ReadEmptyPfx(byte[] pfxBytes, string correctPassword) + { + X509Certificate2Collection coll = new X509Certificate2Collection(); + coll.Import(pfxBytes, correctPassword, s_importFlags); + Assert.Equal(0, coll.Count); + } + + protected override void ReadWrongPassword(byte[] pfxBytes, string wrongPassword) + { + X509Certificate2Collection coll = new X509Certificate2Collection(); + + CryptographicException ex = Assert.ThrowsAny( + () => coll.Import(pfxBytes, wrongPassword, s_importFlags)); + + AssertMessageContains("password", ex); + } + + protected override void ReadUnreadablePfx( + byte[] pfxBytes, + string bestPassword, + int win32Error, + int altWin32Error) + { + X509Certificate2Collection coll = new X509Certificate2Collection(); + + CryptographicException ex = Assert.ThrowsAny( + () => coll.Import(pfxBytes, bestPassword, s_importFlags)); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (altWin32Error != 0 && ex.HResult != altWin32Error) + { + Assert.Equal(win32Error, ex.HResult); + } + } + else + { + Assert.NotNull(ex.InnerException); + } + } + } +} diff --git a/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_SingleCert.cs b/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_SingleCert.cs new file mode 100644 index 000000000000..17e60e7bd3bd --- /dev/null +++ b/src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_SingleCert.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public sealed class PfxFormatTests_SingleCert : PfxFormatTests + { + protected override void ReadPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedCert, + Action otherWork) + { + ReadPfx(pfxBytes, correctPassword, expectedCert, otherWork, s_importFlags); + ReadPfx(pfxBytes, correctPassword, expectedCert, otherWork, s_exportableImportFlags); + } + + protected override void ReadMultiPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedSingleCert, + X509Certificate2[] expectedOrder, + Action perCertOtherWork) + { + ReadPfx(pfxBytes, correctPassword, expectedSingleCert, perCertOtherWork, s_importFlags); + ReadPfx(pfxBytes, correctPassword, expectedSingleCert, perCertOtherWork, s_exportableImportFlags); + } + + private void ReadPfx( + byte[] pfxBytes, + string correctPassword, + X509Certificate2 expectedCert, + Action otherWork, + X509KeyStorageFlags flags) + { + using (X509Certificate2 cert = new X509Certificate2(pfxBytes, correctPassword, flags)) + { + AssertCertEquals(expectedCert, cert); + otherWork?.Invoke(cert); + } + } + + protected override void ReadEmptyPfx(byte[] pfxBytes, string correctPassword) + { + CryptographicException ex = Assert.Throws( + () => new X509Certificate2(pfxBytes, correctPassword, s_importFlags)); + + AssertMessageContains("no certificates", ex); + } + + protected override void ReadWrongPassword(byte[] pfxBytes, string wrongPassword) + { + CryptographicException ex = Assert.ThrowsAny( + () => new X509Certificate2(pfxBytes, wrongPassword, s_importFlags)); + + AssertMessageContains("password", ex); + } + + protected override void ReadUnreadablePfx( + byte[] pfxBytes, + string bestPassword, + int win32Error, + int altWin32Error) + { + CryptographicException ex = Assert.ThrowsAny( + () => new X509Certificate2(pfxBytes, bestPassword, s_importFlags)); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (altWin32Error != 0 && ex.HResult != altWin32Error) + { + Assert.Equal(win32Error, ex.HResult); + } + } + else + { + Assert.NotNull(ex.InnerException); + } + } + } +} diff --git a/src/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj b/src/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj index d88c4351dc7e..a345bde7c002 100644 --- a/src/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj +++ b/src/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj @@ -28,6 +28,10 @@ + + + +