From 1338e4e8a542c3b3e20d99e544e09860f23e99ea Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Fri, 8 Nov 2019 12:34:38 -0800 Subject: [PATCH] Use a custom PFX reader/writer on Unix OSes This change moves PFX import and export primarily into managed code to work around inconsistencies across the operating systems. Current issues: * Linux * Reading * PKCS12_parse doesn't support multiple cert-with-keys. * PKCS12_parse doesn't support reading a PFX with no MAC. * OpenSSL 1.0 had a weird bug where an ECDSA cert inexplicably didn't match to its key. * Writing * PKCS12_create doesn't support multiple cert-with-keys. * PKCS12_create doesn't support writing empty collections. * macOS * Reading * Either SecItemImport does not understand the NULL (vs Empty) password, or we called it wrong... it/we cannot load a PFX which is MACd with the NULL password. * SecItemImport can only support "normalized" PFXes, where "normalized" means "how Windows XP would have written it": * PFX * SafeContents0 (no encryption) (won't load keys from an encrypted SafeContents, IIRC) * ShroudedKey0 (won't load keys from KeyBag (unencrypted), only ShroudedKeyBag (encrypted)) * ... * ShroudedKeyN * SafeContents1 (encrypted) (won't load certs from an unencrypted SafeContents, IIRC) * Cert0 * ... * CertM * MAC * AlgId: HMAC-SHA-1 (IIRC this was a requirement, but it's also the only allowed algorithm on Win7 or Win8.1...) * Writing * SecItemExport fails to create a PFX with only public keys (or, at least, with non-keychain-based certificates). * SecItemExport fails to create a PFX where some elements are in different keychains than others (including "some elements are not in a keychain"). This change moves the necessary ASN types from the Pkcs12 library into Common so they're shared between Pkcs12Info/Pkcs12Builder and X509Certificates, then uses a managed loader and managed writer. Quirks: * SecItemImport(PKCS8) doesn't support marking keys as non-exportable, so non-exportable keyloads on macOS read a PFX, write a normalized PFX in memory, then call SecItemImport(PKCS12). * Because one of the failure modes of SecItemImport(PKCS12) is that it returns certs without private keys associated, it's not possible to call SecItemImport first and fall back to the managed loader. * Windows and Linux both will happily return the wrong private key with a cert if the PFX says to do so, but on macOS the SecIdentityRef creation fails and the cert comes back with no private key. * This isn't a very realistic situation outside of our tests, so it's not something worth doing heroics for right now. The easiest answer is to make HasPrivateKey be true but the GetPrivateKey methods throw... but that's still different than the other platforms, and would be very weird with SslStream. --- .../OSX/Interop.CoreFoundation.CFData.cs | 25 +- .../src/Interop/OSX/Interop.CoreFoundation.cs | 39 +- .../Interop.SecKeyRef.Export.cs | 40 +- .../Interop.SecKeyRef.cs | 13 + .../Interop.X509.cs | 116 +- .../Interop.X509.cs | 2 +- .../Asn1/AlgorithmIdentifierAsn.manual.cs | 2 +- .../Cryptography}/Asn1/DigestInfoAsn.xml | 2 +- .../Cryptography}/Asn1/DigestInfoAsn.xml.cs | 2 +- .../Cryptography/Asn1/Pkcs12}/CertBagAsn.xml | 2 +- .../Asn1/Pkcs12}/CertBagAsn.xml.cs | 2 +- .../Cryptography/Asn1/Pkcs12}/MacData.xml | 4 +- .../Cryptography/Asn1/Pkcs12}/MacData.xml.cs | 6 +- .../Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs | 84 ++ .../Cryptography/Asn1/Pkcs12}/PfxAsn.xml | 6 +- .../Cryptography/Asn1/Pkcs12}/PfxAsn.xml.cs | 12 +- .../Cryptography/Asn1/Pkcs12}/SafeBagAsn.xml | 2 +- .../Asn1/Pkcs12}/SafeBagAsn.xml.cs | 2 +- .../Asn1/Pkcs7}/ContentInfoAsn.xml | 2 +- .../Asn1/Pkcs7}/ContentInfoAsn.xml.cs | 2 +- .../Asn1/Pkcs7}/EncryptedContentInfoAsn.xml | 2 +- .../Pkcs7}/EncryptedContentInfoAsn.xml.cs | 2 +- .../Asn1/Pkcs7}/EncryptedDataAsn.xml | 4 +- .../Asn1/Pkcs7}/EncryptedDataAsn.xml.cs | 6 +- .../Security/Cryptography/CryptoPool.cs | 8 + .../pal_x509.c | 203 +++- .../pal_x509.h | 17 + .../Cryptography/Pal/AnyOS/ManagedPal.Asn.cs | 3 +- .../Pal/AnyOS/ManagedPal.Decode.cs | 1 + .../src/Internal/Cryptography/PkcsHelpers.cs | 2 +- .../System.Security.Cryptography.Pkcs.csproj | 88 +- .../Pkcs/Asn1/EnvelopedDataAsn.xml | 2 +- .../Pkcs/Asn1/EnvelopedDataAsn.xml.cs | 4 +- .../Cryptography/Pkcs/Pkcs12Builder.cs | 1 + .../Cryptography/Pkcs/Pkcs12CertBag.cs | 2 +- .../Security/Cryptography/Pkcs/Pkcs12Info.cs | 70 +- .../Cryptography/Pkcs/Pkcs12SafeContents.cs | 4 +- .../Pkcs/Rfc3161TimestampToken.cs | 1 + .../Security/Cryptography/Pkcs/SignedCms.cs | 1 + .../src/Internal/Cryptography/Helpers.cs | 31 +- .../Pal.OSX/AppleCertificatePal.Pkcs12.cs | 148 +++ .../Pal.OSX/AppleCertificatePal.cs | 117 ++- .../Cryptography/Pal.OSX/ApplePkcs12Reader.cs | 98 ++ .../Pal.OSX/StorePal.ExportPal.cs | 87 +- .../Pal.OSX/StorePal.LoaderPal.cs | 73 +- .../Internal/Cryptography/Pal.OSX/StorePal.cs | 45 +- .../Internal/Cryptography/Pal.OSX/X509Pal.cs | 14 + .../Cryptography/Pal.Unix/ExportProvider.cs | 145 +-- .../Pal.Unix/OpenSslPkcs12Reader.cs | 171 +-- .../Pal.Unix/OpenSslX509CertificateReader.cs | 55 +- .../Pal.Unix/OpenSslX509Encoder.cs | 19 +- .../Cryptography/Pal.Unix/PkcsFormatReader.cs | 86 +- .../Cryptography/Pal.Unix/StorePal.cs | 8 +- .../Pal.Unix/UnixExportProvider.cs | 565 ++++++++++ .../Cryptography/Pal.Unix/UnixPkcs12Reader.cs | 772 ++++++++++++++ .../Pal.Windows/CertificatePal.Import.cs | 9 +- .../SafeHandles/SafePasswordHandle.Unix.cs | 28 - .../SafeHandles/SafePasswordHandle.Windows.cs | 28 - .../Win32/SafeHandles/SafePasswordHandle.cs | 33 +- .../src/Resources/Strings.resx | 9 + ...urity.Cryptography.X509Certificates.csproj | 165 ++- .../Asn1/DistributionPointAsn.xml.cs | 19 +- .../Asn1/DistributionPointNameAsn.xml.cs | 21 +- .../tests/CertTests.cs | 29 +- .../tests/CollectionTests.cs | 39 +- .../tests/CtorTests.cs | 6 +- .../tests/ExportTests.cs | 3 - .../PfxFormatTests.SingleCertGenerator.cs | 137 +++ .../tests/PfxFormatTests.cs | 988 ++++++++++++++++++ .../tests/PfxFormatTests_Collection.cs | 113 ++ .../tests/PfxFormatTests_SingleCert.cs | 85 ++ ...Cryptography.X509Certificates.Tests.csproj | 4 + 72 files changed, 4059 insertions(+), 877 deletions(-) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs => Common/src/System/Security/Cryptography}/Asn1/DigestInfoAsn.xml (91%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs => Common/src/System/Security/Cryptography}/Asn1/DigestInfoAsn.xml.cs (98%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs12}/CertBagAsn.xml (89%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs12}/CertBagAsn.xml.cs (98%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs12}/MacData.xml (88%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs12}/MacData.xml.cs (94%) create mode 100644 src/Common/src/System/Security/Cryptography/Asn1/Pkcs12/PfxAsn.manual.cs rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs12}/PfxAsn.xml (79%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs12}/PfxAsn.xml.cs (83%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs12}/SafeBagAsn.xml (92%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs12}/SafeBagAsn.xml.cs (98%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs7}/ContentInfoAsn.xml (89%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs7}/ContentInfoAsn.xml.cs (98%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs7}/EncryptedContentInfoAsn.xml (93%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs7}/EncryptedContentInfoAsn.xml.cs (98%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs7}/EncryptedDataAsn.xml (85%) rename src/{System.Security.Cryptography.Pkcs/src/System/Security/Cryptography/Pkcs/Asn1 => Common/src/System/Security/Cryptography/Asn1/Pkcs7}/EncryptedDataAsn.xml.cs (92%) create mode 100644 src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/AppleCertificatePal.Pkcs12.cs create mode 100644 src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.OSX/ApplePkcs12Reader.cs create mode 100644 src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixExportProvider.cs create mode 100644 src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnixPkcs12Reader.cs delete mode 100644 src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.Unix.cs delete mode 100644 src/System.Security.Cryptography.X509Certificates/src/Microsoft/Win32/SafeHandles/SafePasswordHandle.Windows.cs create mode 100644 src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.SingleCertGenerator.cs create mode 100644 src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests.cs create mode 100644 src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_Collection.cs create mode 100644 src/System.Security.Cryptography.X509Certificates/tests/PfxFormatTests_SingleCert.cs 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 @@ + + + +