如何解码 SafetyNet JWS 响应?

How to decode SafetyNet JWS response?

我正在调查 Google 在我的 Android 应用程序中提供的 SafetyNet。

首先,我简单地调用了 SafetyNet 证明 API,Base64 解码了 Google 提供的示例中显示的部分。

SafetyNet.getClient(this).attest(NONCE, <API KEY>)
        .addOnSuccessListener(this, new OnSuccessListener<SafetyNetApi.AttestationResponse>() {
            @Override
            public void onSuccess(final SafetyNetApi.AttestationResponse attestationResponse) {
                initialDataExtraction(attestationResponse.getJwsResult());
            }
        })
        .addOnFailureListener(this, new OnFailureListener() {
            @Override
            public void onFailure(@NonNull final Exception exception) {
                if (exception instanceof ApiException) {
                    final ApiException apiException = (ApiException) exception;
                    Log.e(TAG, "onFailure: " + apiException.getMessage() + " " + apiException.getStatusCode());
                } else {
                    Log.e(TAG, "Error: ", exception);
                }
            }
        });

我提取JWS部分如下:-

private byte[] initialDataExtraction(final String jwsResult) {

    final String[] jwsResultParts = jwsResult.split("[.]");
    if (jwsResultParts.length == 3) {
        final byte[] header = Base64.decode(jwsResultParts[0], Base64.NO_WRAP);
        final byte[] data = Base64.decode(jwsResultParts[1], Base64.NO_WRAP);
        final byte[] signature = Base64.decode(jwsResultParts[2], Base64.NO_WRAP);

        Log.d(TAG, "initialDataExtraction: header = " + new String(header, UTF_8));
        Log.d(TAG, "initialDataExtraction: data = " + new String(data, UTF_8));
        Log.d(TAG, "initialDataExtraction: signature = " + new String(signature, UTF_8));

        return data;
    } else {
        Log.e(TAG, "initialDataExtraction: Failure: Illegal JWS signature format. The JWS consists of " + jwsResultParts.length + " parts instead of 3.");
        return null;
    }
}

我正在使用 android.util.Base64 解码部分,大多数情况下解码完成。

虽然我偶尔会收到此异常:-

java.lang.IllegalArgumentException: bad base-64
 at android.util.Base64.decode(Base64.java:161)
 at android.util.Base64.decode(Base64.java:136)
 at android.util.Base64.decode(Base64.java:118)

解码签名部分时。

解码时看到这个间歇性错误,我做错了什么?

然后我开始使用 JWT 库来解码令牌。

首先我尝试了 group: 'com.auth0.android', name: 'jwtdecode', version: '1.1.1'

我试过的代码是

 final JWT jwt = new JWT(jwsResult);

始终失败并出现以下错误

com.auth0.android.jwt.DecodeException: The token's payload had an invalid JSON format.
at com.auth0.android.jwt.JWT.parseJson(JWT.java:235)
at com.auth0.android.jwt.JWT.decode(JWT.java:203)
at com.auth0.android.jwt.JWT.<init>(JWT.java:40)
Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected a string but was BEGIN_ARRAY at line 1 column 23 path $.
at com.google.gson.Gson.fromJson(Gson.java:899)
at com.google.gson.Gson.fromJson(Gson.java:852)
at com.google.gson.Gson.fromJson(Gson.java:801)

此异常似乎是由于 Auth0 库无法解析 headers 4.1.6. "x5c" (X.509 Certificate Chain) Header 格式引起的,这很奇怪,因为 JWS 规范明确指出该值由 JSON 表示数组:-

The "x5c" (X.509 Certificate Chain) Header Parameter contains the
   X.509 public key certificate or certificate chain [RFC5280]
   corresponding to the key used to digitally sign the JWS.  The
   certificate or certificate chain is represented as a JSON array of
   certificate value strings. 

但是,如果我将相同的 jws 结果字符串复制并粘贴到纯 java 项目中并使用 compile 'com.auth0:java-jwt:3.3.0' 并使用此代码:-

String token = "<JWS TOKEN>";
        try {
            final DecodedJWT jwt = JWT.decode(token);
            System.out.println("Header = " + jwt.getHeader());
            System.out.println("Payload = " + jwt.getPayload());
            System.out.println("Signature = " + jwt.getSignature());
                } catch (JWTDecodeException exception){
           throw new RuntimeException(exception);
        }

Jws Token解码成功

我的 Android 应用程序中我做错了什么导致 auth0 android jwt 库按预期停止工作?

然后我在我的 Android 应用程序中尝试了 'io.jsonwebtoken:jjwt:0.9.0' 库。

当我执行这段代码时:-

Jwts.parser().parse(jwsResult).getBody();

它失败了:-

java.lang.IllegalArgumentException: A signing key must be specified if the specified JWT is digitally signed.
 at io.jsonwebtoken.lang.Assert.notNull(Assert.java:85)
 at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:331)

我需要将什么签名密钥传递给 Jwts?我唯一拥有的密钥是 Google API 控制台中的 API 密钥,这是我应该使用的密钥吗?

当我传递如下:

Jwts.parser().setSigningKey<API KEY>.parse(jwsResult).getBody();

这失败了:-

java.lang.IllegalArgumentException: Key bytes can only be specified for HMAC signatures. Please specify a PublicKey or PrivateKey instance.
at io.jsonwebtoken.lang.Assert.isTrue(Assert.java:38)
at io.jsonwebtoken.impl.DefaultJwtParser.parse(DefaultJwtParser.java:324)

解码和使用从 SafetyNet attest API 调用收到的 Jws 结果的正确方法是什么?

我从这个问题

中找到了 java.lang.IllegalArgumentException: bad base-64 问题的修复方法

解码前简单替换jws token中的字符

token.replace('-', '+').replace('_', '/')

我发现这个库不仅可以完成它在 Android.

上运行良好的工作
// https://mvnrepository.com/artifact/com.nimbusds/nimbus-jose-jwt
implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '5.1'


    try {
        final JWSObject jwsObject = JWSObject.parse(jwsResult);
        System.out.println("header = " + jwsObject.getHeader());
        System.out.println("header = " + jwsObject.getHeader().getX509CertChain());
        System.out.println("payload = " + jwsObject.getPayload().toJSONObject());
        System.out.println("signature = " + jwsObject.getSignature());
        System.out.println("signature = " + jwsObject.getSignature().decodeToString());
    } catch (ParseException e) {
        e.printStackTrace();
    }

提供了一些不错的示例:-

https://connect2id.com/products/nimbus-jose-jwt/examples