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

2022-01-10 00:00:00 validation jwt cryptography android java

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

https://github.com/scottyab/safetynethelper

我想在客户端进行验证只是为了减少另一个 Web 服务的开销,此外它还限制每天只有 10k 个请求.

所以在解码 JWS 后,我得到以下信息

示例 JWS 消息响应

xxxx.yyy.zzzz

标题数据

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

有效载荷数据

{"nonce":"<nounce>",时间戳":1472794339527,"apkPackageName":"<apkPackageName>","apkDigestSha256":"",ctsProfileMatch":真,"extension":"<扩展字符串>","apkCertificateDigestSha256":["<apkCertificateDigestSha256 字符串>"],"basicIntegrity":true}

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

<预> <代码> Gw09rv1aBbtd4Er7F5ww_3TT1mPRD5YouMkPkwnRXJq8XW_cxlO4428DHTJdD8Tbep-Iv3nrVRWt2t4pH1uSr2kJ9budQJuXqzOUhN93r2Hfk-UAKUYQYhp89_wOWjSCG4ySVHD4jc9S1HrZlngaUosocOmhN4SzLZN5o8BXyBdXkjhWwgArd4bcLhCWJzmxz5iZfkhDiAyeNRq09CeqjRx_plqAy8eR_OaI_2idZBNIGfd2KmLK_CKaeVjDxuC4BzJsIlVRiuLrvP362Wwhz4r1bHh8flmHr88nK99apP2jkQD2l7lPv8y5F3FN3DKhJ15CzHR6ZbiTOw1fUteifg

现在根据谷歌

<块引用>

"验证兼容性检查响应:提取 SSL 证书来自 JWS 消息的链.验证 SSL 证书链并使用SSL 主机名匹配以验证叶证书是否已颁发到主机名 attest.android.com.使用证书来验证JWS 消息的签名."

我确实有证书字符串和签名我应该如何去验证 SSL 证书,它是字符串和主机名匹配的第二个证书和如何验证签名.

我需要这方面的指针,并且代码被剪断会很有帮助.

解决方案

您要在设备上验证 JWT 签名的方式不安全.考虑下一个案例:

  • 设备已root,具有root权限的恶意软件应用程序捕获您对 Google 的 SafetyNet 的请求并返回自签名回复.

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

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

  1. 您需要从 Google 开发人员处为您的应用获取 API 密钥.
  2. 使用 Android 设备验证 API:

来自 Android 开发者:

<块引用>

注意:验证响应消息的 API 方法的固定速率限制为每个项目每天 10,000 个请求.您应该仅在初始开发阶段使用 verify() 方法进行测试.您不应在生产场景中调用该方法.

[...]

要使用 Android 设备验证 API:

创建包含 JWS 全部内容的 JSON 消息消息格式如下:

{ "signedAttestation": "<输出>getJwsResult()>"}

使用 HTTP POST 请求发送带有"application/json" 的 Content-Type 到以下 URL:https://www.googleapis.com/androidcheck/v1/attestations/verify?key=<你的 API 密钥>

服务验证消息的完整性,如果消息有效,它返回一个 JSON 消息,内容如下内容:{ isValidSignature": true }

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

/**** 使用 Android 设备验证 API 验证结果.** 注意:这仅验证提供的 JWS(JSON Web 签名)消息是从实际的 SafetyNet 服务接收到的.* 它*不*验证负载数据是否与您的原始兼容性检查请求相匹配.* 发布到 https://www.googleapis.com/androidcheck/v1/attestations/verify?key=<你的 API 密钥>** 更多信息请参阅 {link https://developer.android.com/google/play/safetynet/start.html#verify-compat-check}** 由 scottab 于 27/05/2015 创建.*/公共类 AndroidDeviceVerifier {私有静态最终字符串 TAG = AndroidDeviceVerifier.class.getSimpleName();//用于验证安全网响应 - 10,000 个请求/天免费私有静态最终字符串 GOOGLE_VERIFICATION_URL = "https://www.googleapis.com/androidcheck/v1/attestations/verify?key=";私有最终字符串 apiKey;私有最终字符串签名验证;私有 AndroidDeviceVerifierCallback 回调;公共接口 AndroidDeviceVerifierCallback{无效错误(字符串 s);无效成功(布尔 isValidSignature);}public AndroidDeviceVerifier(@NonNull String apiKey, @NonNull String signatureToVerify) {this.apiKey = apiKey;this.signatureToVerify = signatureToVerify;}公共无效验证(AndroidDeviceVerifierCallback androidDeviceVerifierCallback){回调 = androidDeviceVerifierCallback;AndroidDeviceVerifierTask 任务 = new AndroidDeviceVerifierTask();任务.执行();}/*** 为 URL 连接提供信任管理器.默认情况下,这使用系统默认值加上 GoogleApisTrustManager (SSL pinning)* @return 包括系统默认值和 GoogleApisTrustManager(SSL pinning)的 TrustManager 数组* @throws KeyStoreException* @throws NoSuchAlgorithmException*/受保护的 TrustManager[] getTrustManagers() 抛出 KeyStoreException,NoSuchAlgorithmException {TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());//使用默认的系统信任管理器初始化trustManagerFactory.init((KeyStore)null);TrustManager[] defaultTrustManagers = trustManagerFactory.getTrustManagers();TrustManager[] trustManagers = Arrays.copyOf(defaultTrustManagers, defaultTrustManagers.length + 1);//添加我们的谷歌APIs pinning TrustManager以获得额外的安全性trustManagers[defaultTrustManagers.length] = new GoogleApisTrustManager();返回信任管理器;}私有类 AndroidDeviceVerifierTask 扩展 AsyncTask<Void, Void, Boolean>{私人异常错误;@覆盖protected Boolean doInBackground(Void...params) {//Log.d(TAG, "signatureToVerify:" + signatureToVerify);尝试 {URL verifyApiUrl = 新 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": "<getJwsResult() 的输出>"}字符串 requestJsonBody = "{ "signedAttestation": ""+signatureToVerify+""}";byte[] outputInBytes = requestJsonBody.getBytes("UTF-8");输出流 os = urlConnection.getOutputStream();os.write(outputInBytes);os.close();urlConnection.connect();//resp ={ isValidSignature": true }InputStream = urlConnection.getInputStream();StringBuilder sb = new StringBuilder();BufferedReader rd = new BufferedReader(new InputStreamReader(is));字符串线;while ((line = rd.readLine()) != null) {sb.append(line);}字符串响应 = sb.toString();JSONObject responseRoot = new JSONObject(response);if(responseRoot.has("isValidSignature")){返回 responseRoot.getBoolean("isValidSignature");}}catch (异常 e){//请求验证 JWS 消息时出错错误 = e;Log.e(TAG, "验证 JWS 消息时出现问题:" + e.getMessage(), e);返回假;}返回假;}@覆盖受保护的无效 onPostExecute(Boolean aBoolean) {如果(错误!=空){callback.error(error.getMessage());}别的 {callback.success(aBoolean);}}}}

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.

解决方案

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

}

相关文章