Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix X509 test failures on Android #50301

Merged
15 commits merged into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
Expand Down Expand Up @@ -41,16 +42,23 @@ internal static X509Certificate2[] X509ChainGetCertificates(SafeX509ChainContext
var certPtrs = new IntPtr[count];

int res = Interop.AndroidCrypto.X509ChainGetCertificates(ctx, certPtrs, certPtrs.Length);
if (res != SUCCESS)
if (res == 0)
throw new CryptographicException();

Debug.Assert(res <= certPtrs.Length);

var certs = new X509Certificate2[certPtrs.Length];
for (int i = 0; i < certs.Length; i++)
for (int i = 0; i < res; i++)
{
certs[i] = new X509Certificate2(certPtrs[i]);
}

return certs;
if (res == certPtrs.Length)
{
return certs;
}

return certs[0..res];
}

[StructLayout(LayoutKind.Sequential)]
Expand Down Expand Up @@ -90,6 +98,10 @@ internal static extern int X509ChainSetCustomTrustStore(
IntPtr[] customTrustStore,
int customTrustStoreLen);

[DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509ChainSupportsRevocationOptions")]
[return:MarshalAs(UnmanagedType.U1)]
internal static extern bool X509ChainSupportsRevocationOptions();

[DllImport(Libraries.CryptoNative, EntryPoint = "AndroidCryptoNative_X509ChainValidate")]
internal static extern int X509ChainValidate(
SafeX509ChainContextHandle ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,7 +315,7 @@ static jobject AndroidCryptoNative_CreateKeyPairFromCurveParameters(
goto cleanup;

error:
if (loc[privateKey])
if (loc[privateKey] && (*env)->IsInstanceOf(env, loc[privateKey], g_DestroyableClass))
{
// Destroy the private key data.
(*env)->CallVoidMethod(env, loc[privateKey], g_destroy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ void AndroidCryptoNative_EcKeyDestroy(EC_KEY* r)
{
// Destroy the private key data.
jobject privateKey = (*env)->CallObjectMethod(env, r->keyPair, g_keyPairGetPrivateMethod);
if (privateKey)
if (privateKey && (*env)->IsInstanceOf(env, privateKey, g_DestroyableClass))
{
(*env)->CallVoidMethod(env, privateKey, g_destroy);
ReleaseLRef(env, privateKey);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

#include "pal_jni.h"
#include <pthread.h>

JavaVM* gJvm;

Expand Down Expand Up @@ -476,6 +477,8 @@ static jclass GetOptionalClassGRef(JNIEnv *env, const char* name)
if (!TryGetClassGRef(env, name, &klass))
{
LOG_DEBUG("optional class %s was not found", name);
// Failing to find an optional class causes an exception state, which we need to clear.
TryClearJNIExceptions(env);
}

return klass;
Expand Down Expand Up @@ -551,6 +554,8 @@ jmethodID GetOptionalMethod(JNIEnv *env, bool isStatic, jclass klass, const char
jmethodID mid = isStatic ? (*env)->GetStaticMethodID(env, klass, name, sig) : (*env)->GetMethodID(env, klass, name, sig);
if (!mid) {
LOG_INFO("optional method %s %s was not found", name, sig);
// Failing to find an optional method causes an exception state, which we need to clear.
TryClearJNIExceptions(env);
}
return mid;
}
Expand All @@ -566,13 +571,34 @@ jfieldID GetField(JNIEnv *env, bool isStatic, jclass klass, const char* name, co
return fid;
}

static void DetatchThreadFromJNI(void* unused)
{
LOG_DEBUG("Detaching thread from JNI");
(void)unused;
(*gJvm)->DetachCurrentThread(gJvm);
}

static pthread_key_t threadLocalEnvKey;
static pthread_once_t threadLocalEnvInitKey = PTHREAD_ONCE_INIT;

static void
make_key()
{
(void) pthread_key_create(&threadLocalEnvKey, &DetatchThreadFromJNI);
}

JNIEnv* GetJNIEnv()
{
JNIEnv *env;
(*gJvm)->GetEnv(gJvm, (void**)&env, JNI_VERSION_1_6);
if (env)
return env;
jint ret = (*gJvm)->AttachCurrentThreadAsDaemon(gJvm, &env, NULL);

(void) pthread_once(&threadLocalEnvInitKey, make_key);
LOG_DEBUG("Registering JNI thread detach. env ptr %p. Key: %ld", (void*)env, (long)threadLocalEnvKey);
pthread_setspecific(threadLocalEnvKey, env);

assert(ret == JNI_OK && "Unable to attach thread to JVM");
(void)ret;
return env;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ void* AndroidCryptoNative_X509PublicKey(jobject /*X509Certificate*/ cert, PAL_Ke

void* keyHandle;
jobject key = (*env)->CallObjectMethod(env, cert, g_X509CertGetPublicKey);
if (!key)
{
return NULL;
}
switch (algorithm)
{
case PAL_EC:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,16 @@ int32_t AndroidCryptoNative_X509ChainGetCertificates(X509ChainContext* ctx,
// Certificate trustedCert = trustAnchor.getTrustedCert();
// certs[i] = trustedCert;
jobject trustedCert = (*env)->CallObjectMethod(env, ctx->trustAnchor, g_TrustAnchorGetTrustedCert);
certs[i] = ToGRef(env, trustedCert);
if (!(*env)->IsSameObject(env, certs[i-1], certs[i]))
{
certs[i] = ToGRef(env, trustedCert);
ret = i + 1;
}
else
{
ret = i;
certs[i] = NULL;
}

ret = SUCCESS;

Expand Down Expand Up @@ -404,7 +413,7 @@ int32_t AndroidCryptoNative_X509ChainSetCustomTrustStore(X509ChainContext* ctx,
return CheckJNIExceptions(env) ? FAIL : SUCCESS;
}

static bool X509ChainSupportsRevocationOptions(void)
bool AndroidCryptoNative_X509ChainSupportsRevocationOptions(void)
{
return g_CertPathValidatorGetRevocationChecker != NULL && g_PKIXRevocationCheckerClass != NULL;
}
Expand Down Expand Up @@ -483,7 +492,7 @@ static int32_t ValidateWithRevocation(JNIEnv* env,
else
{
certPathToUse = ctx->certPath;
if (X509ChainSupportsRevocationOptions())
if (AndroidCryptoNative_X509ChainSupportsRevocationOptions())
{
// Only add the ONLY_END_ENTITY if we are not just checking the trust anchor. If ONLY_END_ENTITY is
// specified, revocation checking will skip the trust anchor even if it is the only certificate.
Expand Down Expand Up @@ -512,7 +521,7 @@ static int32_t ValidateWithRevocation(JNIEnv* env,
}

jobject params = ctx->params;
if (X509ChainSupportsRevocationOptions())
if (AndroidCryptoNative_X509ChainSupportsRevocationOptions())
{
// PKIXRevocationChecker checker = validator.getRevocationChecker();
loc[checker] = (*env)->CallObjectMethod(env, validator, g_CertPathValidatorGetRevocationChecker);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ Return the number of certificates in the path
PALEXPORT int32_t AndroidCryptoNative_X509ChainGetCertificateCount(X509ChainContext* ctx);

/*
Get the certificates in the path
Get the certificates in the path.

Returns 1 on success, 0 otherwise
Returns the number of certificates exported.
*/
PALEXPORT int32_t AndroidCryptoNative_X509ChainGetCertificates(X509ChainContext* ctx,
jobject* /*X509Certificate[]*/ certs,
Expand All @@ -62,6 +62,11 @@ PALEXPORT int32_t AndroidCryptoNative_X509ChainSetCustomTrustStore(X509ChainCont
jobject* /*X509Certificate[]*/ customTrustStore,
int32_t customTrustStoreLen);

/*
Returns true if revocation checking is supported. Returns false otherwise.
*/
PALEXPORT bool AndroidCryptoNative_X509ChainSupportsRevocationOptions(void);

// Matches managed X509RevocationMode enum
enum
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ int32_t AndroidCryptoNative_X509StoreAddCertificate(jobject /*KeyStore*/ store,
EntryFlags flags;
if (ContainsEntryForAlias(env, store, cert, alias, &flags))
{
ReleaseLRef(env, alias);
EntryFlags matchesFlags = EntryFlags_HasCertificate & EntryFlags_MatchesCertificate;
if ((flags & matchesFlags) != matchesFlags)
{
Expand Down Expand Up @@ -141,20 +142,22 @@ int32_t AndroidCryptoNative_X509StoreAddCertificateWithPrivateKey(jobject /*KeyS
EntryFlags matchesFlags = EntryFlags_HasCertificate & EntryFlags_MatchesCertificate;
if ((flags & matchesFlags) != matchesFlags)
{
RELEASE_LOCALS(loc, env);
LOG_ERROR("Store already contains alias with entry that does not match the expected certificate");
return FAIL;
}

if ((flags & EntryFlags_HasPrivateKey) == EntryFlags_HasPrivateKey)
{
RELEASE_LOCALS(loc, env);
// 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);
(*env)->CallVoidMethod(env, store, g_KeyStoreDeleteEntry, loc[alias]);
}

bool releasePrivateKey = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,13 @@ public static void CreateChain_RSAPSS()

private static bool DetectPssSupport()
{
if (PlatformDetection.IsAndroid)
{
// Android supports PSS at the algorithms layer, but does not support it
// being used in cert chains.
return false;
}

using (X509Certificate2 cert = new X509Certificate2(TestData.PfxData, TestData.PfxDataPassword))
using (RSA rsa = cert.GetRSAPrivateKey())
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,10 +210,13 @@ public static void TestResetMethod()

/// <summary>
/// Tests that when a certificate chain has a root certification which is not trusted by the trust provider,
/// Build returns false and a ChainStatus returns UntrustedRoot
/// Build returns false and a ChainStatus returns UntrustedRoot.
/// Android does not support the detailed status in this test. It always validates time
/// and trusted root. It will fail to build any chain if those are not valid.
/// </summary>
[Fact]
[OuterLoop]
[PlatformSpecific(~TestPlatforms.Android)]
public static void BuildChainExtraStoreUntrustedRoot()
{
using (var testCert = new X509Certificate2(TestFiles.ChainPfxFile, TestData.ChainPfxPassword))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,21 @@ namespace System.Security.Cryptography.X509Certificates.Tests
{
public static class PublicKeyTests
{
public static bool SupportsBrainpool { get; } = IsCurveSupported(ECCurve.NamedCurves.brainpoolP160r1.Oid);
jkoritzinsky marked this conversation as resolved.
Show resolved Hide resolved

private static bool IsCurveSupported(Oid oid)
{
try
{
using ECDsa ecdsa = ECDsa.Create(ECCurve.CreateFromOid(oid));
}
catch (Exception ex) when (ex is CryptographicException || ex is PlatformNotSupportedException)
{
return false;
}
return true;
}

private static PublicKey GetTestRsaKey()
{
using (var cert = new X509Certificate2(TestData.MsCertificate))
Expand Down Expand Up @@ -355,7 +370,7 @@ public static void TestKey_RSA384_ValidatesSignature()
}
}

[Theory, MemberData(nameof(BrainpoolCurves))]
[ConditionalTheory(nameof(SupportsBrainpool)), MemberData(nameof(BrainpoolCurves))]
jkoritzinsky marked this conversation as resolved.
Show resolved Hide resolved
public static void TestKey_ECDsabrainpool_PublicKey(byte[] curveData, byte[] notUsed)
{
_ = notUsed;
Expand Down Expand Up @@ -472,7 +487,7 @@ public static void TestECDHPublicKey_DeriveSecret()
}
}

[Theory, MemberData(nameof(BrainpoolCurves))]
[ConditionalTheory(nameof(SupportsBrainpool)), MemberData(nameof(BrainpoolCurves))]
public static void TestECDsaPublicKey_BrainpoolP160r1_ValidatesSignature(byte[] curveData, byte[] existingSignature)
{
byte[] helloBytes = Encoding.ASCII.GetBytes("Hello");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates.Tests.Common;
using Xunit;

namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests
{
public static partial class DynamicRevocationTests
{
public static bool SupportsDynamicRevocation { get; } = Interop.AndroidCrypto.X509ChainSupportsRevocationOptions();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Security.Cryptography.X509Certificates.Tests.Common;
using Xunit;

namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests
{
public static partial class DynamicRevocationTests
{
public static bool SupportsDynamicRevocation => true;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests
{
[OuterLoop("These tests run serially at about 1 second each, and the code shouldn't change that often.")]
public static class DynamicRevocationTests
[ConditionalClass(typeof(DynamicRevocationTests), nameof(SupportsDynamicRevocation))]
public static partial class DynamicRevocationTests
{
// The CI machines are doing an awful lot of things at once, be generous with the timeout;
internal static readonly TimeSpan s_urlRetrievalLimit = TimeSpan.FromSeconds(15);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,22 @@
<ItemGroup Condition="'$(UseAndroidCrypto)' == 'true'">
<Compile Include="X509StoreMutableTests.Android.cs" />
</ItemGroup>
<ItemGroup Condition="'$(UseAndroidCrypto)' != 'true'">
<Compile Include="RevocationTests\DynamicRevocationTests.Default.cs" />
</ItemGroup>
<ItemGroup Condition="'$(UseAndroidCrypto)' == 'true'">
Copy link
Member

Choose a reason for hiding this comment

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

Let's reuse existing groups when possible.

<Compile Include="RevocationTests\DynamicRevocationTests.Android.cs" />
<Compile Include="$(CommonPath)Interop\Android\System.Security.Cryptography.Native.Android\Interop.X509.cs"
Link="Common\Interop\Android\System.Security.Cryptography.Native.Android\Interop.X509.cs" />
<Compile Include="$(CommonPath)Interop\Android\System.Security.Cryptography.Native.Android\Interop.X509Chain.cs"
Link="Common\Interop\Android\System.Security.Cryptography.Native.Android\Interop.X509Chain.cs" />
<Compile Include="$(CommonPath)Interop\Android\Interop.JObjectLifetime.cs"
Link="Common\Interop\Android\Interop.JObjectLifetime.cs" />
<Compile Include="$(CommonPath)Interop\Android\Interop.Libraries.cs"
Link="Common\Interop\Android\Interop.Libraries.cs" />
<Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs"
Link="Common\Interop\Unix\Interop.Libraries.cs" />
</ItemGroup>
<ItemGroup Condition="'$(UseAppleCrypto)' == 'true'">
<Compile Include="X509StoreMutableTests.OSX.cs" />
<Compile Include="$(CommonPath)Interop\OSX\Interop.CoreFoundation.cs"
Expand Down