googlesamples / android-play-safetynet

Samples for the Google SafetyNet Attestation API
Apache License 2.0
284 stars 131 forks source link

CSharp version not compatible with .NET Core and .NET 5/6 #33

Open bugnuker opened 2 years ago

bugnuker commented 2 years ago

The included sample is not compatible with .NET core, or .NET 5 and 6. This only works with .NET Framework.

The underlying issue is extracting the keys from the token.

.NET and .NET Core will throw a null ref exception because the cast to JArray is not working for .NET and .NET Core.

The real error is: ​Unable to cast object of type 'Microsoft.IdentityModel.Json.Linq.JArray'

It seems this might be due to .NET using System.Text.Json rather than newtonsoft. The space 'Microsoft.IdentityModel.Json.Linq.JArray' is private and can not be accessed.

brunopiovan commented 2 years ago

this should get you started...

using System.IdentityModel.Tokens.Jwt;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using Microsoft.IdentityModel.Tokens;

const string attestationStatementString = "token here";
const string validHostName = "attest.android.com";

var token = new JwtSecurityToken(attestationStatementString);
var x5c = JsonSerializer.Deserialize<string[]>(token.Header.X5c)!;

//or use this if you don't need JwtSecurityToken
//var array = attestationStatementString.Split(new[] { '.' }, 2);
//var header = JwtHeader.Base64UrlDeserialize(array[0]);
//var x5c = JsonSerializer.Deserialize<string[]>(header.X5c)!;

if (x5c.Length == 0) return;

var signingKeys = x5c
    .Select(Convert.FromBase64String)
    .Select(x => new X509Certificate2(x))
    .Select(x => new X509SecurityKey(x));

var validationParameters = new TokenValidationParameters
{
    ValidateIssuer = false,
    ValidateAudience = false,
    ValidateLifetime = false,
    ValidateIssuerSigningKey = true,
    IssuerSigningKeys = signingKeys
};

var handler = new JwtSecurityTokenHandler();
handler.ValidateToken(attestationStatementString, validationParameters, out var validatedToken);

if (validatedToken.SigningKey is not X509SecurityKey x509SecurityKey)
{
    Console.WriteLine("The signing key is invalid.");
    return;
}

var chain = new X509Chain();
var chainBuilt = chain.Build(x509SecurityKey.Certificate);
if (!chainBuilt)
{
    foreach (var chainStatus in chain.ChainStatus)
    {
        Console.WriteLine(string.Format("Chain error: {0} {1}", chainStatus.Status, chainStatus.StatusInformation));
    }

    return;
}

var isHostNameValid = x509SecurityKey.Certificate.GetNameInfo(X509NameType.DnsName, false) == validHostName;
if (isHostNameValid)
{
    Console.WriteLine("Validation ok!");
}
else
{
    Console.WriteLine("Validation failed!");
}
bugnuker commented 2 years ago

This worked great, thanks! The part that was needed:

var x5c = JsonSerializer.Deserialize<string[]>(token.Header.X5c)!;
if (x5c.Length == 0) return;
var signingKeys = x5c
    .Select(Convert.FromBase64String)
    .Select(x => new X509Certificate2(x))
    .Select(x => new X509SecurityKey(x));

Thanks again!