-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Add support for custom macOS keychains exposed using X509Store API #30603
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea seems reasonable, since I noticed that it prevents breaking out of the one directory that the UI would use by default.
Keychain creation on macOS requires more input than is captured with the X509Store API, so attempting to create a new store will fail with a `PlatformNotSupportedException`. | ||
If a keychain is opened by P/Invoke to SecKeychainOpen, the resulting `IntPtr` can be passed to `new X509Store(IntPtr)` to obtain a read/write-capable store (subject to the current user's permissions). | ||
Custom store creation on macOS with the X509Store API will create a new keychain with no password. To create a keychain with password a P/Invoke to SecKeychainCreate could be used. | ||
The resulting `IntPtr` can be passed to `new X509Store(IntPtr)` to obtain a read/write-capable store (subject to the current user's permissions). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IntPtr based sentence should still mention SecKeychainOpen, since this code will only find it in the user Keychains library.
And presumably you'd want to guard the "this works" to say for StoreLocation.CurrentUser
// If it doesn't work (errSecAuthFailed) then fail silently and fallback to the | ||
// default behavior of user interaction. | ||
osStatus = AppleCryptoNative_SecKeychainUnlock(keychain, 0, Array.Empty<byte>()); | ||
if (osStatus == 0 || osStatus == -25293) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a const instead of just a comment.
const int errSecAuthFailed = -25293;
if (osStatus == 0 || osStatus == errSecAuthFailed)
@@ -109,7 +109,18 @@ public static IStorePal FromSystemStore(string storeName, StoreLocation storeLoc | |||
if (ordinalIgnoreCase.Equals("Disallowed", storeName)) | |||
return AppleTrustStore.OpenStore(StoreName.Disallowed, storeLocation, openFlags); | |||
|
|||
break; | |||
if (!IsValidStoreName(storeName)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the ifs from here on out aren't part of the same "case insensitive switch", please switch to explicit bracing.
@@ -109,7 +109,18 @@ public static IStorePal FromSystemStore(string storeName, StoreLocation storeLoc | |||
if (ordinalIgnoreCase.Equals("Disallowed", storeName)) | |||
return AppleTrustStore.OpenStore(StoreName.Disallowed, storeLocation, openFlags); | |||
|
|||
break; | |||
if (!IsValidStoreName(storeName)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the "store creation is not supported on this platform" message really the best message for invalid store names?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The message is actually "The platform does not have a definition for an X509 certificate store named '{0}' with a StoreLocation of '{1}', and does not support creating it.", which may not be perfect, but it is not wrong either.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, and then it also carries a PlatformNotSupportedException within it.
The Linux version of not being a valid store name is at https://github.com/dotnet/corefx/blob/master/src/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/DirectoryBasedStoreProvider.cs#L313-L325
Which suggests that there's an IOException hiding here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I was sure I saw it in the Linux version, but I couldn't find now. I will follow that one then. Thanks!
if (!IsValidStoreName(storeName)) | ||
break; | ||
|
||
var storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", storeName + ".keychain"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please chop this line up, and var
=> string
per the style rules.
internal static SafeKeychainHandle CreateKeychain(string keychainPath) | ||
{ | ||
SafeKeychainHandle keychain; | ||
int osStatus = AppleCryptoNative_SecKeychainCreate( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know I made sure my temporaries don't show up in Keychain.App. Will these? Presumably they should, since they'd show up in MMC on Windows; but you would want to check and see if you need to add them to the enumerate list, or not unadd them, or whatever.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently they don't, but they are under the default location in the "Add Keychain" option. Not sure if there's a reliable way to accomplish this, but I will investigate.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked at the SecKeychainSetSearchList API and it doesn't look very safe for concurrent access. Moreover it changes the search behavior when looking for passwords in the system. I don't think that is desirable, but I am open to second opinion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I thought something did it by default. But I'm not seeing that, and I'll accept the reasoning.
@@ -131,6 +142,11 @@ public static IStorePal FromSystemStore(string storeName, StoreLocation storeLoc | |||
throw new CryptographicException(message, new PlatformNotSupportedException(message)); | |||
} | |||
|
|||
private static bool IsValidStoreName(string storeName) | |||
{ | |||
return !String.IsNullOrWhiteSpace(storeName) && Path.GetFileName(storeName) == storeName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
String
=> string
, per the style rules
if ((openFlags & OpenFlags.OpenExistingOnly) == OpenFlags.OpenExistingOnly) | ||
throw new CryptographicException(SR.Cryptography_X509_StoreNotFound); | ||
|
||
return AppleKeychainStore.CreateKeychain(storePath, openFlags); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to need tests. Since it will modify the keychain display list they should be [OuterLoop]
; and the tests will have to P/Invoke to SecKeychainDelete since we don't have public API for that.
X509FilesystemTests.Unix.cs is the spiritual equivalent for Linux.
Where? Creating new stores during tests (aside from "store creation tests") is a bad idea. It taints the system. |
Thanks for the throughout review. I will address the comments and add unit tests.
I was referring to this comment in corefx/src/System.Security.Cryptography.X509Certificates/tests/X509StoreTests.cs: /* Placeholder information for these tests until they can be written to run reliably.
* Currently such tests would create physical files (Unix) and\or certificates (Windows)
* which can collide with other running tests that use the same cert, or from a
* test suite running more than once at the same time on the same machine.
* Ideally, we use a GUID-named store to aoiv collitions with proper cleanup on Unix and Windows
* and\or have lower testing hooks or use Microsoft Fakes Framework to redirect
* and encapsulate the actual storage logic so it can be tested, along with mock exceptions
* to verify exception handling.
* See issue https://github.com/dotnet/corefx/issues/12833
* and https://github.com/dotnet/corefx/issues/12223
[Fact]
public static void TestAddAndRemove() {}
[Fact]
public static void TestAddRangeAndRemoveRange() {}
*/ |
if (!IsValidStoreName(storeName)) | ||
throw new CryptographicException(SR.Format(SR.Security_InvalidValue, nameof(storeName))); | ||
|
||
storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Keychains", storeName + ".keychain"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm. A concern I have with this approach is that on Windows and Linux the store name is case insensitive. And on macOS it will now become case sensitive. Perhaps this should normalize the store name to ToLowerInvariant()?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can do that. However it won't be possible to access third-party stores with non-lower-case names. My particular use case doesn't require that and it will still be possible using native APIs, so I am fine with that... (will write a unit test for it)
… stores them in memory temporarily until they are closed. Also make the file names always lower-case to preserve case-insensitive behavior from other platforms.
Any more comments? Anything I forgot to test? |
@@ -161,6 +174,52 @@ internal static SafeCFArrayHandle KeychainEnumerateIdentities(SafeKeychainHandle | |||
throw new CryptographicException(); | |||
} | |||
|
|||
internal static SafeKeychainHandle CreateOrOpenKeychain(string keychainPath, bool crateAllowed) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
crate
=> create
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh, good catch...a search for "crate" in CoreFX sources actually shows a couple more occurrences. Most of them are in tests, but one of them is in XmlMelformMapping string. How ironic :-)
if (!IsValidStoreName(storeName)) | ||
throw new CryptographicException(SR.Format(SR.Security_InvalidValue, nameof(storeName))); | ||
|
||
storePath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Library", "Keychains", storeName.ToLowerInvariant() + ".keychain"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please chop the long line (my VS code segment width and Github's default width are pretty similar)
string storePath = Path.Combine(
Environment.GetFolderPath(etc),
"Library",
"Keychains",
storeName.ToLowerInvariant() + ".keychain");
@@ -269,5 +346,21 @@ private static int GetStoreCertificateCount(X509Store store) | |||
return coll.Collection.Count; | |||
} | |||
} | |||
|
|||
class TemporaryX509Store : IDisposable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
private class
Looks like I'm down to spelling and formatting, so probably good on the next iteration :) |
Any chance to get this merged? |
@dotnet-bot Test this please |
Rationale: The X509Store API is heavily used by Mono unit tests and it is helpful to have it available on all supported platforms. There are already comments in the CoreFX unit tests referencing a similar need.