scottyab / safetynethelper

SafetyNet Helper wraps the Google Play Services SafetyNet.API and verifies Safety Net API response with the Android Device Verification API.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Create exmaple server side implementation to improve safetynet strength

scottyab opened this issue · comments

Based on feedback from Google's security team, for the safetynet to be more secure way of checking device integrity the

  • generate the nonce on the server, and have the server implement anti-replay
  • Verification of nonce performed server side
  • Verification of JWS should also be performed on the server side because the crypto libraries on your device may have been owned and hence it can return verified even if it's not.

How were you looking to implement this?

I have implemented the server but some of the cert verifications get pretty hairy.
Mostly the following:

  • 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.

More information on what needs to be done to check the JWS (as well as som client side code) can be found here: https://developer.android.com/training/safetynet/index.html

Hey @robsmall I was looking for a open source nodeJS, PHP, ruby (or any web language really) server implementation that I could either link to or include in this repo. This way people can see how the implement the server side validation - much of which I've done in the library already in Java. I guess it would also be cool if I could easily host on heroku.com for the smaple safternet app I have in the play store.

I figured two APIs from the server:
/initSafteyNet returns the nonce for the client to pass to Google SafteyNet.
/verifySafteyNet client sends the JWS message from Google SafteyNet for verification on the server. It checks the nonce/time etc. The api call return ctsTest:pass/fail. If fail on between validation checks.

If you've already implemented the server great maybe you could share with me privately if it's not ready for open source?

Hey @scottyab & @robsmall I'm looking for solution for client side validation of SSL certificate and Signature received in JWS response instead of using Verification API is there any code snippet on how can we validate signature

I just put up so question on this.
http://stackoverflow.com/questions/39286379/how-to-validate-safety-net-jws-signature-from-header-data-in-android-app

@khalid64927 thanks for commenting. If i understand your question... you're after client side parsing/reading and validation? This is exactly what safetynethelper does, maybe I need to make the readme.md clearer. But just to be clear it's more secure to have a server perform the validation (which is the point of this issue). In future it would probably help if you raised a new issue. Thanks

@scottyab Yes you are right I have to do the validation on client due to unavailability of service on the server at the moment.

And what i'm looking for is Validation of Signature and SSL chain ( all points also mentioned by @robsmall ). I can see safetynethelp does the payload validation and use verification API to do Signature validation but app i'm working has over million user and that's hitting limit with verification api (10k per day) so as per google. They suggested we can do that on client side as well and not required to hit this api. Since then i've been tinkering on this approach do you might be having any insights on this

Arrh ok, i'm with you. I would of thought only Google can verify the request came from Google? That's the point of that Google Verification API? maybe i'm missing something.

Hey guys,

Sorry for taking a while to respond to this thread. You can verify that the request came from google by verifying the cert chain and that the leaf cert came from attest.android.com. This is part of what the Google Verification API does, but it can also be done manually with some heavier lifting to avoid the rate limit. We had to patch some existing libraries in order to do so in a clean fashion.

Hey Rob,
Well I've got the domain validation in Leaf cert pretty straight forward and as for validation of signature I'm working with jjwt library that seems to be failing even on online jwt validation tool trying figure out why, did you had success validating signature part

Hi All,
Below is the code snippet for SSL domain validation and signature validation

`private boolean verifyLeafCert(String certString){

    boolean isValid = false;
    boolean isDomainValid = false;

    byte[] certData = Base64.decode(certString, Base64.DEFAULT);

    Log.d(TAG, "verifyLeafCert byte data"+ certData);


    if(certString == null ){
        return isValid;
    }

    CertificateFactory cf = null;
    Certificate ca = null;

    InputStream in = null;
    InputStream caInput;
    try {

        cf = CertificateFactory.getInstance("X.509");
        in = new ByteArrayInputStream(certData);
        caInput = new BufferedInputStream(in);

        ca = cf.generateCertificate(caInput);

        // save certificate for later use during signature validation
        addCert(ca);

        // checking the domain name
        Principal hostName = ((X509Certificate) ca).getSubjectDN();

        String[] subject = hostName.getName().split("\\,");

        for (String domainName: subject)
        {
            Log.d(TAG, "verifyLeafCert  domain name :: "+domainName);
            if(GOOGLE_DOMAIN_NAME.equalsIgnoreCase(domainName)){
                isDomainValid = true;
            }
        }

        if(!isDomainValid){
            Log.d(TAG, "verifyLeafCert  this certificate do not have from valid domain :: expected domain is attest.android.com");
            return false;
        }

        Log.d(TAG, "verifyLeafCert  isDomainValid :: "+ isDomainValid);

        // checking the validity of the cert
        Date expiryDate = ((X509Certificate) ca).getNotAfter();

        Date currentDate = new Date();


        Log.d(TAG, "verifyLeafCert  expiry date  :: " + expiryDate);
        Log.d(TAG, "verifyLeafCert  current date ::" + currentDate);

        if(!expiryDate.after(currentDate)){
            Log.d(TAG, "verifyLeafCert  certificate has expired :: ");
            return false;

        }

        isValid = true;

    }catch (CertificateException ce){
        ce.printStackTrace();
        Log.d(TAG, "verifyLeafCert failed to do validation");
        return isValid;
    }finally {
        try {
            in.close();
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    Log.d(TAG, "verifyLeafCert cert is valid");

    return isValid;
}`

