From: Elinor Fung Date: Fri, 19 Mar 2021 17:24:26 +0000 (-0700) Subject: [AndroidCrypto] Implement Root (read-only) and CurrentUser/My certificate stores... X-Git-Tag: submit/tizen/20210909.063632~2564 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=eae9a1d125592bb93013ad169d287543dedc54ae;p=platform%2Fupstream%2Fdotnet%2Fruntime.git [AndroidCrypto] Implement Root (read-only) and CurrentUser/My certificate stores (#48862) --- diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509.cs index 2ec104f..b40581f 100644 --- a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509.cs +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509.cs @@ -89,6 +89,7 @@ internal static partial class Interop DSA, EC, RSA, + UnknownAlgorithm = -1, } [DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509PublicKey")] diff --git a/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509Store.cs b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509Store.cs new file mode 100644 index 0000000..52e12eb --- /dev/null +++ b/src/libraries/Common/src/Interop/Android/System.Security.Cryptography.Native.Android/Interop.X509Store.cs @@ -0,0 +1,71 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Win32.SafeHandles; + +internal static partial class Interop +{ + internal static partial class AndroidCrypto + { + [DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509StoreAddCertificate")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern unsafe bool X509StoreAddCertificate( + SafeX509StoreHandle store, + SafeX509Handle cert, + string hashString); + + [DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509StoreAddCertificateWithPrivateKey")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern unsafe bool X509StoreAddCertificateWithPrivateKey( + SafeX509StoreHandle store, + SafeX509Handle cert, + SafeKeyHandle key, + PAL_KeyAlgorithm algorithm, + string hashString); + + [DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509StoreContainsCertificate")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern unsafe bool X509StoreContainsCertificate( + SafeX509StoreHandle store, + SafeX509Handle cert, + string hashString); + + [DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509StoreEnumerateCertificates")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern unsafe bool X509StoreEnumerateCertificates( + SafeX509StoreHandle storeHandle, + delegate* unmanaged callback, + void *callbackContext); + + [DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509StoreEnumerateTrustedCertificates")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern unsafe bool X509StoreEnumerateTrustedCertificates( + byte systemOnly, + delegate* unmanaged callback, + void *callbackContext); + + [DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509StoreOpenDefault")] + internal static extern unsafe SafeX509StoreHandle X509StoreOpenDefault(); + + [DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509StoreRemoveCertificate")] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern unsafe bool X509StoreRemoveCertificate( + SafeX509StoreHandle store, + SafeX509Handle cert, + string hashString); + } +} + +namespace System.Security.Cryptography.X509Certificates +{ + internal sealed class SafeX509StoreHandle : Interop.JObjectLifetime.SafeJObjectHandle + { + public SafeX509StoreHandle() + { + } + } +} diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/CMakeLists.txt b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/CMakeLists.txt index 51b1466..21771ac 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/CMakeLists.txt +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/CMakeLists.txt @@ -23,6 +23,7 @@ set(NATIVECRYPTO_SOURCES pal_ssl.c pal_sslstream.c pal_x509.c + pal_x509store.c ) add_library(System.Security.Cryptography.Native.Android diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_eckey.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_eckey.c index d25b9a7..e3e6542 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_eckey.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_eckey.c @@ -19,7 +19,7 @@ EC_KEY* AndroidCryptoNative_NewEcKey(jobject curveParameters, jobject keyPair) return keyInfo; } -EC_KEY* AndroidCryptoNative_NewEcKeyFromPublicKey(JNIEnv *env, jobject /*ECPublicKey*/ publicKey) +EC_KEY* AndroidCryptoNative_NewEcKeyFromKeys(JNIEnv *env, jobject /*ECPublicKey*/ publicKey, jobject /*ECPrivateKey*/ privateKey) { assert(publicKey != NULL); @@ -27,7 +27,7 @@ EC_KEY* AndroidCryptoNative_NewEcKeyFromPublicKey(JNIEnv *env, jobject /*ECPubli return NULL; jobject curveParameters = (*env)->CallObjectMethod(env, publicKey, g_ECPublicKeyGetParams); - return AndroidCryptoNative_NewEcKey(ToGRef(env, curveParameters), AndroidCryptoNative_CreateKeyPair(env, publicKey, NULL)); + return AndroidCryptoNative_NewEcKey(ToGRef(env, curveParameters), AndroidCryptoNative_CreateKeyPair(env, publicKey, privateKey)); } #pragma clang diagnostic push diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_eckey.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_eckey.h index 14692ab..2127155 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_eckey.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_eckey.h @@ -16,7 +16,7 @@ typedef struct EC_KEY } EC_KEY; EC_KEY* AndroidCryptoNative_NewEcKey(jobject curveParameters, jobject keyPair); -EC_KEY* AndroidCryptoNative_NewEcKeyFromPublicKey(JNIEnv *env, jobject /*ECPublicKey*/ publicKey); +EC_KEY* AndroidCryptoNative_NewEcKeyFromKeys(JNIEnv *env, jobject /*ECPublicKey*/ publicKey, jobject /*ECPrivateKey*/ privateKey); /* Cleans up and deletes an EC_KEY instance. diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_jni.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_jni.c index d35b2c4..bd63173 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_jni.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_jni.c @@ -85,6 +85,12 @@ jmethodID g_sslCtxGetDefaultSslParamsMethod; jclass g_GCMParameterSpecClass; jmethodID g_GCMParameterSpecCtor; +// java/security/interfaces/DSAKey +jclass g_DSAKeyClass; + +// java/security/interfaces/ECKey +jclass g_ECKeyClass; + // java/security/interfaces/RSAKey jclass g_RSAKeyClass; jmethodID g_RSAKeyGetModulus; @@ -106,6 +112,27 @@ jmethodID g_keyPairGenInitializeWithParamsMethod; jmethodID g_keyPairGenInitializeMethod; jmethodID g_keyPairGenGenKeyPairMethod; +// java/security/KeyStore +jclass g_KeyStoreClass; +jmethodID g_KeyStoreGetInstance; +jmethodID g_KeyStoreAliases; +jmethodID g_KeyStoreContainsAlias; +jmethodID g_KeyStoreDeleteEntry; +jmethodID g_KeyStoreGetCertificate; +jmethodID g_KeyStoreGetEntry; +jmethodID g_KeyStoreLoad; +jmethodID g_KeyStoreSetCertificateEntry; +jmethodID g_KeyStoreSetKeyEntry; + +// java/security/KeyStore$PrivateKeyEntry +jclass g_PrivateKeyEntryClass; +jmethodID g_PrivateKeyEntryGetCertificate; +jmethodID g_PrivateKeyEntryGetPrivateKey; + +// java/security/KeyStore$TrustedCertificateEntry +jclass g_TrustedCertificateEntryClass; +jmethodID g_TrustedCertificateEntryGetTrustedCertificate; + // java/security/Signature jclass g_SignatureClass; jmethodID g_SignatureGetInstance; @@ -130,6 +157,7 @@ jmethodID g_CertPathGetEncoded; // java/security/cert/X509Certificate jclass g_X509CertClass; +jmethodID g_X509CertEquals; jmethodID g_X509CertGetEncoded; jmethodID g_X509CertGetPublicKey; @@ -254,6 +282,11 @@ jmethodID g_CollectionSize; jclass g_DateClass; jmethodID g_DateGetTime; +// java/util/Enumeration +jclass g_Enumeration; +jmethodID g_EnumerationHasMoreElements; +jmethodID g_EnumerationNextElement; + // java/util/Iterator jclass g_IteratorClass; jmethodID g_IteratorHasNext; @@ -537,9 +570,14 @@ JNI_OnLoad(JavaVM *vm, void *reserved) g_CertPathClass = GetClassGRef(env, "java/security/cert/CertPath"); g_CertPathGetEncoded = GetMethod(env, false, g_CertPathClass, "getEncoded", "(Ljava/lang/String;)[B"); - g_X509CertClass = GetClassGRef(env, "java/security/cert/X509Certificate"); - g_X509CertGetEncoded = GetMethod(env, false, g_X509CertClass, "getEncoded", "()[B"); - g_X509CertGetPublicKey = GetMethod(env, false, g_X509CertClass, "getPublicKey", "()Ljava/security/PublicKey;"); + g_X509CertClass = GetClassGRef(env, "java/security/cert/X509Certificate"); + g_X509CertEquals = GetMethod(env, false, g_X509CertClass, "equals", "(Ljava/lang/Object;)Z"); + g_X509CertGetEncoded = GetMethod(env, false, g_X509CertClass, "getEncoded", "()[B"); + g_X509CertGetPublicKey = GetMethod(env, false, g_X509CertClass, "getPublicKey", "()Ljava/security/PublicKey;"); + + g_DSAKeyClass = GetClassGRef(env, "java/security/interfaces/DSAKey"); + + g_ECKeyClass = GetClassGRef(env, "java/security/interfaces/ECKey"); g_RSAKeyClass = GetClassGRef(env, "java/security/interfaces/RSAKey"); g_RSAKeyGetModulus = GetMethod(env, false, g_RSAKeyClass, "getModulus", "()Ljava/math/BigInteger;"); @@ -558,6 +596,24 @@ JNI_OnLoad(JavaVM *vm, void *reserved) g_keyPairGenInitializeWithParamsMethod = GetMethod(env, false, g_keyPairGenClass, "initialize", "(Ljava/security/spec/AlgorithmParameterSpec;)V"); g_keyPairGenGenKeyPairMethod = GetMethod(env, false, g_keyPairGenClass, "genKeyPair", "()Ljava/security/KeyPair;"); + g_KeyStoreClass = GetClassGRef(env, "java/security/KeyStore"); + g_KeyStoreGetInstance = GetMethod(env, true, g_KeyStoreClass, "getInstance", "(Ljava/lang/String;)Ljava/security/KeyStore;"); + g_KeyStoreAliases = GetMethod(env, false, g_KeyStoreClass, "aliases", "()Ljava/util/Enumeration;"); + g_KeyStoreContainsAlias = GetMethod(env, false, g_KeyStoreClass, "containsAlias", "(Ljava/lang/String;)Z"); + g_KeyStoreDeleteEntry = GetMethod(env, false, g_KeyStoreClass, "deleteEntry", "(Ljava/lang/String;)V"); + g_KeyStoreGetCertificate = GetMethod(env, false, g_KeyStoreClass, "getCertificate", "(Ljava/lang/String;)Ljava/security/cert/Certificate;"); + g_KeyStoreGetEntry = GetMethod(env, false, g_KeyStoreClass, "getEntry", "(Ljava/lang/String;Ljava/security/KeyStore$ProtectionParameter;)Ljava/security/KeyStore$Entry;"); + g_KeyStoreLoad = GetMethod(env, false, g_KeyStoreClass, "load", "(Ljava/io/InputStream;[C)V"); + g_KeyStoreSetCertificateEntry = GetMethod(env, false, g_KeyStoreClass, "setCertificateEntry", "(Ljava/lang/String;Ljava/security/cert/Certificate;)V"); + g_KeyStoreSetKeyEntry = GetMethod(env, false, g_KeyStoreClass, "setKeyEntry", "(Ljava/lang/String;Ljava/security/Key;[C[Ljava/security/cert/Certificate;)V"); + + g_PrivateKeyEntryClass = GetClassGRef(env, "java/security/KeyStore$PrivateKeyEntry"); + g_PrivateKeyEntryGetCertificate = GetMethod(env, false, g_PrivateKeyEntryClass, "getCertificate", "()Ljava/security/cert/Certificate;"); + g_PrivateKeyEntryGetPrivateKey = GetMethod(env, false, g_PrivateKeyEntryClass, "getPrivateKey", "()Ljava/security/PrivateKey;"); + + g_TrustedCertificateEntryClass = GetClassGRef(env, "java/security/KeyStore$TrustedCertificateEntry"); + g_TrustedCertificateEntryGetTrustedCertificate = GetMethod(env, false, g_TrustedCertificateEntryClass, "getTrustedCertificate", "()Ljava/security/cert/Certificate;"); + g_SignatureClass = GetClassGRef(env, "java/security/Signature"); g_SignatureGetInstance = GetMethod(env, true, g_SignatureClass, "getInstance", "(Ljava/lang/String;)Ljava/security/Signature;"); g_SignatureInitSign = GetMethod(env, false, g_SignatureClass, "initSign", "(Ljava/security/PrivateKey;)V"); @@ -665,6 +721,10 @@ JNI_OnLoad(JavaVM *vm, void *reserved) g_DateClass = GetClassGRef(env, "java/util/Date"); g_DateGetTime = GetMethod(env, false, g_DateClass, "getTime", "()J"); + g_Enumeration = GetClassGRef(env, "java/util/Enumeration"); + g_EnumerationHasMoreElements = GetMethod(env, false, g_Enumeration, "hasMoreElements", "()Z"); + g_EnumerationNextElement = GetMethod(env, false, g_Enumeration, "nextElement", "()Ljava/lang/Object;"); + g_IteratorClass = GetClassGRef(env, "java/util/Iterator"); g_IteratorHasNext = GetMethod(env, false, g_IteratorClass, "hasNext", "()Z"); g_IteratorNext = GetMethod(env, false, g_IteratorClass, "next", "()Ljava/lang/Object;"); diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_jni.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_jni.h index 0b43632..d662ea5 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_jni.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_jni.h @@ -109,9 +109,16 @@ extern jmethodID g_CertPathGetEncoded; // java/security/cert/X509Certificate extern jclass g_X509CertClass; +extern jmethodID g_X509CertEquals; extern jmethodID g_X509CertGetEncoded; extern jmethodID g_X509CertGetPublicKey; +// java/security/interfaces/DSAKey +extern jclass g_DSAKeyClass; + +// java/security/interfaces/ECKey +extern jclass g_ECKeyClass; + // java/security/interfaces/RSAKey extern jclass g_RSAKeyClass; extern jmethodID g_RSAKeyGetModulus; @@ -133,6 +140,27 @@ extern jmethodID g_keyPairGenInitializeMethod; extern jmethodID g_keyPairGenInitializeWithParamsMethod; extern jmethodID g_keyPairGenGenKeyPairMethod; +// java/security/KeyStore +extern jclass g_KeyStoreClass; +extern jmethodID g_KeyStoreGetInstance; +extern jmethodID g_KeyStoreAliases; +extern jmethodID g_KeyStoreContainsAlias; +extern jmethodID g_KeyStoreDeleteEntry; +extern jmethodID g_KeyStoreGetCertificate; +extern jmethodID g_KeyStoreGetEntry; +extern jmethodID g_KeyStoreLoad; +extern jmethodID g_KeyStoreSetCertificateEntry; +extern jmethodID g_KeyStoreSetKeyEntry; + +// java/security/KeyStore$PrivateKeyEntry +extern jclass g_PrivateKeyEntryClass; +extern jmethodID g_PrivateKeyEntryGetCertificate; +extern jmethodID g_PrivateKeyEntryGetPrivateKey; + +// java/security/KeyStore$TrustedCertificateEntry +extern jclass g_TrustedCertificateEntryClass; +extern jmethodID g_TrustedCertificateEntryGetTrustedCertificate; + // java/security/Signature extern jclass g_SignatureClass; extern jmethodID g_SignatureGetInstance; @@ -263,6 +291,11 @@ extern jmethodID g_CollectionSize; extern jclass g_DateClass; extern jmethodID g_DateGetTime; +// java/util/Enumeration +extern jclass g_Enumeration; +extern jmethodID g_EnumerationHasMoreElements; +extern jmethodID g_EnumerationNextElement; + // java/util/Iterator extern jclass g_IteratorClass; extern jmethodID g_IteratorHasNext; diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_rsa.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_rsa.c index 68091b7..0fb5296 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_rsa.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_rsa.c @@ -162,7 +162,7 @@ PALEXPORT RSA* AndroidCryptoNative_DecodeRsaSubjectPublicKeyInfo(uint8_t* buf, i return FAIL; } - RSA* rsa = AndroidCryptoNative_NewRsaFromPublicKey(env, publicKey); + RSA* rsa = AndroidCryptoNative_NewRsaFromKeys(env, publicKey, NULL /*privateKey*/); (*env)->DeleteLocalRef(env, publicKey); return rsa; @@ -385,15 +385,16 @@ PALEXPORT int32_t AndroidCryptoNative_SetRsaParameters(RSA* rsa, return CheckJNIExceptions(env) ? FAIL : SUCCESS; } -RSA* AndroidCryptoNative_NewRsaFromPublicKey(JNIEnv* env, jobject /*RSAPublicKey*/ key) +RSA* AndroidCryptoNative_NewRsaFromKeys(JNIEnv* env, jobject /*RSAPublicKey*/ publicKey, jobject /*RSAPrivateKey*/ privateKey) { - if (!(*env)->IsInstanceOf(env, key, g_RSAPublicKeyClass)) + if (!(*env)->IsInstanceOf(env, publicKey, g_RSAPublicKeyClass)) return NULL; - jobject modulus = (*env)->CallObjectMethod(env, key, g_RSAKeyGetModulus); + jobject modulus = (*env)->CallObjectMethod(env, publicKey, g_RSAKeyGetModulus); RSA* ret = AndroidCryptoNative_RsaCreate(); - ret->publicKey = AddGRef(env, key); + ret->publicKey = AddGRef(env, publicKey); + ret->privateKey = AddGRef(env, privateKey); ret->keyWidthInBits = AndroidCryptoNative_GetBigNumBytes(modulus) * 8; (*env)->DeleteLocalRef(env, modulus); diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_rsa.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_rsa.h index 2bba552..5cc99ec 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_rsa.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_rsa.h @@ -41,4 +41,5 @@ PALEXPORT int32_t AndroidCryptoNative_SetRsaParameters(RSA* rsa, uint8_t* p, int32_t pLength, uint8_t* dmp1, int32_t dmp1Length, uint8_t* q, int32_t qLength, uint8_t* dmq1, int32_t dmq1Length, uint8_t* iqmp, int32_t iqmpLength); +RSA* AndroidCryptoNative_NewRsaFromKeys(JNIEnv* env, jobject /*RSAPublicKey*/ publicKey, jobject /*RSAPrivateKey*/ privateKey); RSA* AndroidCryptoNative_NewRsaFromPublicKey(JNIEnv* env, jobject /*RSAPublicKey*/ key); diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509.c index b544040..f244e04 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509.c +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509.c @@ -258,13 +258,13 @@ void* AndroidCryptoNative_X509PublicKey(jobject /*X509Certificate*/ cert, PAL_Ke switch (algorithm) { case PAL_EC: - keyHandle = AndroidCryptoNative_NewEcKeyFromPublicKey(env, key); + keyHandle = AndroidCryptoNative_NewEcKeyFromKeys(env, key, NULL /*privateKey*/); break; case PAL_DSA: - keyHandle = AndroidCryptoNative_CreateKeyPair(env, key, NULL); + keyHandle = AndroidCryptoNative_CreateKeyPair(env, key, NULL /*privateKey*/); break; case PAL_RSA: - keyHandle = AndroidCryptoNative_NewRsaFromPublicKey(env, key); + keyHandle = AndroidCryptoNative_NewRsaFromKeys(env, key, NULL /*privateKey*/); break; default: keyHandle = NULL; diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509.h index 476d0de..e00e52f 100644 --- a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509.h +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509.h @@ -56,8 +56,10 @@ enum PAL_DSA = 0, PAL_EC = 1, PAL_RSA = 2, + + PAL_UnknownAlgorithm = -1, }; -typedef uint32_t PAL_KeyAlgorithm; +typedef int32_t PAL_KeyAlgorithm; /* Gets an opaque handle for a certificate's public key diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509store.c b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509store.c new file mode 100644 index 0000000..3f553cb --- /dev/null +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509store.c @@ -0,0 +1,465 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "pal_x509store.h" +#include "pal_eckey.h" +#include "pal_misc.h" +#include "pal_rsa.h" + +#include +#include +#include + +#define INIT_LOCALS(name, ...) \ + enum { __VA_ARGS__, count_##name }; \ + jobject name[count_##name] = { 0 } \ + +#define RELEASE_LOCALS(name, env) \ +do { \ + for (int i_##name = 0; i_##name < count_##name; ++i_##name) \ + { \ + jobject local = name[i_##name]; \ + if (local != NULL) \ + (*env)->DeleteLocalRef(env, local); \ + } \ +} while (0) + +typedef enum +{ + EntryFlags_None = 0, + EntryFlags_HasCertificate = 1, + EntryFlags_HasPrivateKey = 2, + EntryFlags_MatchesCertificate = 4, +} EntryFlags; + +// Returns whether or not the store contains the specified alias +// If the entry exists, the flags parameter is set based on the contents of the entry +static bool ContainsEntryForAlias( + JNIEnv* env, jobject /*KeyStore*/ store, jobject /*X509Certificate*/ cert, jstring alias, EntryFlags* flags) +{ + bool ret = false; + EntryFlags flagsLocal = EntryFlags_None; + + INIT_LOCALS(loc, entry, existingCert); + + bool containsAlias = (*env)->CallBooleanMethod(env, store, g_KeyStoreContainsAlias, alias); + if (!containsAlias) + goto cleanup; + + ret = true; + + // KeyStore.Entry entry = store.getEntry(alias, null); + // if (entry instanceof KeyStore.PrivateKeyEntry) { + // existingCert = ((KeyStore.PrivateKeyEntry)entry).getCertificate(); + // } else if (entry instanceof KeyStore.TrustedCertificateEntry) { + // existingCert = ((KeyStore.TrustedCertificateEntry)entry).getTrustedCertificate(); + // } + loc[entry] = (*env)->CallObjectMethod(env, store, g_KeyStoreGetEntry, alias, NULL); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + if ((*env)->IsInstanceOf(env, loc[entry], g_PrivateKeyEntryClass)) + { + // Private key entries always have a certificate + flagsLocal |= EntryFlags_HasCertificate; + flagsLocal |= EntryFlags_HasPrivateKey; + loc[existingCert] = (*env)->CallObjectMethod(env, loc[entry], g_PrivateKeyEntryGetCertificate); + } + else if ((*env)->IsInstanceOf(env, loc[entry], g_TrustedCertificateEntryClass)) + { + flagsLocal |= EntryFlags_HasCertificate; + loc[existingCert] = (*env)->CallObjectMethod(env, loc[entry], g_TrustedCertificateEntryGetTrustedCertificate); + } + else + { + // Entry for alias exists, but doesn't represent a certificate or private key + certificate + goto cleanup; + } + + assert(loc[existingCert] != NULL); + if ((*env)->CallBooleanMethod(env, cert, g_X509CertEquals, loc[existingCert])) + { + flagsLocal |= EntryFlags_MatchesCertificate; + } + +cleanup: + RELEASE_LOCALS(loc, env); + *flags = flagsLocal; + return ret; +} + +static bool ContainsMatchingCertificateForAlias(JNIEnv* env, + jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + jstring alias) +{ + EntryFlags flags; + if (!ContainsEntryForAlias(env, store, cert, alias, &flags)) + return false; + + EntryFlags matchesFlags = EntryFlags_HasCertificate & EntryFlags_MatchesCertificate; + return (flags & matchesFlags) == matchesFlags; +} + +int32_t AndroidCryptoNative_X509StoreAddCertificate(jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + const char* hashString) +{ + assert(store != NULL); + assert(cert != NULL); + + JNIEnv* env = GetJNIEnv(); + + jstring alias = JSTRING(hashString); + EntryFlags flags; + if (ContainsEntryForAlias(env, store, cert, alias, &flags)) + { + EntryFlags matchesFlags = EntryFlags_HasCertificate & EntryFlags_MatchesCertificate; + if ((flags & matchesFlags) != matchesFlags) + { + LOG_ERROR("Store already contains alias with entry that does not match the expected certificate"); + return FAIL; + } + + // Certificate is already in store - nothing to do + LOG_DEBUG("Store already contains certificate"); + return SUCCESS; + } + + // store.setCertificateEntry(alias, cert); + (*env)->CallVoidMethod(env, store, g_KeyStoreSetCertificateEntry, alias, cert); + (*env)->DeleteLocalRef(env, alias); + + return CheckJNIExceptions(env) ? FAIL : SUCCESS; +} + +int32_t AndroidCryptoNative_X509StoreAddCertificateWithPrivateKey(jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + void* key, + PAL_KeyAlgorithm algorithm, + const char* hashString) +{ + assert(store != NULL); + assert(cert != NULL); + assert(key != NULL); + + int32_t ret = FAIL; + JNIEnv* env = GetJNIEnv(); + + INIT_LOCALS(loc, alias, certs); + jobject privateKey = NULL; + + loc[alias] = JSTRING(hashString); + + EntryFlags flags; + if (ContainsEntryForAlias(env, store, cert, loc[alias], &flags)) + { + EntryFlags matchesFlags = EntryFlags_HasCertificate & EntryFlags_MatchesCertificate; + if ((flags & matchesFlags) != matchesFlags) + { + LOG_ERROR("Store already contains alias with entry that does not match the expected certificate"); + return FAIL; + } + + if ((flags & EntryFlags_HasPrivateKey) == EntryFlags_HasPrivateKey) + { + // Certificate with private key is already in store - nothing to do + LOG_DEBUG("Store already contains certificate with private key"); + return SUCCESS; + } + + // Delete existing entry. We will replace the existing cert with the cert + private key. + // store.deleteEntry(alias); + (*env)->CallVoidMethod(env, store, g_KeyStoreDeleteEntry, alias); + } + + bool releasePrivateKey = true; + switch (algorithm) + { + case PAL_EC: + { + EC_KEY* ec = (EC_KEY*)key; + privateKey = (*env)->CallObjectMethod(env, ec->keyPair, g_keyPairGetPrivateMethod); + break; + } + case PAL_DSA: + { + // key is a KeyPair jobject + privateKey = (*env)->CallObjectMethod(env, key, g_keyPairGetPrivateMethod); + break; + } + case PAL_RSA: + { + RSA* rsa = (RSA*)key; + privateKey = rsa->privateKey; + releasePrivateKey = false; // Private key is a global ref stored directly on RSA handle + break; + } + default: + { + releasePrivateKey = false; + LOG_ERROR("Unknown algorithm for private key"); + goto cleanup; + } + } + + // X509Certificate[] certs = new X509Certificate[] { cert }; + // store.setKeyEntry(alias, privateKey, null, certs); + loc[certs] = (*env)->NewObjectArray(env, 1, g_X509CertClass, cert); + (*env)->CallVoidMethod(env, store, g_KeyStoreSetKeyEntry, loc[alias], privateKey, NULL, loc[certs]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + ret = SUCCESS; + +cleanup: + RELEASE_LOCALS(loc, env); + if (releasePrivateKey) + { + (*env)->DeleteLocalRef(env, privateKey); + } + + return ret; +} + +bool AndroidCryptoNative_X509StoreContainsCertificate(jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + const char* hashString) +{ + assert(store != NULL); + assert(cert != NULL); + + JNIEnv* env = GetJNIEnv(); + jstring alias = JSTRING(hashString); + + bool containsCert = ContainsMatchingCertificateForAlias(env, store, cert, alias); + (*env)->DeleteLocalRef(env, alias); + return containsCert; +} + +static void* HandleFromKeys(JNIEnv* env, + jobject /*PublicKey*/ publicKey, + jobject /*PrivateKey*/ privateKey, + PAL_KeyAlgorithm* algorithm) +{ + if ((*env)->IsInstanceOf(env, privateKey, g_DSAKeyClass)) + { + *algorithm = PAL_DSA; + return AndroidCryptoNative_CreateKeyPair(env, publicKey, privateKey); + } + else if ((*env)->IsInstanceOf(env, privateKey, g_ECKeyClass)) + { + *algorithm = PAL_EC; + return AndroidCryptoNative_NewEcKeyFromKeys(env, publicKey, privateKey); + } + else if ((*env)->IsInstanceOf(env, privateKey, g_RSAKeyClass)) + { + *algorithm = PAL_RSA; + return AndroidCryptoNative_NewRsaFromKeys(env, publicKey, privateKey); + } + + LOG_INFO("Ignoring unknown privake key type"); + *algorithm = PAL_UnknownAlgorithm; + return NULL; +} + +static int32_t +EnumerateCertificates(JNIEnv* env, jobject /*KeyStore*/ store, EnumCertificatesCallback cb, void* context) +{ + int32_t ret = FAIL; + + // Enumeration aliases = store.aliases(); + jobject aliases = (*env)->CallObjectMethod(env, store, g_KeyStoreAliases); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + // while (aliases.hasMoreElements()) { + // String alias = aliases.nextElement(); + // KeyStore.Entry entry = store.getEntry(alias); + // if (entry instanceof KeyStore.PrivateKeyEntry) { + // ... + // } else if (entry instanceof KeyStore.TrustedCertificateEntry) { + // ... + // } + // } + jboolean hasNext = (*env)->CallBooleanMethod(env, aliases, g_EnumerationHasMoreElements); + while (hasNext) + { + INIT_LOCALS(loc, alias, entry, cert, publicKey, privateKey); + + loc[alias] = (*env)->CallObjectMethod(env, aliases, g_EnumerationNextElement); + ON_EXCEPTION_PRINT_AND_GOTO(loop_cleanup); + + loc[entry] = (*env)->CallObjectMethod(env, store, g_KeyStoreGetEntry, loc[alias], NULL); + ON_EXCEPTION_PRINT_AND_GOTO(loop_cleanup); + + if ((*env)->IsInstanceOf(env, loc[entry], g_PrivateKeyEntryClass)) + { + // Certificate cert = entry.getCertificate(); + // Public publicKey = cert.getPublicKey(); + // PrivateKey privateKey = entry.getPrivateKey(); + loc[cert] = (*env)->CallObjectMethod(env, loc[entry], g_PrivateKeyEntryGetCertificate); + loc[publicKey] = (*env)->CallObjectMethod(env, loc[cert], g_X509CertGetPublicKey); + loc[privateKey] = (*env)->CallObjectMethod(env, loc[entry], g_PrivateKeyEntryGetPrivateKey); + + PAL_KeyAlgorithm keyAlgorithm = PAL_UnknownAlgorithm; + void* keyHandle = HandleFromKeys(env, loc[publicKey], loc[privateKey], &keyAlgorithm); + + // Private key entries always have a certificate. + // For key algorithms we recognize, the certificate and private key handle are given to the callback. + // For key algorithms we do not recognize, only the certificate will be given to the callback. + cb(AddGRef(env, loc[cert]), keyHandle, keyAlgorithm, context); + } + else if ((*env)->IsInstanceOf(env, loc[entry], g_TrustedCertificateEntryClass)) + { + // Certificate cert = entry.getTrustedCertificate(); + loc[cert] = (*env)->CallObjectMethod(env, loc[entry], g_TrustedCertificateEntryGetTrustedCertificate); + cb(AddGRef(env, loc[cert]), NULL /*privateKey*/, PAL_UnknownAlgorithm, context); + } + + loop_cleanup: + RELEASE_LOCALS(loc, env); + + hasNext = (*env)->CallBooleanMethod(env, aliases, g_EnumerationHasMoreElements); + } + + ret = SUCCESS; + +cleanup: + (*env)->DeleteLocalRef(env, aliases); + return ret; +} + +int32_t AndroidCryptoNative_X509StoreEnumerateCertificates(jobject /*KeyStore*/ store, + EnumCertificatesCallback cb, + void* context) +{ + assert(store != NULL); + assert(cb != NULL); + + JNIEnv* env = GetJNIEnv(); + return EnumerateCertificates(env, store, cb, context); +} + +static bool SystemAliasFilter(JNIEnv* env, jstring alias) +{ + const char systemPrefix[] = "system:"; + size_t prefixLen = (sizeof(systemPrefix) - 1) / sizeof(char); + + const char* aliasPtr = (*env)->GetStringUTFChars(env, alias, NULL); + bool isSystem = (strncmp(aliasPtr, systemPrefix, prefixLen) == 0); + (*env)->ReleaseStringUTFChars(env, alias, aliasPtr); + return isSystem; +} + +typedef bool (*FilterAliasFunction)(JNIEnv* env, jstring alias); +static int32_t EnumerateTrustedCertificates( + JNIEnv* env, jobject /*KeyStore*/ store, bool systemOnly, EnumTrustedCertificatesCallback cb, void* context) +{ + int32_t ret = FAIL; + + // Filter to only system certificates if necessary + // System certificates are included for 'current user' (matches Windows) + FilterAliasFunction filter = systemOnly ? &SystemAliasFilter : NULL; + + // Enumeration aliases = store.aliases(); + jobject aliases = (*env)->CallObjectMethod(env, store, g_KeyStoreAliases); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + // while (aliases.hasMoreElements()) { + // String alias = aliases.nextElement(); + // X509Certificate cert = (X509Certificate)store.getCertificate(alias); + // if (cert != null) { + // cb(cert, context); + // } + // } + jboolean hasNext = (*env)->CallBooleanMethod(env, aliases, g_EnumerationHasMoreElements); + while (hasNext) + { + jstring alias = (*env)->CallObjectMethod(env, aliases, g_EnumerationNextElement); + ON_EXCEPTION_PRINT_AND_GOTO(loop_cleanup); + + if (filter == NULL || filter(env, alias)) + { + jobject cert = (*env)->CallObjectMethod(env, store, g_KeyStoreGetCertificate, alias); + if (cert != NULL && !CheckJNIExceptions(env)) + { + cert = ToGRef(env, cert); + cb(cert, context); + } + } + + hasNext = (*env)->CallBooleanMethod(env, aliases, g_EnumerationHasMoreElements); + + loop_cleanup: + (*env)->DeleteLocalRef(env, alias); + } + + ret = SUCCESS; + +cleanup: + (*env)->DeleteLocalRef(env, aliases); + return ret; +} + +int32_t AndroidCryptoNative_X509StoreEnumerateTrustedCertificates(bool systemOnly, + EnumTrustedCertificatesCallback cb, + void* context) +{ + assert(cb != NULL); + JNIEnv* env = GetJNIEnv(); + + int32_t ret = FAIL; + INIT_LOCALS(loc, storeType, store); + + // KeyStore store = KeyStore.getInstance("AndroidCAStore"); + // store.load(null, null); + loc[storeType] = JSTRING("AndroidCAStore"); + loc[store] = (*env)->CallStaticObjectMethod(env, g_KeyStoreClass, g_KeyStoreGetInstance, loc[storeType]); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + (*env)->CallVoidMethod(env, loc[store], g_KeyStoreLoad, NULL, NULL); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + + ret = EnumerateTrustedCertificates(env, loc[store], systemOnly, cb, context); + +cleanup: + RELEASE_LOCALS(loc, env); + return ret; +} + +jobject /*KeyStore*/ AndroidCryptoNative_X509StoreOpenDefault(void) +{ + JNIEnv* env = GetJNIEnv(); + jobject ret = NULL; + + // KeyStore store = KeyStore.getInstance("AndroidKeyStore"); + // store.load(null, null); + jstring storeType = JSTRING("AndroidKeyStore"); + jobject store = (*env)->CallStaticObjectMethod(env, g_KeyStoreClass, g_KeyStoreGetInstance, storeType); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + (*env)->CallVoidMethod(env, store, g_KeyStoreLoad, NULL, NULL); + ON_EXCEPTION_PRINT_AND_GOTO(cleanup); + ret = ToGRef(env, store); + +cleanup: + (*env)->DeleteLocalRef(env, storeType); + return ret; +} + +int32_t AndroidCryptoNative_X509StoreRemoveCertificate(jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + const char* hashString) +{ + assert(store != NULL); + + JNIEnv* env = GetJNIEnv(); + + jstring alias = JSTRING(hashString); + if (!ContainsMatchingCertificateForAlias(env, store, cert, alias)) + { + // Certificate is not in store - nothing to do + return SUCCESS; + } + + // store.deleteEntry(alias); + (*env)->CallVoidMethod(env, store, g_KeyStoreDeleteEntry, alias); + + (*env)->DeleteLocalRef(env, alias); + return CheckJNIExceptions(env) ? FAIL : SUCCESS; +} diff --git a/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509store.h b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509store.h new file mode 100644 index 0000000..1702f19 --- /dev/null +++ b/src/libraries/Native/Unix/System.Security.Cryptography.Native.Android/pal_x509store.h @@ -0,0 +1,72 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#include "pal_jni.h" +#include "pal_x509.h" + +/* +Add a certificate to the specified key store + +If the certificate is already in the store, this function simply returns success. +Returns 1 on success, 0 otherwise. +*/ +PALEXPORT int32_t AndroidCryptoNative_X509StoreAddCertificate(jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + const char* hashString); + +/* +Add a certificate with a private key to the specified key store + +If the certificate is already in the store, it is replaced. This means that a certificate +without a private key will be replaced with one with a private key. +Returns 1 on success, 0 otherwise. +*/ +PALEXPORT int32_t AndroidCryptoNative_X509StoreAddCertificateWithPrivateKey(jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + void* key, + PAL_KeyAlgorithm algorithm, + const char* hashString); + +/* +Get whether or not a certificate is contained in the specified key store +*/ +PALEXPORT bool AndroidCryptoNative_X509StoreContainsCertificate(jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + const char* hashString); + +/* +Enumerate certificates for the specified key store +The certificate and key passed to the callback will already be a global jobject reference. +*/ +typedef void (*EnumCertificatesCallback)(jobject /*X509Certificate*/ cert, + void* privateKey, + PAL_KeyAlgorithm algorithm, + void* context); +PALEXPORT int32_t AndroidCryptoNative_X509StoreEnumerateCertificates(jobject /*KeyStore*/ store, + EnumCertificatesCallback cb, + void* context); + +/* +Enumerate trusted certificates +The certificate passed to the callback will already be a global jobject reference. + +Returns 1 on success, 0 otherwise. +*/ +typedef void (*EnumTrustedCertificatesCallback)(jobject /*X509Certificate*/ cert, void* context); +PALEXPORT int32_t AndroidCryptoNative_X509StoreEnumerateTrustedCertificates(bool isSystem, + EnumTrustedCertificatesCallback cb, + void* context); +/* +Open the default key store +*/ +PALEXPORT jobject /*KeyStore*/ AndroidCryptoNative_X509StoreOpenDefault(void); + +/* +Remove a certificate from the specified key store + +If the certificate is not in the store, this function returns success. +Returns 1 on success, 0 otherwise. +*/ +PALEXPORT int32_t AndroidCryptoNative_X509StoreRemoveCertificate(jobject /*KeyStore*/ store, + jobject /*X509Certificate*/ cert, + const char* hashString); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/AndroidCertificatePal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/AndroidCertificatePal.cs index 740b878..658c2da 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/AndroidCertificatePal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/AndroidCertificatePal.cs @@ -121,12 +121,12 @@ namespace Internal.Cryptography.Pal } } - private AndroidCertificatePal(SafeX509Handle handle) + internal AndroidCertificatePal(SafeX509Handle handle) { _cert = handle; } - private AndroidCertificatePal(SafeX509Handle handle, SafeKeyHandle privateKey) + internal AndroidCertificatePal(SafeX509Handle handle, SafeKeyHandle privateKey) { _cert = handle; _privateKey = privateKey; diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.AndroidKeyStore.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.AndroidKeyStore.cs new file mode 100644 index 0000000..898d79c2 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.AndroidKeyStore.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Win32.SafeHandles; + +namespace Internal.Cryptography.Pal +{ + internal sealed partial class StorePal + { + private sealed class AndroidKeyStore : IStorePal + { + private readonly bool _readOnly; + private readonly SafeX509StoreHandle _keyStoreHandle; + + public static AndroidKeyStore OpenDefault(OpenFlags openFlags) + { + SafeX509StoreHandle store = Interop.AndroidCrypto.X509StoreOpenDefault(); + if (store.IsInvalid) + { + store.Dispose(); + throw new CryptographicException(); + } + + return new AndroidKeyStore(store, openFlags); + } + + private AndroidKeyStore(SafeX509StoreHandle keyStoreHandle, OpenFlags openFlags) + { + _keyStoreHandle = keyStoreHandle; + _readOnly = (openFlags & (OpenFlags.ReadWrite | OpenFlags.MaxAllowed)) == 0; + } + + public SafeHandle SafeHandle => _keyStoreHandle; + + public void Dispose() + { + _keyStoreHandle.Dispose(); + } + + public void Add(ICertificatePal cert) + { + if (_readOnly) + throw new CryptographicException(SR.Cryptography_X509_StoreReadOnly); + + AndroidCertificatePal certPal = (AndroidCertificatePal)cert; + string hashString = GetCertificateHashString(cert); + + bool success; + if (certPal.HasPrivateKey) + { + Interop.AndroidCrypto.PAL_KeyAlgorithm algorithm = certPal.PrivateKeyHandle switch + { + // The AndroidKeyStore doesn't support adding DSA private key entries in newer versions (API 23+) + // Our minimum supported version (API 21) does support it, but for simplicity, we simply block adding + // certificates with DSA private keys on all versions instead of trying to support it on two versions. + SafeDsaHandle _ => throw new PlatformNotSupportedException(SR.Cryptography_X509_StoreDSAPrivateKeyNotSupported), + SafeEcKeyHandle _ => Interop.AndroidCrypto.PAL_KeyAlgorithm.EC, + SafeRsaHandle _ => Interop.AndroidCrypto.PAL_KeyAlgorithm.RSA, + _ => throw new NotSupportedException(SR.NotSupported_KeyAlgorithm) + }; + + success = Interop.AndroidCrypto.X509StoreAddCertificateWithPrivateKey(_keyStoreHandle, certPal.SafeHandle, certPal.PrivateKeyHandle, algorithm, hashString); + } + else + { + success = Interop.AndroidCrypto.X509StoreAddCertificate(_keyStoreHandle, certPal.SafeHandle, hashString); + } + + if (!success) + throw new CryptographicException(SR.Cryptography_X509_StoreAddFailure); + } + + public void Remove(ICertificatePal cert) + { + string hashString = GetCertificateHashString(cert); + AndroidCertificatePal certPal = (AndroidCertificatePal)cert; + if (_readOnly) + { + bool containsCert = Interop.AndroidCrypto.X509StoreContainsCertificate(_keyStoreHandle, certPal.SafeHandle, hashString); + if (containsCert) + throw new CryptographicException(SR.Cryptography_X509_StoreReadOnly); + + // Removing a non-existent certificate is not an error + return; + } + + bool success = Interop.AndroidCrypto.X509StoreRemoveCertificate(_keyStoreHandle, certPal.SafeHandle, hashString); + if (!success) + throw new CryptographicException(SR.Cryptography_X509_StoreRemoveFailure); + } + + public void CloneTo(X509Certificate2Collection collection) + { + EnumCertificatesContext context = default; + context.Results = new HashSet(); + unsafe + { + bool success = Interop.AndroidCrypto.X509StoreEnumerateCertificates( + _keyStoreHandle, + &EnumCertificatesCallback, + Unsafe.AsPointer(ref context)); + if (!success) + { + throw new CryptographicException(SR.Cryptography_X509_StoreEnumerateFailure); + } + } + + foreach (X509Certificate2 cert in context.Results) + { + collection.Add(cert); + } + } + + private static string GetCertificateHashString(ICertificatePal certPal) + { + return X509Certificate.GetCertHashString(HashAlgorithmName.SHA256, certPal); + } + + private struct EnumCertificatesContext + { + public HashSet Results; + } + + [UnmanagedCallersOnly] + private static unsafe void EnumCertificatesCallback(void* certPtr, void* privateKeyPtr, Interop.AndroidCrypto.PAL_KeyAlgorithm privateKeyAlgorithm, void* context) + { + ref EnumCertificatesContext callbackContext = ref Unsafe.As(ref *(byte*)context); + + AndroidCertificatePal certPal; + var handle = new SafeX509Handle((IntPtr)certPtr); + if (privateKeyPtr != null) + { + SafeKeyHandle privateKey = privateKeyAlgorithm switch + { + Interop.AndroidCrypto.PAL_KeyAlgorithm.DSA => new SafeDsaHandle((IntPtr)privateKeyPtr), + Interop.AndroidCrypto.PAL_KeyAlgorithm.EC => new SafeEcKeyHandle((IntPtr)privateKeyPtr), + Interop.AndroidCrypto.PAL_KeyAlgorithm.RSA => new SafeRsaHandle((IntPtr)privateKeyPtr), + _ => throw new NotSupportedException(SR.NotSupported_KeyAlgorithm) + }; + certPal = new AndroidCertificatePal(handle, privateKey); + } + else + { + certPal = new AndroidCertificatePal(handle); + } + + var cert = new X509Certificate2(certPal); + if (!callbackContext.Results.Add(cert)) + cert.Dispose(); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.TrustedStore.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.TrustedStore.cs new file mode 100644 index 0000000..6998610 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.TrustedStore.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using Microsoft.Win32.SafeHandles; + +namespace Internal.Cryptography.Pal +{ + internal sealed partial class StorePal + { + private sealed class TrustedStore : IStorePal + { + private readonly StoreLocation _location; + + internal TrustedStore(StoreLocation location) + { + _location = location; + } + + public SafeHandle? SafeHandle => null; + + public void Dispose() + { + } + + public void Add(ICertificatePal cert) + { + throw new CryptographicException(SR.Cryptography_X509_StoreReadOnly); + } + + public void Remove(ICertificatePal cert) + { + throw new CryptographicException(SR.Cryptography_X509_StoreReadOnly); + } + + public void CloneTo(X509Certificate2Collection collection) + { + EnumCertificatesContext context = default; + context.Results = new HashSet(); + + bool systemOnly = _location == StoreLocation.LocalMachine; + unsafe + { + bool success = Interop.AndroidCrypto.X509StoreEnumerateTrustedCertificates( + (byte)(systemOnly ? 1 : 0), + &EnumCertificatesCallback, + Unsafe.AsPointer(ref context)); + if (!success) + { + throw new CryptographicException(SR.Cryptography_X509_StoreEnumerateFailure); + } + } + + foreach (X509Certificate2 cert in context.Results) + { + collection.Add(cert); + } + } + + private struct EnumCertificatesContext + { + public HashSet Results; + } + + [UnmanagedCallersOnly] + private static unsafe void EnumCertificatesCallback(void* certPtr, void* context) + { + ref EnumCertificatesContext callbackContext = ref Unsafe.As(ref *(byte*)context); + var handle = new SafeX509Handle((IntPtr)certPtr); + var cert = new X509Certificate2(new AndroidCertificatePal(handle)); + if (!callbackContext.Results.Add(cert)) + cert.Dispose(); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.cs index 3fe9028..09ec3ce 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Android/StorePal.cs @@ -53,7 +53,59 @@ namespace Internal.Cryptography.Pal public static IStorePal FromSystemStore(string storeName, StoreLocation storeLocation, OpenFlags openFlags) { - throw new NotImplementedException(nameof(FromSystemStore)); + bool isReadWrite = (openFlags & OpenFlags.ReadWrite) == OpenFlags.ReadWrite; + if (isReadWrite && storeLocation == StoreLocation.LocalMachine) + { + // All LocalMachine stores are read-only from an Android application's perspective + throw new CryptographicException( + SR.Cryptography_Unix_X509_MachineStoresReadOnly, + new PlatformNotSupportedException(SR.Cryptography_Unix_X509_MachineStoresReadOnly)); + } + + StringComparer ordinalIgnoreCase = StringComparer.OrdinalIgnoreCase; + switch (storeLocation) + { + case StoreLocation.CurrentUser: + { + // Matches Unix behaviour of getting a disallowed store that is always empty. + if (ordinalIgnoreCase.Equals(X509Store.DisallowedStoreName, storeName)) + { + return new UnsupportedDisallowedStore(openFlags); + } + + if (ordinalIgnoreCase.Equals(X509Store.MyStoreName, storeName)) + { + return AndroidKeyStore.OpenDefault(openFlags); + } + + if (ordinalIgnoreCase.Equals(X509Store.RootStoreName, storeName)) + { + // Android only allows updating the trusted store through the built-in settings application + if (isReadWrite) + { + throw new CryptographicException(SR.Security_AccessDenied); + } + + return new TrustedStore(storeLocation); + } + break; + } + case StoreLocation.LocalMachine: + { + if (ordinalIgnoreCase.Equals(X509Store.RootStoreName, storeName)) + { + return new TrustedStore(storeLocation); + } + + break; + } + } + + if ((openFlags & OpenFlags.OpenExistingOnly) == OpenFlags.OpenExistingOnly) + throw new CryptographicException(SR.Cryptography_X509_StoreNotFound); + + string message = SR.Format(SR.Cryptography_X509_StoreCannotCreate, storeName, storeLocation); + throw new CryptographicException(message, new PlatformNotSupportedException(message)); } private static ICertificatePal[] ReadPkcs12Collection(ReadOnlySpan rawData, SafePasswordHandle password) diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/DirectoryBasedStoreProvider.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/DirectoryBasedStoreProvider.cs index 0cf1cca..0928ced 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/DirectoryBasedStoreProvider.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/DirectoryBasedStoreProvider.cs @@ -423,70 +423,27 @@ namespace Internal.Cryptography.Pal } } - internal sealed class UnsupportedDisallowedStore : IStorePal + internal static IStorePal OpenDisallowedStore(OpenFlags openFlags) { - private readonly bool _readOnly; - - internal UnsupportedDisallowedStore(OpenFlags openFlags) + string storePath = GetStorePath(X509Store.DisallowedStoreName); + try { - // ReadOnly is 0x00, so it is implicit unless either ReadWrite or MaxAllowed - // was requested. - OpenFlags writeFlags = openFlags & (OpenFlags.ReadWrite | OpenFlags.MaxAllowed); - - if (writeFlags == OpenFlags.ReadOnly) + if (Directory.Exists(storePath)) { - _readOnly = true; - } - - string storePath = GetStorePath(X509Store.DisallowedStoreName); - - try - { - if (Directory.Exists(storePath)) + // If it has no files, leave it alone. + foreach (string filePath in Directory.EnumerateFiles(storePath)) { - // If it has no files, leave it alone. - foreach (string filePath in Directory.EnumerateFiles(storePath)) - { - string msg = SR.Format(SR.Cryptography_Unix_X509_DisallowedStoreNotEmpty, storePath); - throw new CryptographicException(msg, new PlatformNotSupportedException(msg)); - } + string msg = SR.Format(SR.Cryptography_Unix_X509_DisallowedStoreNotEmpty, storePath); + throw new CryptographicException(msg, new PlatformNotSupportedException(msg)); } } - catch (IOException) - { - // Suppress the exception, treat the store as empty. - } - } - - public void Dispose() - { - // Nothing to do. - } - - public void CloneTo(X509Certificate2Collection collection) - { - // Never show any data. - } - - public void Add(ICertificatePal cert) - { - if (_readOnly) - { - throw new CryptographicException(SR.Cryptography_X509_StoreReadOnly); - } - - throw new CryptographicException( - SR.Cryptography_Unix_X509_NoDisallowedStore, - new PlatformNotSupportedException(SR.Cryptography_Unix_X509_NoDisallowedStore)); } - - public void Remove(ICertificatePal cert) + catch (IOException) { - // Remove never throws if it does no measurable work. - // Since CloneTo always says the store is empty, no measurable work is ever done. + // Suppress the exception, treat the store as empty. } - SafeHandle? IStorePal.SafeHandle { get; } + return new UnsupportedDisallowedStore(openFlags); } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs index d0f583d..749beed 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/StorePal.cs @@ -139,7 +139,7 @@ namespace Internal.Cryptography.Pal { if (X509Store.DisallowedStoreName.Equals(storeName, StringComparison.OrdinalIgnoreCase)) { - return new DirectoryBasedStoreProvider.UnsupportedDisallowedStore(openFlags); + return DirectoryBasedStoreProvider.OpenDisallowedStore(openFlags); } return new DirectoryBasedStoreProvider(storeName, openFlags); diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnsupportedDisallowedStore.cs b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnsupportedDisallowedStore.cs new file mode 100644 index 0000000..fbdcd08 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Internal/Cryptography/Pal.Unix/UnsupportedDisallowedStore.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text; + +namespace Internal.Cryptography.Pal +{ + internal sealed class UnsupportedDisallowedStore : IStorePal + { + private readonly bool _readOnly; + + internal UnsupportedDisallowedStore(OpenFlags openFlags) + { + // ReadOnly is 0x00, so it is implicit unless either ReadWrite or MaxAllowed + // was requested. + OpenFlags writeFlags = openFlags & (OpenFlags.ReadWrite | OpenFlags.MaxAllowed); + + if (writeFlags == OpenFlags.ReadOnly) + { + _readOnly = true; + } + } + + public void Dispose() + { + // Nothing to do. + } + + public void CloneTo(X509Certificate2Collection collection) + { + // Never show any data. + } + + public void Add(ICertificatePal cert) + { + if (_readOnly) + { + throw new CryptographicException(SR.Cryptography_X509_StoreReadOnly); + } + + throw new CryptographicException( + SR.Cryptography_Unix_X509_NoDisallowedStore, + new PlatformNotSupportedException(SR.Cryptography_Unix_X509_NoDisallowedStore)); + } + + public void Remove(ICertificatePal cert) + { + // Remove never throws if it does no measurable work. + // Since CloneTo always says the store is empty, no measurable work is ever done. + } + + SafeHandle? IStorePal.SafeHandle { get; } + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx index 6d619e4..cd76a2a 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/Resources/Strings.resx @@ -292,9 +292,18 @@ The X509 certificate store is read-only. + + The X509 certificate could not be removed from the store. + 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. + + Adding a DSA private key to the store is not supported on this platform. + + + Failed to enumerate certificates from the store. + The certificate contents do not contain a PEM with a CERTIFICATE label, or the content is malformed. diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj index 20dbf62..88e77c6 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj +++ b/src/libraries/System.Security.Cryptography.X509Certificates/src/System.Security.Cryptography.X509Certificates.csproj @@ -317,6 +317,7 @@ + + + + + $(DefineConstants);HAVE_THUMBPRINT_OVERLOADS $(DefineConstants);Unix true - $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Browser;$(NetCoreAppCurrent)-OSX;$(NetCoreAppCurrent)-iOS;$(NetCoreAppCurrent)-tvOS + $(NetCoreAppCurrent)-windows;$(NetCoreAppCurrent)-Unix;$(NetCoreAppCurrent)-Android;$(NetCoreAppCurrent)-Browser;$(NetCoreAppCurrent)-OSX;$(NetCoreAppCurrent)-iOS;$(NetCoreAppCurrent)-tvOS + + + + true + true @@ -70,7 +76,7 @@ - + @@ -92,7 +98,13 @@ Link="Common\System\IO\PersistedFiles.Names.Unix.cs" /> - + + + + + + + diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.Android.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.Android.cs new file mode 100644 index 0000000..76e6082 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.Android.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + public static partial class X509StoreMutableTests + { + public static bool PermissionsAllowStoreWrite { get; } = true; + + [Theory] + [InlineData(nameof(TestData.RsaCertificate), TestData.RsaCertificate, TestData.RsaPkcs8Key)] + [InlineData(nameof(TestData.EcDhCertificate), TestData.EcDhCertificate, TestData.EcDhPkcs8Key)] + [InlineData(nameof(TestData.ECDsaCertificate), TestData.ECDsaCertificate, TestData.ECDsaPkcs8Key)] + public static void AddRemove_CertWithPrivateKey(string testCase, string certPem, string keyPem) + { + _ = testCase; + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + using (var cert = X509Certificate2.CreateFromPem(certPem, keyPem)) + { + store.Open(OpenFlags.ReadWrite); + + // Make sure cert is not already in the store + store.Remove(cert); + Assert.False(IsCertInStore(cert, store), "Certificate should not be found on pre-condition"); + + // Add + store.Add(cert); + Assert.True(IsCertInStore(cert, store), "Certificate should be found after add"); + Assert.True(StoreHasPrivateKey(store, cert), "Certificate in store should have a private key"); + + // Remove + store.Remove(cert); + Assert.False(IsCertInStore(cert, store), "Certificate should not be found after remove"); + } + } + + [Fact] + public static void Add_CertWithPrivateKey_NotSupportedAlgorithm() + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + using (var cert = X509Certificate2.CreateFromPem(TestData.DsaCertificate, TestData.DsaPkcs8Key)) + { + store.Open(OpenFlags.ReadWrite); + + // Make sure cert is not already in the store + store.Remove(cert); + Assert.False(IsCertInStore(cert, store), "Certificate should not be found on pre-condition"); + + // Add - throws PNSE + Assert.Throws(() => store.Add(cert)); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.OSX.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.OSX.cs index 422c71a..7678da5 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.OSX.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.OSX.cs @@ -5,9 +5,8 @@ using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests { - [OuterLoop("Modifies system state")] [PlatformSpecific(TestPlatforms.OSX)] - public static class X509StoreMutableTests_OSX + public static partial class X509StoreMutableTests { public static bool PermissionsAllowStoreWrite { get; } = TestPermissions(); @@ -117,133 +116,6 @@ namespace System.Security.Cryptography.X509Certificates.Tests } [ConditionalFact(nameof(PermissionsAllowStoreWrite))] - public static void AddToStoreTwice() - { - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) - using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) - using (var certOnly = new X509Certificate2(cert.RawData)) - { - store.Open(OpenFlags.ReadWrite); - - // Defensive removal. - store.Remove(certOnly); - Assert.False(IsCertInStore(cert, store), "PtxData certificate was found on pre-condition"); - - store.Add(cert); - Assert.True(IsCertInStore(certOnly, store), "PtxData certificate was found after add"); - - // No exception for duplicate item. - store.Add(cert); - - // Cleanup - store.Remove(certOnly); - } - } - - [ConditionalFact(nameof(PermissionsAllowStoreWrite))] - public static void AddPrivateAfterPublic() - { - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) - using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) - using (var certOnly = new X509Certificate2(cert.RawData)) - { - store.Open(OpenFlags.ReadWrite); - - // Defensive removal. - store.Remove(certOnly); - Assert.False(IsCertInStore(cert, store), "PtxData certificate was found on pre-condition"); - - store.Add(certOnly); - Assert.True(IsCertInStore(certOnly, store), "PtxData certificate was found after add"); - Assert.False(StoreHasPrivateKey(store, certOnly), "Store has a private key for PfxData after public-only add"); - - // Add the private key - store.Add(cert); - Assert.True(StoreHasPrivateKey(store, certOnly), "Store has a private key for PfxData after PFX add"); - - // Cleanup - store.Remove(certOnly); - } - } - - [ConditionalFact(nameof(PermissionsAllowStoreWrite))] - public static void AddPublicAfterPrivate() - { - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) - using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) - using (var certOnly = new X509Certificate2(cert.RawData)) - { - store.Open(OpenFlags.ReadWrite); - - // Defensive removal. - store.Remove(certOnly); - Assert.False(IsCertInStore(cert, store), "PtxData certificate was found on pre-condition"); - - // Add the private key - store.Add(cert); - Assert.True(IsCertInStore(certOnly, store), "PtxData certificate was found after add"); - Assert.True(StoreHasPrivateKey(store, certOnly), "Store has a private key for PfxData after PFX add"); - - // Add the public key with no private key - store.Add(certOnly); - Assert.True(StoreHasPrivateKey(store, certOnly), "Store has a private key for PfxData after public-only add"); - - // Cleanup - store.Remove(certOnly); - } - } - - [ConditionalFact(nameof(PermissionsAllowStoreWrite))] - public static void VerifyRemove() - { - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) - using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) - { - store.Open(OpenFlags.ReadWrite); - - // Defensive removal. Sort of circular, but it's the best we can do. - store.Remove(cert); - Assert.False(IsCertInStore(cert, store), "PtxData certificate was found on pre-condition"); - - store.Add(cert); - Assert.True(IsCertInStore(cert, store), "PtxData certificate was found after add"); - - store.Remove(cert); - Assert.False(IsCertInStore(cert, store), "PtxData certificate was found after remove"); - } - } - - [ConditionalFact(nameof(PermissionsAllowStoreWrite))] - public static void RemovePublicDeletesPrivateKey() - { - using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) - using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) - using (var certOnly = new X509Certificate2(cert.RawData)) - { - store.Open(OpenFlags.ReadWrite); - - // Defensive removal. - store.Remove(cert); - Assert.False(IsCertInStore(cert, store), "PtxData certificate was found on pre-condition"); - - // Add the private key - store.Add(cert); - Assert.True(IsCertInStore(cert, store), "PtxData certificate was found after add"); - - store.Remove(certOnly); - Assert.False(IsCertInStore(cert, store), "PtxData certificate was found after remove"); - - // Add back the public key only - store.Add(certOnly); - Assert.True(IsCertInStore(cert, store), "PtxData certificate was found after public-only add"); - Assert.False(StoreHasPrivateKey(store, cert), "Store has a private key for cert after public-only add"); - - // Cleanup - store.Remove(certOnly); - } - } - - [ConditionalFact(nameof(PermissionsAllowStoreWrite))] public static void CustomStore_ReadWrite() { using (var store = new X509Store("CustomKeyChain_CoreFX", StoreLocation.CurrentUser)) @@ -320,47 +192,6 @@ namespace System.Security.Cryptography.X509Certificates.Tests Assert.ThrowsAny(() => store.Open(OpenFlags.ReadWrite)); } - private static bool StoreHasPrivateKey(X509Store store, X509Certificate2 forCert) - { - using (ImportedCollection coll = new ImportedCollection(store.Certificates)) - { - foreach (X509Certificate2 storeCert in coll.Collection) - { - if (forCert.Equals(storeCert)) - { - return storeCert.HasPrivateKey; - } - } - } - - Assert.True(false, $"Certificate ({forCert.Subject}) exists in the store"); - return false; - } - - private static bool IsCertInStore(X509Certificate2 cert, X509Store store) - { - using (ImportedCollection coll = new ImportedCollection(store.Certificates)) - { - foreach (X509Certificate2 storeCert in coll.Collection) - { - if (cert.Equals(storeCert)) - { - return true; - } - } - } - - return false; - } - - private static int GetStoreCertificateCount(X509Store store) - { - using (var coll = new ImportedCollection(store.Certificates)) - { - return coll.Collection.Count; - } - } - private class TemporaryX509Store : IDisposable { private X509Store _store; diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.cs new file mode 100644 index 0000000..e49b1c8 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreMutableTests.cs @@ -0,0 +1,183 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Security.Cryptography.X509Certificates.Tests +{ + [OuterLoop("Modifies system state")] + public static partial class X509StoreMutableTests + { + [ConditionalFact(nameof(PermissionsAllowStoreWrite))] + public static void AddToStoreTwice() + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) + using (var certOnly = new X509Certificate2(cert.RawData)) + { + store.Open(OpenFlags.ReadWrite); + + // Defensive removal. + store.Remove(certOnly); + Assert.False(IsCertInStore(cert, store), "PfxData certificate was found on pre-condition"); + + store.Add(cert); + Assert.True(IsCertInStore(certOnly, store), "PfxData certificate was found after add"); + + // No exception for duplicate item. + store.Add(cert); + + // Cleanup + store.Remove(certOnly); + } + } + + [ConditionalFact(nameof(PermissionsAllowStoreWrite))] + public static void AddPrivateAfterPublic() + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) + using (var certOnly = new X509Certificate2(cert.RawData)) + { + store.Open(OpenFlags.ReadWrite); + + // Defensive removal. + store.Remove(certOnly); + Assert.False(IsCertInStore(cert, store), "PfxData certificate was found on pre-condition"); + + store.Add(certOnly); + Assert.True(IsCertInStore(certOnly, store), "PfxData certificate was found after add"); + Assert.False(StoreHasPrivateKey(store, certOnly), "Store has a private key for PfxData after public-only add"); + + // Add the private key + store.Add(cert); + Assert.True(StoreHasPrivateKey(store, certOnly), "Store has a private key for PfxData after PFX add"); + + // Cleanup + store.Remove(certOnly); + } + } + + [ConditionalFact(nameof(PermissionsAllowStoreWrite))] + public static void AddPublicAfterPrivate() + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) + using (var certOnly = new X509Certificate2(cert.RawData)) + { + store.Open(OpenFlags.ReadWrite); + + // Defensive removal. + store.Remove(certOnly); + Assert.False(IsCertInStore(cert, store), "PfxData certificate was found on pre-condition"); + + // Add the private key + store.Add(cert); + Assert.True(IsCertInStore(certOnly, store), "PfxData certificate was found after add"); + Assert.True(StoreHasPrivateKey(store, certOnly), "Store has a private key for PfxData after PFX add"); + + // Add the public key with no private key + store.Add(certOnly); + Assert.True(StoreHasPrivateKey(store, certOnly), "Store has a private key for PfxData after public-only add"); + + // Cleanup + store.Remove(certOnly); + } + } + + [ConditionalTheory(nameof(PermissionsAllowStoreWrite))] + [InlineData(true)] + [InlineData(false)] + public static void VerifyRemove(bool withPrivateKey) + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + using (var certWithPrivateKey = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) + using (var certOnly = new X509Certificate2(certWithPrivateKey.RawData)) + { + X509Certificate2 cert = withPrivateKey ? certWithPrivateKey : certOnly; + store.Open(OpenFlags.ReadWrite); + + // Defensive removal. Sort of circular, but it's the best we can do. + store.Remove(cert); + Assert.False(IsCertInStore(cert, store), "PfxData certificate was found on pre-condition"); + + store.Add(cert); + Assert.True(IsCertInStore(cert, store), "PfxData certificate was found after add"); + + store.Remove(cert); + Assert.False(IsCertInStore(cert, store), "PfxData certificate was found after remove"); + } + } + + [ConditionalFact(nameof(PermissionsAllowStoreWrite))] + public static void RemovePublicDeletesPrivateKey() + { + using (var store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) + using (var cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword, X509KeyStorageFlags.Exportable)) + using (var certOnly = new X509Certificate2(cert.RawData)) + { + store.Open(OpenFlags.ReadWrite); + + // Defensive removal. + store.Remove(cert); + Assert.False(IsCertInStore(cert, store), "PfxData certificate was found on pre-condition"); + + // Add the private key + store.Add(cert); + Assert.True(IsCertInStore(cert, store), "PfxData certificate was found after add"); + + store.Remove(certOnly); + Assert.False(IsCertInStore(cert, store), "PfxData certificate was found after remove"); + + // Add back the public key only + store.Add(certOnly); + Assert.True(IsCertInStore(cert, store), "PfxData certificate was found after public-only add"); + Assert.False(StoreHasPrivateKey(store, cert), "Store has a private key for cert after public-only add"); + + // Cleanup + store.Remove(certOnly); + } + } + + private static bool StoreHasPrivateKey(X509Store store, X509Certificate2 forCert) + { + using (ImportedCollection coll = new ImportedCollection(store.Certificates)) + { + foreach (X509Certificate2 storeCert in coll.Collection) + { + if (forCert.Equals(storeCert)) + { + return storeCert.HasPrivateKey; + } + } + } + + Assert.True(false, $"Certificate ({forCert.Subject}) exists in the store"); + return false; + } + + private static bool IsCertInStore(X509Certificate2 cert, X509Store store) + { + using (ImportedCollection coll = new ImportedCollection(store.Certificates)) + { + foreach (X509Certificate2 storeCert in coll.Collection) + { + if (cert.Equals(storeCert)) + { + return true; + } + } + } + + return false; + } + + private static int GetStoreCertificateCount(X509Store store) + { + using (var coll = new ImportedCollection(store.Certificates)) + { + return coll.Collection.Count; + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreTests.cs b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreTests.cs index 71b13d8..ea0906d 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreTests.cs +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/X509StoreTests.cs @@ -9,6 +9,7 @@ using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using Microsoft.DotNet.RemoteExecutor; +using Test.Cryptography; using Xunit; namespace System.Security.Cryptography.X509Certificates.Tests @@ -94,9 +95,9 @@ namespace System.Security.Cryptography.X509Certificates.Tests } } - [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.OSX)] // API not supported via OpenSSL + [PlatformSpecific(PlatformSupport.OpenSSL)] // API not supported via OpenSSL [Fact] - public static void Constructor_StoreHandle_Unix() + public static void Constructor_StoreHandle_OpenSSL() { using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser)) {