关于Java:如何从Android应用程序中的标头数据验证Safety Net JWS签名

How to validate Safety Net JWS signature from header data in Android app

我正在使用SafetyNet API来检查设备是否已植根
并使用以下有用的代码,但这使用了Android验证API
验证JWT签名:

https://github.com/scottyab/safetynethelper

我只想在客户端进行验证,以减少所有其他Web服务的开销,此外,它每天的请求量只有1万次。

因此,在解码JWS之后,我得到了以下信息

示例JWS消息响应

xxxx.yyy.zzzz

标题数据

1
{"alg":"RS256","x5c":["<certificate1 string>","<certificate2 string>"]}

有效载荷数据

1
2
3
4
5
6
7
{"nonce":"<nounce>",
"timestampMs":1472794339527,
"apkPackageName":"",
"apkDigestSha256":"<sha digest string>",
"ctsProfileMatch":true,
"extension":"<extension string>",
"apkCertificateDigestSha256":[""],"basicIntegrity":true}

签名
在这一部分中,如果执行Base64解码,它将变得不可读,因此下面是JWS中最后一个元素收到的Signature字符串

1
Gw09rv1aBbtd4Er7F5ww_3TT1mPRD5YouMkPkwnRXJq8XW_cxlO4428DHTJdD8Tbep-Iv3nrVRWt2t4pH1uSr2kJ9budQJuXqzOUhN93r2Hfk-UAKUYQYhp89_wOWjSCG4ySVHD4jc9S1HrZlngaUosocOmhN4SzLZN5o8BXyBdXkjhWwgArd4bcLhCWJzmxz5iZfkhDiAyeNRq09CeqjRx_plqAy8eR_OaI_2idZBNIGfd2KmLK_CKaeVjDxuC4BzJsIlVRiuLrvP362Wwhz4r1bHh8flmHr88nK99apP2jkQD2l7lPv8y5F3FN3DKhJ15CzHR6ZbiTOw1fUteifg

现在按照Google

"Verify the compatibility check response: Extract the SSL certificate
chain from the JWS message. Validate the SSL certificate chain and use
SSL hostname matching to verify that the leaf certificate was issued
to the hostname attest.android.com. Use the certificate to verify the
signature of the JWS message."

我确实具有证书字符串和签名,我该如何验证SSL证书,该证书是第二个证书上的字符串和主机名匹配,并且
如何验证签名。

我需要有关此的指针,并且代码摘要将非常有帮助。


您要在设备上验证JWT签名的方式并不安全。考虑下一种情况:

  • 该设备已root,具有root特权的恶意软件应用程序
    将您的请求捕获到Google的SafetyNet并返回自签名
    响应。

  • 当您使用自己的服务器服务验证响应时-您将得到的响应不是Google提供的。如果您在设备上本地执行此操作-同一恶意软件应用程序可能会捕获您的请求,以验证JWT签名并使用true

    进行响应。

