From 28d3f317abe7697dcc6ccd08fd18dc417e73bde4 Mon Sep 17 00:00:00 2001 From: Jeremy Barton Date: Thu, 27 May 2021 14:47:21 -0700 Subject: [PATCH] Add root store directory fallback on Linux/Unix. When reading the root store directory for the first time, if the read produced no data and the SSL_CERT_DIR environment variable wasn't set, see if /etc/ssl/certs gives a different answer. This change also changes the LastWriteTime model for caching to not pin the symlink target on the first read, and support the bundle file being a symlink (and the target being updated to trigger refresh). --- .../Interop.Crypto.cs | 12 +- .../pal_x509_root.c | 12 +- .../pal_x509_root.h | 4 +- .../Pal.Unix/CachedSystemStoreProvider.cs | 142 ++++++++++++++------- 4 files changed, 114 insertions(+), 56 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs index 3572c42..9e692c7 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.Crypto.cs @@ -68,21 +68,23 @@ internal static partial class Interop [return: MarshalAs(UnmanagedType.Bool)] internal static extern bool PushX509StackField(SafeSharedX509StackHandle stack, SafeX509Handle x509); - internal static string? GetX509RootStorePath() + internal static string? GetX509RootStorePath(out bool defaultPath) { - return Marshal.PtrToStringAnsi(GetX509RootStorePath_private()); + IntPtr ptr = GetX509RootStorePath_private(out byte usedDefault); + defaultPath = (usedDefault != 0); + return Marshal.PtrToStringAnsi(ptr); } internal static string? GetX509RootStoreFile() { - return Marshal.PtrToStringAnsi(GetX509RootStoreFile_private()); + return Marshal.PtrToStringAnsi(GetX509RootStoreFile_private(out _)); } [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_GetX509RootStorePath")] - private static extern IntPtr GetX509RootStorePath_private(); + private static extern IntPtr GetX509RootStorePath_private(out byte defaultPath); [DllImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_GetX509RootStoreFile")] - private static extern IntPtr GetX509RootStoreFile_private(); + private static extern IntPtr GetX509RootStoreFile_private(out byte defaultPath); [DllImport(Libraries.CryptoNative)] private static extern int CryptoNative_X509StoreSetVerifyTime( diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_x509_root.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_x509_root.c index 5c8d671..81b3d80 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_x509_root.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_x509_root.c @@ -6,25 +6,33 @@ #include -const char* CryptoNative_GetX509RootStorePath() +const char* CryptoNative_GetX509RootStorePath(uint8_t* defaultPath) { + assert(defaultPath != NULL); + const char* dir = getenv(X509_get_default_cert_dir_env()); + *defaultPath = 0; if (!dir) { dir = X509_get_default_cert_dir(); + *defaultPath = 1; } return dir; } -const char* CryptoNative_GetX509RootStoreFile() +const char* CryptoNative_GetX509RootStoreFile(uint8_t* defaultPath) { + assert(defaultPath != NULL); + const char* file = getenv(X509_get_default_cert_file_env()); + *defaultPath = 0; if (!file) { file = X509_get_default_cert_file(); + *defaultPath = 1; } return file; diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_x509_root.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_x509_root.h index ddcc439..207b600 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_x509_root.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_x509_root.h @@ -8,11 +8,11 @@ Look up the directory in which all certificate files therein are considered trusted (root or trusted intermediate). */ -PALEXPORT const char* CryptoNative_GetX509RootStorePath(void); +PALEXPORT const char* CryptoNative_GetX509RootStorePath(uint8_t* defaultPath); /* Look up the file in which all certificates are considered trusted (root or trusted intermediate), in addition to those files in the root store path. */ -PALEXPORT const char* CryptoNative_GetX509RootStoreFile(void); +PALEXPORT const char* CryptoNative_GetX509RootStoreFile(uint8_t* defaultPath); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CachedSystemStoreProvider.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CachedSystemStoreProvider.cs index 4cd99fa..b6a9ea5 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CachedSystemStoreProvider.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/CachedSystemStoreProvider.cs @@ -23,14 +23,13 @@ namespace Internal.Cryptography.Pal private static readonly TimeSpan s_lastWriteRecheckInterval = TimeSpan.FromSeconds(5); private static readonly TimeSpan s_assumeInvalidInterval = TimeSpan.FromMinutes(5); private static readonly Stopwatch s_recheckStopwatch = new Stopwatch(); - private static readonly DirectoryInfo? s_rootStoreDirectoryInfo = SafeOpenRootDirectoryInfo(); - private static readonly DirectoryInfo? s_rootLinkedStoreDirectoryInfo = SafeOpenLinkedRootDirectoryInfo(); + private static DirectoryInfo? s_rootStoreDirectoryInfo = SafeOpenRootDirectoryInfo(); + private static bool s_defaultRootDir; private static readonly FileInfo? s_rootStoreFileInfo = SafeOpenRootFileInfo(); // Use non-Value-Tuple so that it's an atomic update. private static Tuple? s_nativeCollections; private static DateTime s_directoryCertsLastWrite; - private static DateTime s_linkCertsLastWrite; private static DateTime s_fileCertsLastWrite; private readonly bool _isRoot; @@ -98,19 +97,16 @@ namespace Internal.Cryptography.Pal { FileInfo? fileInfo = s_rootStoreFileInfo; DirectoryInfo? dirInfo = s_rootStoreDirectoryInfo; - DirectoryInfo? linkInfo = s_rootLinkedStoreDirectoryInfo; fileInfo?.Refresh(); dirInfo?.Refresh(); - linkInfo?.Refresh(); if (ret == null || elapsed > s_assumeInvalidInterval || - (fileInfo != null && fileInfo.Exists && fileInfo.LastWriteTimeUtc != s_fileCertsLastWrite) || - (dirInfo != null && dirInfo.Exists && dirInfo.LastWriteTimeUtc != s_directoryCertsLastWrite) || - (linkInfo != null && linkInfo.Exists && linkInfo.LastWriteTimeUtc != s_linkCertsLastWrite)) + (fileInfo != null && fileInfo.Exists && ContentWriteTime(fileInfo) != s_fileCertsLastWrite) || + (dirInfo != null && dirInfo.Exists && ContentWriteTime(dirInfo) != s_directoryCertsLastWrite)) { - ret = LoadMachineStores(dirInfo, fileInfo, linkInfo); + ret = LoadMachineStores(dirInfo, fileInfo); } } } @@ -121,8 +117,7 @@ namespace Internal.Cryptography.Pal private static Tuple LoadMachineStores( DirectoryInfo? rootStorePath, - FileInfo? rootStoreFile, - DirectoryInfo? linkedRootPath) + FileInfo? rootStoreFile) { Debug.Assert( Monitor.IsEntered(s_recheckStopwatch), @@ -135,40 +130,65 @@ namespace Internal.Cryptography.Pal DateTime newFileTime = default; DateTime newDirTime = default; - DateTime newLinkTime = default; var uniqueRootCerts = new HashSet(); var uniqueIntermediateCerts = new HashSet(); + bool firstLoad = (s_nativeCollections == null); if (rootStoreFile != null && rootStoreFile.Exists) { - newFileTime = rootStoreFile.LastWriteTimeUtc; + newFileTime = ContentWriteTime(rootStoreFile); ProcessFile(rootStoreFile); } + bool hasStoreData = false; + if (rootStorePath != null && rootStorePath.Exists) { - newDirTime = rootStorePath.LastWriteTimeUtc; - foreach (FileInfo file in rootStorePath.EnumerateFiles()) + newDirTime = ContentWriteTime(rootStorePath); + hasStoreData = ProcessDir(rootStorePath); + } + + if (firstLoad && !hasStoreData && s_defaultRootDir) + { + DirectoryInfo etcSslCerts = new DirectoryInfo("/etc/ssl/certs"); + + if (etcSslCerts.Exists) { - ProcessFile(file); + DateTime tmpTime = ContentWriteTime(etcSslCerts); + hasStoreData = ProcessDir(etcSslCerts); + + if (hasStoreData) + { + newDirTime = tmpTime; + s_rootStoreDirectoryInfo = etcSslCerts; + } } } - if (linkedRootPath != null && linkedRootPath.Exists) + bool ProcessDir(DirectoryInfo dir) { - newLinkTime = linkedRootPath.LastWriteTimeUtc; + bool hasStoreData = false; + + foreach (FileInfo file in dir.EnumerateFiles()) + { + hasStoreData |= ProcessFile(file); + } + + return hasStoreData; } - void ProcessFile(FileInfo file) + bool ProcessFile(FileInfo file) { + bool readData = false; + using (SafeBioHandle fileBio = Interop.Crypto.BioNewFile(file.FullName, "rb")) { // The handle may be invalid, for example when we don't have read permission for the file. if (fileBio.IsInvalid) { Interop.Crypto.ErrClearError(); - return; + return false; } // Some distros ship with two variants of the same certificate. @@ -181,6 +201,7 @@ namespace Internal.Cryptography.Pal while (OpenSslX509CertificateReader.TryReadX509PemNoAux(fileBio, out pal) || OpenSslX509CertificateReader.TryReadX509Der(fileBio, out pal)) { + readData = true; X509Certificate2 cert = new X509Certificate2(pal); // The HashSets are just used for uniqueness filters, they do not survive this method. @@ -226,6 +247,8 @@ namespace Internal.Cryptography.Pal cert.Dispose(); } } + + return readData; } foreach (X509Certificate2 cert in uniqueRootCerts) @@ -255,7 +278,6 @@ namespace Internal.Cryptography.Pal Volatile.Write(ref s_nativeCollections, newCollections); s_directoryCertsLastWrite = newDirTime; s_fileCertsLastWrite = newFileTime; - s_linkCertsLastWrite = newLinkTime; s_recheckStopwatch.Restart(); return newCollections; } @@ -282,7 +304,7 @@ namespace Internal.Cryptography.Pal private static DirectoryInfo? SafeOpenRootDirectoryInfo() { - string? rootDirectory = Interop.Crypto.GetX509RootStorePath(); + string? rootDirectory = Interop.Crypto.GetX509RootStorePath(out s_defaultRootDir); if (!string.IsNullOrEmpty(rootDirectory)) { @@ -300,42 +322,68 @@ namespace Internal.Cryptography.Pal return null; } - private static DirectoryInfo? SafeOpenLinkedRootDirectoryInfo() + private static DateTime ContentWriteTime(FileInfo info) { - string? rootDirectory = Interop.Crypto.GetX509RootStorePath(); + string path = info.FullName; + string? target = Interop.Sys.ReadLink(path); - if (!string.IsNullOrEmpty(rootDirectory)) + if (string.IsNullOrEmpty(target)) { - string? linkedDirectory = Interop.Sys.ReadLink(rootDirectory); - if (linkedDirectory == null) - { - return null; - } + return info.LastWriteTimeUtc; + } - if (linkedDirectory[0] == '/') - { - rootDirectory = linkedDirectory; - } - else - { - // relative link - var root = new DirectoryInfo(rootDirectory); - root = new DirectoryInfo(Path.Join(root.Parent?.FullName, linkedDirectory)); - rootDirectory = root.FullName; - } + if (target[0] != '/') + { + target = Path.Join(info.Directory?.FullName, target); + } - try + try + { + var targetInfo = new FileInfo(target); + + if (targetInfo.Exists) { - return new DirectoryInfo(rootDirectory); + return targetInfo.LastWriteTimeUtc; } - catch (ArgumentException) + } + catch (ArgumentException) + { + // If we can't load information about the link path, just treat it as not a link. + } + + return info.LastWriteTimeUtc; + } + + private static DateTime ContentWriteTime(DirectoryInfo info) + { + string path = info.FullName; + string? target = Interop.Sys.ReadLink(path); + + if (string.IsNullOrEmpty(target)) + { + return info.LastWriteTimeUtc; + } + + if (target[0] != '/') + { + target = Path.Join(info.Parent?.FullName, target); + } + + try + { + var targetInfo = new DirectoryInfo(target); + + if (targetInfo.Exists) { - // If SSL_CERT_DIR is set to the empty string, or anything else which gives - // "The path is not of a legal form", then the GetX509RootStoreFile value is ignored. + return targetInfo.LastWriteTimeUtc; } } + catch (ArgumentException) + { + // If we can't load information about the link path, just treat it as not a link. + } - return null; + return info.LastWriteTimeUtc; } } } -- 2.7.4