PageRenderTime 77ms CodeModel.GetById 52ms RepoModel.GetById 0ms app.codeStats 0ms

/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/DirectoryBasedStoreProvider.cs

https://gitlab.com/0072016/0072016-corefx-
C# | 374 lines | 266 code | 58 blank | 50 comment | 45 complexity | 13049d24e0eaa43b614fedef48a6f196 MD5 | raw file
  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. using System;
  5. using System.Diagnostics;
  6. using System.IO;
  7. using System.Security.Cryptography;
  8. using System.Security.Cryptography.X509Certificates;
  9. using System.Text;
  10. namespace Internal.Cryptography.Pal
  11. {
  12. /// <summary>
  13. /// Provides an implementation of an X509Store which is backed by files in a directory.
  14. /// </summary>
  15. internal class DirectoryBasedStoreProvider : IStorePal
  16. {
  17. // {thumbprint}.1.pfx to {thumbprint}.9.pfx
  18. private const int MaxSaveAttempts = 9;
  19. private const string PfxExtension = ".pfx";
  20. // *.pfx ({thumbprint}.pfx or {thumbprint}.{ordinal}.pfx)
  21. private const string PfxWildcard = "*" + PfxExtension;
  22. // .*.pfx ({thumbprint}.{ordinal}.pfx)
  23. private const string PfxOrdinalWildcard = "." + PfxWildcard;
  24. private static string s_userStoreRoot;
  25. private readonly string _storePath;
  26. private readonly bool _readOnly;
  27. #if DEBUG
  28. static DirectoryBasedStoreProvider()
  29. {
  30. Debug.Assert(
  31. 0 == OpenFlags.ReadOnly,
  32. "OpenFlags.ReadOnly is not zero, read-only detection will not work");
  33. }
  34. #endif
  35. internal DirectoryBasedStoreProvider(string storeName, OpenFlags openFlags)
  36. {
  37. if (string.IsNullOrEmpty(storeName))
  38. {
  39. throw new CryptographicException(SR.Arg_EmptyOrNullString);
  40. }
  41. string directoryName = GetDirectoryName(storeName);
  42. if (s_userStoreRoot == null)
  43. {
  44. // Do this here instead of a static field initializer so that
  45. // the static initializer isn't capable of throwing the "home directory not found"
  46. // exception.
  47. s_userStoreRoot = PersistedFiles.GetUserFeatureDirectory(
  48. X509Persistence.CryptographyFeatureName,
  49. X509Persistence.X509StoresSubFeatureName);
  50. }
  51. _storePath = Path.Combine(s_userStoreRoot, directoryName);
  52. if (0 != (openFlags & OpenFlags.OpenExistingOnly))
  53. {
  54. if (!Directory.Exists(_storePath))
  55. {
  56. throw new CryptographicException(SR.Cryptography_X509_StoreNotFound);
  57. }
  58. }
  59. // ReadOnly is 0x00, so it is implicit unless either ReadWrite or MaxAllowed
  60. // was requested.
  61. OpenFlags writeFlags = openFlags & (OpenFlags.ReadWrite | OpenFlags.MaxAllowed);
  62. if (writeFlags == OpenFlags.ReadOnly)
  63. {
  64. _readOnly = true;
  65. }
  66. }
  67. public void Dispose()
  68. {
  69. }
  70. public byte[] Export(X509ContentType contentType, string password)
  71. {
  72. // Export is for X509Certificate2Collections in their IStorePal guise,
  73. // if someone wanted to export whole stores they'd need to do
  74. // store.Certificates.Export(...), which would end up in the
  75. // CollectionBackedStoreProvider.
  76. Debug.Fail("Export was unexpected on a DirectoryBasedStore");
  77. throw new InvalidOperationException();
  78. }
  79. public void CopyTo(X509Certificate2Collection collection)
  80. {
  81. Debug.Assert(collection != null);
  82. if (!Directory.Exists(_storePath))
  83. {
  84. return;
  85. }
  86. foreach (string filePath in Directory.EnumerateFiles(_storePath, PfxWildcard))
  87. {
  88. try
  89. {
  90. collection.Add(new X509Certificate2(filePath));
  91. }
  92. catch (CryptographicException)
  93. {
  94. // The file wasn't a certificate, move on to the next one.
  95. }
  96. }
  97. }
  98. public void Add(ICertificatePal certPal)
  99. {
  100. if (_readOnly)
  101. {
  102. // Windows compatibility: Remove only throws when it needs to do work, add throws always.
  103. throw new CryptographicException(SR.Cryptography_X509_StoreReadOnly);
  104. }
  105. // This may well be the first time that we've added something to this store.
  106. Directory.CreateDirectory(_storePath);
  107. uint userId = Interop.Sys.GetEUid();
  108. EnsureDirectoryPermissions(_storePath, userId);
  109. OpenSslX509CertificateReader cert = (OpenSslX509CertificateReader)certPal;
  110. using (X509Certificate2 copy = new X509Certificate2(cert.DuplicateHandles()))
  111. {
  112. string thumbprint = copy.Thumbprint;
  113. bool findOpenSlot;
  114. // The odds are low that we'd have a thumbprint collision, but check anyways.
  115. string existingFilename = FindExistingFilename(copy, _storePath, out findOpenSlot);
  116. if (existingFilename != null)
  117. {
  118. if (!copy.HasPrivateKey)
  119. {
  120. return;
  121. }
  122. try
  123. {
  124. using (X509Certificate2 fromFile = new X509Certificate2(existingFilename))
  125. {
  126. if (fromFile.HasPrivateKey)
  127. {
  128. // We have a private key, the file has a private key, we're done here.
  129. return;
  130. }
  131. }
  132. }
  133. catch (CryptographicException)
  134. {
  135. // We can't read this file anymore, but a moment ago it was this certificate,
  136. // so go ahead and overwrite it.
  137. }
  138. }
  139. string destinationFilename;
  140. FileMode mode = FileMode.CreateNew;
  141. if (existingFilename != null)
  142. {
  143. destinationFilename = existingFilename;
  144. mode = FileMode.Create;
  145. }
  146. else if (findOpenSlot)
  147. {
  148. destinationFilename = FindOpenSlot(thumbprint);
  149. }
  150. else
  151. {
  152. destinationFilename = Path.Combine(_storePath, thumbprint + PfxExtension);
  153. }
  154. using (FileStream stream = new FileStream(destinationFilename, mode))
  155. {
  156. EnsureFilePermissions(stream, userId);
  157. byte[] pkcs12 = copy.Export(X509ContentType.Pkcs12);
  158. stream.Write(pkcs12, 0, pkcs12.Length);
  159. }
  160. }
  161. }
  162. public void Remove(ICertificatePal certPal)
  163. {
  164. OpenSslX509CertificateReader cert = (OpenSslX509CertificateReader)certPal;
  165. using (X509Certificate2 copy = new X509Certificate2(cert.DuplicateHandles()))
  166. {
  167. bool hadCandidates;
  168. string currentFilename = FindExistingFilename(copy, _storePath, out hadCandidates);
  169. if (currentFilename != null)
  170. {
  171. if (_readOnly)
  172. {
  173. // Windows compatibility, the readonly check isn't done until after a match is found.
  174. throw new CryptographicException(SR.Cryptography_X509_StoreReadOnly);
  175. }
  176. File.Delete(currentFilename);
  177. }
  178. }
  179. }
  180. private static string FindExistingFilename(X509Certificate2 cert, string storePath, out bool hadCandidates)
  181. {
  182. hadCandidates = false;
  183. foreach (string maybeMatch in Directory.EnumerateFiles(storePath, cert.Thumbprint + PfxWildcard))
  184. {
  185. hadCandidates = true;
  186. try
  187. {
  188. using (X509Certificate2 candidate = new X509Certificate2(maybeMatch))
  189. {
  190. if (candidate.Equals(cert))
  191. {
  192. return maybeMatch;
  193. }
  194. }
  195. }
  196. catch (CryptographicException)
  197. {
  198. // Contents weren't interpretable as a certificate, so it's not a match.
  199. }
  200. }
  201. return null;
  202. }
  203. private string FindOpenSlot(string thumbprint)
  204. {
  205. // We already know that {thumbprint}.pfx is taken, so start with {thumbprint}.1.pfx
  206. // We need space for {thumbprint} (thumbprint.Length)
  207. // And ".0.pfx" (6)
  208. // If MaxSaveAttempts is big enough to use more than one digit, we need that space, too (MaxSaveAttempts / 10)
  209. StringBuilder pathBuilder = new StringBuilder(thumbprint.Length + PfxOrdinalWildcard.Length + (MaxSaveAttempts / 10));
  210. pathBuilder.Append(thumbprint);
  211. pathBuilder.Append('.');
  212. int prefixLength = pathBuilder.Length;
  213. for (int i = 1; i <= MaxSaveAttempts; i++)
  214. {
  215. pathBuilder.Length = prefixLength;
  216. pathBuilder.Append(i);
  217. pathBuilder.Append(PfxExtension);
  218. string builtPath = Path.Combine(_storePath, pathBuilder.ToString());
  219. if (!File.Exists(builtPath))
  220. {
  221. return builtPath;
  222. }
  223. }
  224. throw new CryptographicException(SR.Cryptography_X509_StoreNoFileAvailable);
  225. }
  226. private static string GetDirectoryName(string storeName)
  227. {
  228. Debug.Assert(storeName != null);
  229. try
  230. {
  231. string fileName = Path.GetFileName(storeName);
  232. if (!StringComparer.Ordinal.Equals(storeName, fileName))
  233. {
  234. throw new CryptographicException(SR.Format(SR.Security_InvalidValue, "storeName"));
  235. }
  236. }
  237. catch (IOException e)
  238. {
  239. throw new CryptographicException(e.Message, e);
  240. }
  241. return storeName.ToLowerInvariant();
  242. }
  243. /// <summary>
  244. /// Checks the store directory has the correct permissions.
  245. /// </summary>
  246. /// <param name="path">
  247. /// The path of the directory to check.
  248. /// </param>
  249. /// <param name="userId">
  250. /// The current userId from GetEUid().
  251. /// </param>
  252. private static void EnsureDirectoryPermissions(string path, uint userId)
  253. {
  254. Interop.Sys.FileStatus dirStat;
  255. if (Interop.Sys.Stat(path, out dirStat) != 0)
  256. {
  257. Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
  258. throw new CryptographicException(
  259. SR.Cryptography_FileStatusError,
  260. new IOException(error.GetErrorMessage(), error.RawErrno));
  261. }
  262. if (dirStat.Uid != userId)
  263. {
  264. throw new CryptographicException(SR.Format(SR.Cryptography_OwnerNotCurrentUser, path));
  265. }
  266. if ((dirStat.Mode & (int)Interop.Sys.Permissions.S_IRWXU) != (int)Interop.Sys.Permissions.S_IRWXU)
  267. {
  268. throw new CryptographicException(SR.Format(SR.Cryptography_InvalidDirectoryPermissions, path));
  269. }
  270. }
  271. /// <summary>
  272. /// Checks the file has the correct permissions and attempts to modify them if they're inappropriate.
  273. /// </summary>
  274. /// <param name="stream">
  275. /// The file stream to check.
  276. /// </param>
  277. /// <param name="userId">
  278. /// The current userId from GetEUid().
  279. /// </param>
  280. private static void EnsureFilePermissions(FileStream stream, uint userId)
  281. {
  282. // Verify that we're creating files with u+rw and g-rw, o-rw.
  283. const Interop.Sys.Permissions requiredPermissions =
  284. Interop.Sys.Permissions.S_IRUSR | Interop.Sys.Permissions.S_IWUSR;
  285. const Interop.Sys.Permissions forbiddenPermissions =
  286. Interop.Sys.Permissions.S_IRGRP | Interop.Sys.Permissions.S_IWGRP |
  287. Interop.Sys.Permissions.S_IROTH | Interop.Sys.Permissions.S_IWOTH;
  288. Interop.Sys.FileStatus stat;
  289. if (Interop.Sys.FStat(stream.SafeFileHandle, out stat) != 0)
  290. {
  291. Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
  292. throw new CryptographicException(
  293. SR.Cryptography_FileStatusError,
  294. new IOException(error.GetErrorMessage(), error.RawErrno));
  295. }
  296. if (stat.Uid != userId)
  297. {
  298. throw new CryptographicException(SR.Format(SR.Cryptography_OwnerNotCurrentUser, stream.Name));
  299. }
  300. if ((stat.Mode & (int)requiredPermissions) != (int)requiredPermissions ||
  301. (stat.Mode & (int)forbiddenPermissions) != 0)
  302. {
  303. if (Interop.Sys.FChMod(stream.SafeFileHandle, (int)requiredPermissions) < 0)
  304. {
  305. Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
  306. throw new CryptographicException(
  307. SR.Format(SR.Cryptography_InvalidFilePermissions, stream.Name),
  308. new IOException(error.GetErrorMessage(), error.RawErrno));
  309. }
  310. Debug.Assert(Interop.Sys.FStat(stream.SafeFileHandle, out stat) == 0);
  311. Debug.Assert((stat.Mode & (int)requiredPermissions) == (int)requiredPermissions);
  312. Debug.Assert((stat.Mode & (int)forbiddenPermissions) == 0);
  313. }
  314. }
  315. }
  316. }