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 。进行响应。
无论如何,您可以在本地执行此操作:
来自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