无论如何,您可以在本地执行此操作:

  • 您需要从Google开发人员那里获取针对您的应用程序的API密钥。
  • 使用Android设备验证API:
  • 来自Android开发人员:

    Note: The API method to verify response messages has a fixed rate limit of 10,000 requests per day, per project. You should use the verify() method only for testing during the initial development stage. You shouldn't call the method in a production scenario.

    [...]

    To use the Android Device Verification API:

    Create a JSON message containing the entire contents of the JWS
    message in the following format:

    1
    {"signedAttestation":"<output of> getJwsResult()>" }

    Use an HTTP POST request to send the message with a
    Content-Type of "application/json" to the following URL:
    https://www.googleapis.com/androidcheck/v1/attestations/verify?key=

    The service validates the integrity of the message, and if
    the message is valid, it returns a JSON message with the following
    contents: {"isValidSignature": true }

    所以实际上(来自SafetyNet Helper的代码):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    /**
     *
     * Validates the result with Android Device Verification API.
     *
     * Note: This only validates that the provided JWS (JSON Web Signature) message was received from the actual SafetyNet service.
     * It does *not* verify that the payload data matches your original compatibility check request.
     * POST to https://www.googleapis.com/androidcheck/v1/attestations/verify?key=<your API key>
     *
     * More info see {link https://developer.android.com/google/play/safetynet/start.html#verify-compat-check}
     *
     * Created by scottab on 27/05/2015.
     */

    public class AndroidDeviceVerifier {

        private static final String TAG = AndroidDeviceVerifier.class.getSimpleName();

        //used to verifiy the safety net response - 10,000 requests/day free
        private static final String GOOGLE_VERIFICATION_URL ="https://www.googleapis.com/androidcheck/v1/attestations/verify?key=";

        private final String apiKey;
        private final String signatureToVerify;
        private AndroidDeviceVerifierCallback callback;

        public interface AndroidDeviceVerifierCallback{
            void error(String s);
            void success(boolean isValidSignature);
        }

        public AndroidDeviceVerifier(@NonNull String apiKey, @NonNull String signatureToVerify) {
            this.apiKey = apiKey;
            this.signatureToVerify = signatureToVerify;
        }

        public void verify(AndroidDeviceVerifierCallback androidDeviceVerifierCallback){
            callback = androidDeviceVerifierCallback;
            AndroidDeviceVerifierTask task = new AndroidDeviceVerifierTask();
            task.execute();
        }

        /**
         * Provide the trust managers for the URL connection. By Default this uses the system defaults plus the GoogleApisTrustManager (SSL pinning)
         * @return array of TrustManager including system defaults plus the GoogleApisTrustManager (SSL pinning)
         * @throws KeyStoreException
         * @throws NoSuchAlgorithmException
         */

        protected TrustManager[] getTrustManagers() throws KeyStoreException, NoSuchAlgorithmException {
            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
            //init with the default system trustmanagers
            trustManagerFactory.init((KeyStore)null);
            TrustManager[] defaultTrustManagers = trustManagerFactory.getTrustManagers();
            TrustManager[] trustManagers = Arrays.copyOf(defaultTrustManagers, defaultTrustManagers.length + 1);
            //add our Google APIs pinning TrustManager for extra security
            trustManagers[defaultTrustManagers.length] = new GoogleApisTrustManager();
            return trustManagers;
        }



        private class AndroidDeviceVerifierTask extends AsyncTask<Void, Void, Boolean>{

            private Exception error;

            @Override
            protected Boolean doInBackground(Void... params) {

                //Log.d(TAG,"signatureToVerify:" + signatureToVerify);

                try {
                    URL verifyApiUrl = new URL(GOOGLE_VERIFICATION_URL + apiKey);

                    SSLContext sslContext = SSLContext.getInstance("TLS");
                    sslContext.init(null, getTrustManagers(), null);

                    HttpsURLConnection urlConnection = (HttpsURLConnection) verifyApiUrl.openConnection();
                    urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());

                    urlConnection.setRequestMethod("POST");
                    urlConnection.setRequestProperty("Content-Type","application/json");

                    //build post body {"signedAttestation":"<output of getJwsResult()>" }
                    String requestJsonBody ="{ "signedAttestation": ""+signatureToVerify+""}";
                    byte[] outputInBytes = requestJsonBody.getBytes("UTF-8");
                    OutputStream os = urlConnection.getOutputStream();
                    os.write(outputInBytes);
                    os.close();

                    urlConnection.connect();

                    //resp ={"isValidSignature": true }
                    InputStream is = urlConnection.getInputStream();
                    StringBuilder sb = new StringBuilder();
                    BufferedReader rd = new BufferedReader(new InputStreamReader(is));
                    String line;
                    while ((line = rd.readLine()) != null) {
                        sb.append(line);
                    }
                    String response = sb.toString();
                    JSONObject responseRoot = new JSONObject(response);
                    if(responseRoot.has("isValidSignature")){
                        return responseRoot.getBoolean("isValidSignature");
                    }
                }catch (Exception e){
                    //something went wrong requesting validation of the JWS Message
                    error = e;
                    Log.e(TAG,"problem validating JWS Message :" + e.getMessage(), e);
                    return false;
                }
                return false;
            }

            @Override
            protected void onPostExecute(Boolean aBoolean) {
                if(error!=null){
                    callback.error(error.getMessage());
                }else {
                    callback.success(aBoolean);
                }
            }
        }

    }


    Android开发者博客文章中的项目#5使用SafetyNet Attestation API时您可能做错的十件事是:

    Using the test attestation verification service for production
    In order to simplify development and testing of the SafetyNet Attestation API, Google offers an online verification service that checks the digital signature of a SafetyNet Attestation result using a simple HTTPS request.

    As useful as this service may seem, it is designed for test purposes only, and it has very strict usage quotas that will not be increased upon request. Instead, you should implement the digital signature verification logic on your server in a way that it doesn't depend on Google's servers. Most JWT libraries offer signature verification functionality, and we have code samples that show how to perform this verification in Java and C#. We plan to provide samples for more languages in the future.


    有一个开源的android库,可以帮助执行验证:jwtk / jjwt