Signature validation using JOSE library

`public boolean parseJws(String responseString){
Log.d(TAG, " parseJws:: method end");

    if(X509_CERTIFICATES == null || X509_CERTIFICATES.size() <= 0 ){
        Log.d(TAG, " parseJwsJOSE :: X509_CERTIFICATES not initialised ");
        return false;
    }


    JWSObject jwsObject = null;

    SignedJWT signedJwt = null;

    boolean isVerified = false;

    try {
        signedJwt = SignedJWT.parse(responseString);

        RSAPublicKey rsaPublicKey = (RSAPublicKey) X509_CERTIFICATES.get(0).getPublicKey();

        JWSVerifier jwsVerifier = new RSASSAVerifier(rsaPublicKey);

        isVerified = signedJwt.verify(jwsVerifier);

    } catch (java.text.ParseException e) {
        // Invalid JWS object encoding
        return false;
    }catch (JOSEException e) {
        // Invalid JWS object encoding
        e.printStackTrace();
        return  false;
    }


    if(isVerified){
        Log.d(TAG, " parseJws:: JWS signature is valid");
    }else{
        Log.d(TAG, " parseJws :: JWS signature failed validation discard this token");
    }

    return isVerified;

}`

Hey @khalid64927,

Without diving too far into the code snippit I just wanted to ask some quick questions/give some comments:

  1. It seems like you are only doing leaf and signature validation, have you thought about doing the cert chain validation as well?
  2. For validating the signature, you need to take a few things into account:
  3. The algorithm to use (which comes from the header portion of the JWS (header_info['alg']). For example: "RS256".
  4. The data you are signing is the first 2 piece of the JWS (.)
  5. The signature you are checking against is the 3rd part of the JWS
  6. You should be using the leaf cert for your validation.

One thing that is helpful is parsing the response string into their respective 3 pieces so you can work with them separately as needed.

I hope this was helpful. If not, I can dive more into the code snippits you posted. I used python for my implementation, just as an FYI.

Hi @robsmall

  1. I'm not doing the cert chain validation yet for that I suppose simply check for public keys in cert chain, but This will be problem in case google update the certs in their server, usually future keys should be added in app before updating on server.
  2. Yes that is the process and it is implemented by the library you can look into it.
    As for the key used in signature validation in snippet it is leaf cert (X5O9.CERTIFICATES.get(0) returns leaf cert ).

For cross verification you can try manipulating the payload/header data part the Signatures wouldn't match.

@khalid64927

I'm not doing the cert chain validation yet for that I suppose simply check for public keys in cert chain, but This will be problem in case google update the certs in their server, usually future keys should be added in app before updating on server.

I don't think you should run into any issues here since this is all included in the JWS. They actually just updated their cert a few days ago and it did not cause any issues.

Yes that is the process and it is implemented by the library you can look into it.
As for the key used in signature validation in snippet it is leaf cert (X5O9.CERTIFICATES.get(0) returns leaf cert).

That makes sense, but when you call SignedJWT.parse(responseString); what are you getting back that you assign to signedJwt? What you want to be validating is everything up until the second '.' from the JWS, where the JWS is:

BASE64URL(UTF8(JWS Protected Header)) || '.' ||
      BASE64URL(JWS Payload) || '.' ||
      BASE64URL(JWS Signature)

My apologies if I am ignorant to the inner workings of the library you are using, I should be able to spend some more time looking at this over the weekend if you are still in need.

For more info on JWS: https://tools.ietf.org/html/draft-ietf-jose-json-web-signature-36

If anybody requires the host validation in PHP follow this URL https://github.com/phpseclib/phpseclib, get headers from the JWS which consists of two certificates in x5c get the 0th element and load in to X509 class and get the dnProp which should be equal to attest.android.com

If someone figured out my app's apkCertificateDigestSha256 would that be a disaster or no big deal?

@bob-seeger I believe that the SHA-256 hash of any apps signing cert can be obtained by running keytool -list -printcert -jarfile /path/to/file.apk.

I don't see any issue with someone getting their hands on the hash for your cert since they should not be able to create an app signed by said cert (if they do, then you do have a problem!) so they will not be able to get a valid JWS blob containing said apkCertificateDigestSha256 value. Please reach out if you would like more of an explanation on this.

@robsmall My only pitfall is what you initially bulleted at the top of this forum:

  1. Extract the SSL certificate chain from the JWS message.

  2. Validate the SSL certificate chain and use SSL hostname matching to verify that the leaf certificate was issued to the hostname attest.android.com.

  3. Use the certificate to verify the signature of the JWS message.

All of which I'm not currently doing because the server code samples provided by the Google employees are only in Java and C++ which makes me extremely angry because my back end is pure PHP.

@ikoz in this forum provided a link to his Github with some PHP however his code doesn't actually do any of the SSL stuff you bullet pointed at the top of this forum. And @ikoz must have missed the part of the article where they explicitly say not to verify the JWS response with Google in a production scenario.

I (and the world) need a PHP translation of the following

https://github.com/googlesamples/android-play-safetynet/blob/master/server/java/src/main/java/OfflineVerify.java

@robsmall It's been a while, but did you manage to find any solutions for PHP. I am in a similar situation.