1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
4 
5 using Microsoft.Win32.SafeHandles;
6 using System;
7 using System.IO;
8 using System.Diagnostics;
9 using System.Runtime.InteropServices;
10 using System.Security;
11 using System.Security.Cryptography.X509Certificates;
12 
13 using Internal.Cryptography.Pal.Native;
14 
15 namespace Internal.Cryptography.Pal
16 {
17     internal sealed partial class CertificatePal : IDisposable, ICertificatePal
18     {
FromBlob(byte[] rawData, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)19         public static ICertificatePal FromBlob(byte[] rawData, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)
20         {
21             return FromBlobOrFile(rawData, null, password, keyStorageFlags);
22         }
23 
FromFile(string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)24         public static ICertificatePal FromFile(string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)
25         {
26             return FromBlobOrFile(null, fileName, password, keyStorageFlags);
27         }
28 
FromBlobOrFile(byte[] rawData, string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)29         private static ICertificatePal FromBlobOrFile(byte[] rawData, string fileName, SafePasswordHandle password, X509KeyStorageFlags keyStorageFlags)
30         {
31             Debug.Assert(rawData != null || fileName != null);
32             Debug.Assert(password != null);
33 
34             bool loadFromFile = (fileName != null);
35 
36             PfxCertStoreFlags pfxCertStoreFlags = MapKeyStorageFlags(keyStorageFlags);
37             bool persistKeySet = (0 != (keyStorageFlags & X509KeyStorageFlags.PersistKeySet));
38 
39             CertEncodingType msgAndCertEncodingType;
40             ContentType contentType;
41             FormatType formatType;
42             SafeCertStoreHandle hCertStore = null;
43             SafeCryptMsgHandle hCryptMsg = null;
44             SafeCertContextHandle pCertContext = null;
45 
46             try
47             {
48                 unsafe
49                 {
50                     fixed (byte* pRawData = rawData)
51                     {
52                         fixed (char* pFileName = fileName)
53                         {
54                             CRYPTOAPI_BLOB certBlob = new CRYPTOAPI_BLOB(loadFromFile ? 0 : rawData.Length, pRawData);
55 
56                             CertQueryObjectType objectType = loadFromFile ? CertQueryObjectType.CERT_QUERY_OBJECT_FILE : CertQueryObjectType.CERT_QUERY_OBJECT_BLOB;
57                             void* pvObject = loadFromFile ? (void*)pFileName : (void*)&certBlob;
58 
59                             bool success = Interop.crypt32.CryptQueryObject(
60                                 objectType,
61                                 pvObject,
62                                 X509ExpectedContentTypeFlags,
63                                 X509ExpectedFormatTypeFlags,
64                                 0,
65                                 out msgAndCertEncodingType,
66                                 out contentType,
67                                 out formatType,
68                                 out hCertStore,
69                                 out hCryptMsg,
70                                 out pCertContext
71                                     );
72                             if (!success)
73                             {
74                                 int hr = Marshal.GetHRForLastWin32Error();
75                                 throw hr.ToCryptographicException();
76                             }
77                         }
78                     }
79 
80                     if (contentType == ContentType.CERT_QUERY_CONTENT_PKCS7_SIGNED || contentType == ContentType.CERT_QUERY_CONTENT_PKCS7_SIGNED_EMBED)
81                     {
82                         pCertContext = GetSignerInPKCS7Store(hCertStore, hCryptMsg);
83                     }
84                     else if (contentType == ContentType.CERT_QUERY_CONTENT_PFX)
85                     {
86                         if (loadFromFile)
87                             rawData = File.ReadAllBytes(fileName);
88                         pCertContext = FilterPFXStore(rawData, password, pfxCertStoreFlags);
89                     }
90 
91                     CertificatePal pal = new CertificatePal(pCertContext, deleteKeyContainer: !persistKeySet);
92                     pCertContext = null;
93                     return pal;
94                 }
95             }
96             finally
97             {
98                 if (hCertStore != null)
99                     hCertStore.Dispose();
100                 if (hCryptMsg != null)
101                     hCryptMsg.Dispose();
102                 if (pCertContext != null)
103                     pCertContext.Dispose();
104             }
105         }
106 
GetSignerInPKCS7Store(SafeCertStoreHandle hCertStore, SafeCryptMsgHandle hCryptMsg)107         private static unsafe SafeCertContextHandle GetSignerInPKCS7Store(SafeCertStoreHandle hCertStore, SafeCryptMsgHandle hCryptMsg)
108         {
109             // make sure that there is at least one signer of the certificate store
110             int dwSigners;
111             int cbSigners = sizeof(int);
112             if (!Interop.crypt32.CryptMsgGetParam(hCryptMsg, CryptMessageParameterType.CMSG_SIGNER_COUNT_PARAM, 0, out dwSigners, ref cbSigners))
113                 throw Marshal.GetHRForLastWin32Error().ToCryptographicException();
114             if (dwSigners == 0)
115                 throw ErrorCode.CRYPT_E_SIGNER_NOT_FOUND.ToCryptographicException();
116 
117             // get the first signer from the store, and use that as the loaded certificate
118             int cbData = 0;
119             if (!Interop.crypt32.CryptMsgGetParam(hCryptMsg, CryptMessageParameterType.CMSG_SIGNER_INFO_PARAM, 0, null, ref cbData))
120                 throw Marshal.GetHRForLastWin32Error().ToCryptographicException();
121 
122             fixed (byte* pCmsgSignerBytes = new byte[cbData])
123             {
124                 if (!Interop.crypt32.CryptMsgGetParam(hCryptMsg, CryptMessageParameterType.CMSG_SIGNER_INFO_PARAM, 0, pCmsgSignerBytes, ref cbData))
125                     throw Marshal.GetHRForLastWin32Error().ToCryptographicException();
126 
127                 CMSG_SIGNER_INFO_Partial* pCmsgSignerInfo = (CMSG_SIGNER_INFO_Partial*)pCmsgSignerBytes;
128 
129                 CERT_INFO certInfo = default(CERT_INFO);
130                 certInfo.Issuer.cbData = pCmsgSignerInfo->Issuer.cbData;
131                 certInfo.Issuer.pbData = pCmsgSignerInfo->Issuer.pbData;
132                 certInfo.SerialNumber.cbData = pCmsgSignerInfo->SerialNumber.cbData;
133                 certInfo.SerialNumber.pbData = pCmsgSignerInfo->SerialNumber.pbData;
134 
135                 SafeCertContextHandle pCertContext = null;
136                 if (!Interop.crypt32.CertFindCertificateInStore(hCertStore, CertFindType.CERT_FIND_SUBJECT_CERT, &certInfo, ref pCertContext))
137                     throw Marshal.GetHRForLastWin32Error().ToCryptographicException();
138                 return pCertContext;
139             }
140         }
141 
FilterPFXStore(byte[] rawData, SafePasswordHandle password, PfxCertStoreFlags pfxCertStoreFlags)142         private static SafeCertContextHandle FilterPFXStore(byte[] rawData, SafePasswordHandle password, PfxCertStoreFlags pfxCertStoreFlags)
143         {
144             SafeCertStoreHandle hStore;
145             unsafe
146             {
147                 fixed (byte* pbRawData = rawData)
148                 {
149                     CRYPTOAPI_BLOB certBlob = new CRYPTOAPI_BLOB(rawData.Length, pbRawData);
150                     hStore = Interop.crypt32.PFXImportCertStore(ref certBlob, password, pfxCertStoreFlags);
151                     if (hStore.IsInvalid)
152                         throw Marshal.GetHRForLastWin32Error().ToCryptographicException();
153                 }
154             }
155 
156             try
157             {
158                 // Find the first cert with private key. If none, then simply take the very first cert. Along the way, delete the keycontainers
159                 // of any cert we don't accept.
160                 SafeCertContextHandle pCertContext = SafeCertContextHandle.InvalidHandle;
161                 SafeCertContextHandle pEnumContext = null;
162                 while (Interop.crypt32.CertEnumCertificatesInStore(hStore, ref pEnumContext))
163                 {
164                     if (pEnumContext.ContainsPrivateKey)
165                     {
166                         if ((!pCertContext.IsInvalid) && pCertContext.ContainsPrivateKey)
167                         {
168                             // We already found our chosen one. Free up this one's key and move on.
169 
170                             // If this one has a persisted private key, clean up the key file.
171                             // If it was an ephemeral private key no action is required.
172                             if (pEnumContext.HasPersistedPrivateKey)
173                             {
174                                 SafeCertContextHandleWithKeyContainerDeletion.DeleteKeyContainer(pEnumContext);
175                             }
176                         }
177                         else
178                         {
179                             // Found our first cert that has a private key. Set him up as our chosen one but keep iterating
180                             // as we need to free up the keys of any remaining certs.
181                             pCertContext.Dispose();
182                             pCertContext = pEnumContext.Duplicate();
183                         }
184                     }
185                     else
186                     {
187                         if (pCertContext.IsInvalid)
188                             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.
189                     }
190                 }
191 
192                 if (pCertContext.IsInvalid)
193                 {
194                     // For compat, setting "hr" to ERROR_INVALID_PARAMETER even though ERROR_INVALID_PARAMETER is not actually an HRESULT.
195                     throw ErrorCode.ERROR_INVALID_PARAMETER.ToCryptographicException();
196                 }
197 
198                 return pCertContext;
199             }
200             finally
201             {
202                 hStore.Dispose();
203             }
204         }
205 
MapKeyStorageFlags(X509KeyStorageFlags keyStorageFlags)206         private static PfxCertStoreFlags MapKeyStorageFlags(X509KeyStorageFlags keyStorageFlags)
207         {
208             if ((keyStorageFlags & X509Certificate.KeyStorageFlagsAll) != keyStorageFlags)
209                 throw new ArgumentException(SR.Argument_InvalidFlag, nameof(keyStorageFlags));
210 
211             PfxCertStoreFlags pfxCertStoreFlags = 0;
212             if ((keyStorageFlags & X509KeyStorageFlags.UserKeySet) == X509KeyStorageFlags.UserKeySet)
213                 pfxCertStoreFlags |= PfxCertStoreFlags.CRYPT_USER_KEYSET;
214             else if ((keyStorageFlags & X509KeyStorageFlags.MachineKeySet) == X509KeyStorageFlags.MachineKeySet)
215                 pfxCertStoreFlags |= PfxCertStoreFlags.CRYPT_MACHINE_KEYSET;
216 
217             if ((keyStorageFlags & X509KeyStorageFlags.Exportable) == X509KeyStorageFlags.Exportable)
218                 pfxCertStoreFlags |= PfxCertStoreFlags.CRYPT_EXPORTABLE;
219             if ((keyStorageFlags & X509KeyStorageFlags.UserProtected) == X509KeyStorageFlags.UserProtected)
220                 pfxCertStoreFlags |= PfxCertStoreFlags.CRYPT_USER_PROTECTED;
221 
222             // If a user is asking for an Ephemeral key they should be willing to test their code to find out
223             // that it will no longer import into CAPI. This solves problems of legacy CSPs being
224             // difficult to do SHA-2 RSA signatures with, simplifies the story for UWP, and reduces the
225             // complexity of pointer interpretation.
226             if ((keyStorageFlags & X509KeyStorageFlags.EphemeralKeySet) == X509KeyStorageFlags.EphemeralKeySet)
227                 pfxCertStoreFlags |= PfxCertStoreFlags.PKCS12_NO_PERSIST_KEY | PfxCertStoreFlags.PKCS12_ALWAYS_CNG_KSP;
228 
229             // In the full .NET Framework loading a PFX then adding the key to the Windows Certificate Store would
230             // enable a native application compiled against CAPI to find that private key and interoperate with it.
231             //
232             // For CoreFX this behavior is being retained.
233             //
234             // For .NET Native (UWP) the API used to delete the private key (if it wasn't added to a store) is not
235             // allowed to be called if the key is stored in CAPI.  So for UWP force the key to be stored in the
236             // CNG Key Storage Provider, then deleting the key with CngKey.Delete will clean up the file on disk, too.
237 
238             return pfxCertStoreFlags;
239         }
240 
241         private const ExpectedContentTypeFlags X509ExpectedContentTypeFlags =
242             ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_CERT |
243             ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_SERIALIZED_CERT |
244             ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED |
245             ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED |
246             ExpectedContentTypeFlags.CERT_QUERY_CONTENT_FLAG_PFX;
247 
248         private const ExpectedFormatTypeFlags X509ExpectedFormatTypeFlags = ExpectedFormatTypeFlags.CERT_QUERY_FORMAT_FLAG_ALL;
249     }
250 }
251