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

橙三吉。 提交于 2021-01-21 05:14:27

问题


I'm using SafetyNet API for checking if device is rooted or not and using the below helpful code but this uses Android verification API to validate the JWT signature:

https://github.com/scottyab/safetynethelper

And I want to validate on client side only to reduce the overhead of another web service all and besides it has limitation on only 10k request per day.

So after decoding the JWS i'm getting the below info

Sample JWS message response

xxxx.yyy.zzzz

Header data

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

Payload data

{"nonce":"<nounce>",
"timestampMs":1472794339527,
"apkPackageName":"<apkPackageName>",
"apkDigestSha256":"<sha digest string>",
"ctsProfileMatch":true,
"extension":"<extension string>",
"apkCertificateDigestSha256":["<apkCertificateDigestSha256 string>"],"basicIntegrity":true}

Signature in this part if perform Base64 decoding it becomes unreadable so below is the Signature string as received in JWS last element

Gw09rv1aBbtd4Er7F5ww_3TT1mPRD5YouMkPkwnRXJq8XW_cxlO4428DHTJdD8Tbep-Iv3nrVRWt2t4pH1uSr2kJ9budQJuXqzOUhN93r2Hfk-UAKUYQYhp89_wOWjSCG4ySVHD4jc9S1HrZlngaUosocOmhN4SzLZN5o8BXyBdXkjhWwgArd4bcLhCWJzmxz5iZfkhDiAyeNRq09CeqjRx_plqAy8eR_OaI_2idZBNIGfd2KmLK_CKaeVjDxuC4BzJsIlVRiuLrvP362Wwhz4r1bHh8flmHr88nK99apP2jkQD2l7lPv8y5F3FN3DKhJ15CzHR6ZbiTOw1fUteifg

Now as per 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."

I do have the cert string and signature how should I go about validating SSL certificate which is string and host name matching on second cert and how to validate signature.

I need pointers on this and code snipped would be very helpful.


回答1:


The way you want to validate JWT signature on the device is not secure. Think about next case:

  • the device is rooted, malware application with root privileges catches your request to Google's SafetyNet and returns self-signed response.

  • When you verify the response with your own server service - you will get that the response you've got wasn't provided by Google. If you do this locally on the device - the same malware app could catch you request to verify JWT signature and respond with true.

Anyway, you can do this locally:

  1. You need to get API key from Google developers for your application.
  2. Use the Android Device Verification API:

From Android Developers:

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:

{ "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=<your API 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 }

So actually (code from SafetyNet Helper):

/**
 *
 * 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);
            }
        }
    }

}



回答2:


Item #5 on the Android Developers Blog article 10 things you might be doing wrong when using the SafetyNet Attestation API says:

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.




回答3:


There's an open source android library that is capable of helping perform the validation: jwtk/jjwt



来源:https://stackoverflow.com/questions/39286379/how-to-validate-safety-net-jws-signature-from-header-data-in-android-app

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!