Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Add support for custom macOS keychains exposed using X509Store API #30603

Merged
merged 8 commits into from
Jul 10, 2018

Conversation

filipnavara
Copy link
Member

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.

Copy link
Member

@bartonjs bartonjs left a 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).
Copy link
Member

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)
Copy link
Member

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))
Copy link
Member

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))
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Member Author

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");
Copy link
Member

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(
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member Author

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.

Copy link
Member

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;
Copy link
Member

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);
Copy link
Member

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.

@bartonjs
Copy link
Member

There are already comments in the CoreFX unit tests referencing a similar need.

Where? Creating new stores during tests (aside from "store creation tests") is a bad idea. It taints the system.

@filipnavara
Copy link
Member Author

Thanks for the throughout review. I will address the comments and add unit tests.

There are already comments in the CoreFX unit tests referencing a similar need.

Where? Creating new stores during tests (aside from "store creation tests") is a bad idea. It taints the system.

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() {}
        */

@filipnavara filipnavara reopened this Jun 22, 2018
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");
Copy link
Member

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()?

Copy link
Member Author

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.
@filipnavara filipnavara changed the title WIP: Add support for custom macOS keychains exposed using X509Store API Add support for custom macOS keychains exposed using X509Store API Jun 27, 2018
@filipnavara
Copy link
Member Author

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crate => create

Copy link
Member Author

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");
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

private class

@bartonjs
Copy link
Member

Looks like I'm down to spelling and formatting, so probably good on the next iteration :)

@filipnavara
Copy link
Member Author

Any chance to get this merged?

@bartonjs
Copy link
Member

@dotnet-bot Test this please

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants