[Huawei Developers] ask011:
Overview

In the previous post, we learned how to integrate the UserDetect API in Safety Detect into your app to identify fake users. I've been glad to receive so much positive feedback. In this post, I'll introduce SysIntegrity (system integrity check), yet another handy function in Safety Detect. Apps can face malicious attacks when they run on insecure devices, for example, rooted devices. For certain apps, such as e-commerce apps, ensuring a secure running environment during user transactions is particularly crucial. SysIntegrity enables apps to do so by helping detect device system-related risks in advance.

I encountered this function on the HUAWEI Developers website. SysIntegrity works by integrating the Safety Detect SDK into your app, and calling the SysIntegrity API in Safety Detect. It performs the check within a Trusted Execution Environment (TEE) and signs the check result with an X.509 digital certificate to ensure that it is trustworthy and tamperproof.

Now let's take a look at what it's like in practice.

Next, I'll show you how to integrate SysIntegrity.

Content

1 Preparations

1.1 Installing Android studio

1.2 Configuring App Information in AppGallery Connect

1.3 Configuring the Huawei Maven Repository Address

1.4 Adding Build Dependencies

1.5 Configuring Obfuscation Scripts

2 Code Development

2.1 Creating a SafetyDetectClient Instance and Generating a Nonce

2.2 Calling the SysIntegrity API

2.3 Verifying the Check Result on the App Server

2.4 Obtaining the System Integrity Check Result

1. Preparations

1.1 Installing Android Studio

To install Android Studio, please refer to the following:

l Android Studio Official Website

l Android Studio Installation and Configuration

1.2 Configuring App Information in AppGallery Connect

Before developing an app, you'll need to configure the app information in AppGallery Connect. For more details, please refer to Preparations.

1.3 Configuring the Huawei Maven Repository Address

Open the build.gradle file in the root directory of your Android Studio project.

Add the AppGallery Connect plug-in and the Maven repository address.

(1) Go to buildscript > repositories and configure the Maven repository address for the HMS Core SDK.

(2) Go to allprojects > repositories and configure the Maven repository address for the HMS Core SDK.

(3) If the agconnect-services.json file has been added to the app, go to buildscript > dependencies and add the AppGallery Connect plug-in configuration.

<p style="line-height: 1.5em;">apply plugin: 'com.huawei.agconnect'
</p>

Add the build dependency in the dependencies section.

<p style="line-height: 1.5em;">dependencies {
implementation 'com.huawei.hms:safetydetect:5.0.5.302'
}
</p>

1.5 Configuring Obfuscation Scripts

If you are using AndResGuard, add its trustlist to the build.gradle file in the app directory of your project. For more details about the code, please refer to Configuring Obfuscation Scripts on the HUAWEI Developers website.

2. Code Development

2.1 Creating a SafetyDetectClient Instance and Generating a Nonce

The nonce value will be contained in the check result. You can check the nonce value to verify that the returned result corresponds to your request, and does not encounter replay attacks. A nonce value should:

l Be used only once.

l Contain 16 to 66 bytes.

l Be derived from data sent to your server (Recommended).

2.2 Calling the SysIntegrity API

(1) The SysIntegrity API has two input parameters: the nonce value (obtained during the previous step) and your app ID. You can obtain the app ID from the agconnect-services.jsonfile under the app directory of your project.

Ø Sign in to AppGallery Connect and click My projects.

Ø Find your app project and click the desired app name.

Ø Go to Project Setting > General information, and view the app ID in the App information area.

<p style="line-height: 1.5em;">private void onAdapterItemClick(int position) {
// Call the SysIntegrity API to check for risks in the payment environment.
SafetyDetectUtil.detectSysIntegrity(this, new ICallBack<Boolean>() {
@Override
public void onSuccess(Boolean baseIntegrity) {
if (baseIntegrity) {
// The system integrity is not corrupted, and the user can proceed with the purchase.
buy(productInfo);
} else {
// The system integrity is corrupted. A popup is displayed to warn the user and ask the user whether to continue.
showRootTipDialog(productInfo);
}
}
…
});
}
</p>

(3) In my app, I encapsulated the SysIntegrity API in the detectSysIntegrity method of the SafetyDetectUtil.java class. The sample code is as follows:

<p style="line-height: 1.5em;">public static void detectSysIntegrity(final Activity activity, final ICallBack<? super Boolean> callBack) {
// Generate a nonce value.
byte[] nonce = ("Sample" + System.currentTimeMillis()).getBytes(StandardCharsets.UTF_8);
// Read the app_id field from the agconnect-services.json file in the app directory.
String appId = AGConnectServicesConfig.fromContext(activity).getString("client/app_id");
// Obtain the Safety Detect client, call the SysIntegrity API, and add a success event listener.
SysIntegrityRequest sysintegrityrequest = new SysIntegrityRequest();
sysintegrityrequest.setAppid(appId);
sysintegrityrequest.setNonce(nonce);
// PS256 or RS256
sysintegrityrequest.setAlg("RS256");
Task task = mClient.sysIntegrity(sysintegrityrequest);
task.addOnSuccessListener(new OnSuccessListener<SysIntegrityResp>() {
@Override
public void onSuccess(SysIntegrityResp response) {
// Call the getResult method of the SysIntegrityResp class to obtain the check result.
String jwsStr = response.getResult();
VerifyResultHandler verifyResultHandler = new VerifyResultHandler(jwsStr, callBack);
// Send the check result to your server for verification.
verifyJws(activity, jwsStr, verifyResultHandler);
}
});
}
</p>

(4) Here, I called the relevant app server API in the verifyJws method to verify the check result. The third parameter in this method is an object of the VerifyResultHandler class, which implements a callback API to process the verification result.

2.3 Verifying the Check Result on the App Server

After receiving the check result from the TSMS server, the app will send the result to the app server. The server uses the HUAWEI CBG Root CA certificate to verify the signature and certificate chain in the check result, and thereby determines whether the check result is valid.

The sample code for the app server to read the certificate and verify the JWS string is as follows:

(1) Parse the header, payload, and signature from the JWS string.

<p style="line-height: 1.5em;">public JwsVerifyResp verifyJws(JwsVerifyReq jwsVerifyReq) {
// Obtain the JWS information sent from the app to the server.
String jwsStr = jwsVerifyReq.getJws();
// Parse the JWS segments. A JWS has three fixed segments, which are separated by periods (.).
String[] jwsSplit = jwsStr.split("\\.");
try {
// Perform Base64 decoding on each segment and construct a JWS object for each decoded segment.
JWSObject jwsObject = new JWSObject(new Base64URL(jwsSplit[0]), new Base64URL(jwsSplit[1]), new Base64URL(jwsSplit[2]));
// Verify the JWS and set the verification result.
boolean result = VerifySignatureUtil.verifySignature(jwsObject);
// Construct the response body for check result verification on the app server.
JwsVerifyResp jwsVerifyResp = new JwsVerifyResp();
jwsVerifyResp.setResult(result);
} catch (ParseException | NoSuchAlgorithmException e) {
RUN_LOG.catching(e);
}
return jwsVerifyResp;
}
</p>

(2) Use the verifySignature method of the VerifySignatureUtil class to verify relevant information, including the JWS signature algorithm, certificate chain, host name in signing certificate, and JWS signature. The sample code is as follows:

<p style="line-height: 1.5em;">public static boolean verifySignature(JWSObject jws) throws NoSuchAlgorithmException {
JWSAlgorithm jwsAlgorithm = jws.getHeader().getAlgorithm();
// 1. Verify the JWS signature algorithm.
if ("RS256".equals(jwsAlgorithm.getName())) {
// Verify the certificate chain and obtain an instance of the Signature class based on the signature algorithm to verify the signature.
return verify(Signature.getInstance("SHA256withRSA"), jws);
}
return false;
}
private static boolean verify(Signature signature, JWSObject jws) {
// Extract the certificate chain information from the JWS header and convert the certificate chain into a proper type for subsequent processing.
X509Certificate[] certs = extractX509CertChain(jws);
// 2. Verify the certificate chain.
try {
verifyCertChain(certs);
} catch (Exception e) {
return false;
}
// 3. Verify the domain name in the signing certificate (leaf certificate). The domain name must be sysintegrity.platform.hicloud.com.
try {
new DefaultHostnameVerifier().verify("sysintegrity.platform.hicloud.com", certs[0]);
} catch (SSLException e) {
return false;
}
// 4. Verify the JWS signature information using the public key obtained from the signing certificate.
PublicKey pubKey = certs[0].getPublicKey();
try {
// Use the public key obtained from the signing certificate to initialize the Signature instance.
signature.initVerify(pubKey);
// Extract the input signature from the JWS and pass it to the Signature instance.
signature.update(jws.getSigningInput());
// Use the Signature instance to verify the signature information.
return signature.verify(jws.getSignature().decode());
} catch (InvalidKeyException | SignatureException e) {
return false;
}
}
</p>

(3) Call the extractX509CertChain method to extract the certificate chain from the JWS header. The sample code is as follows:

<p style="line-height: 1.5em;">private static X509Certificate[] extractX509CertChain(JWSObject jws) {
List<X509Certificate> certs = new ArrayList<>();
List<com.nimbusds.jose.util.Base64> x509CertChain = jws.getHeader().getX509CertChain();
try {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
certs.addAll(x509CertChain.stream().map(cert -> {
try {
return (X509Certificate) certFactory.generateCertificate( new ByteArrayInputStream(cert.decode()) );
} catch (CertificateException e) {
RUN_LOG.error("X5c extract failed!");
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toList()));
} catch (CertificateException e) {
RUN_LOG.error("X5c extract failed!");
}
return (X509Certificate[]) certs.toArray();
}
</p>

(4) Call the verifyCertChain method to verify the certificate chain. The sample code is as follows:

<p style="line-height: 1.5em;">private static void verifyCertChain(X509Certificate[] certs) throws CertificateException, NoSuchAlgorithmException,
InvalidKeyException, NoSuchProviderException, SignatureException {
// Verify the validity period and issuing relationship of each certificate one by one.
for (int i = 0; i < certs.length - 1; ++i) {
certs[i].checkValidity();
PublicKey pubKey = certs[i + 1].getPublicKey();
certs[i].verify(pubKey);
}
// Use the preset HUAWEI CBG Root CA certificate to verify the last certificate in the certificate chain.
PublicKey caPubKey = huaweiCbgRootCaCert.getPublicKey();
certs[certs.length - 1].verify(caPubKey);
}
</p>

(5) Load the HUAWEI CBG Root CA certificate in the static code snippet of the VerifySignatureUtil class. The sample code is as follows:

<p style="line-height: 1.5em;">static {
// Load the preset HUAWEI CBG Root CA certificate.
File filepath = "~/certs/Huawei_cbg_root.cer";
try (FileInputStream in = new FileInputStream(filepath)) {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
huaweiCbgRootCaCert = (X509Certificate) cf.generateCertificate(in);
} catch (IOException | CertificateException e) {
RUN_LOG.error("HUAWEI CBG root cert load failed!");
}
}
</p>

We have now verified the check result on the app server, and the verification result will be returned to the app for subsequent service processing.

2.4 Obtaining the System Integrity Check Result

(1) Once you complete the steps above, the app will obtain the reliable system integrity check result from the payload of the JWS string. Parse the system integrity check result from the callback API of the VerifyResultHandler class as follows:

<p style="line-height: 1.5em;">private static final class VerifyResultHandler implements ICallBack<Boolean> {
private final String jwsStr;
private final ICallBack<? super Boolean> callBack;
private VerifyResultHandler(String jwsStr, ICallBack<? super Boolean> callBack) {
this.jwsStr = jwsStr;
this.callBack = callBack;
}

@Override
public void onSuccess(Boolean verified) {
if (verified) {
// Extract the system integrity check result that has been successfully verified by the app server.
String payloadDetail = new String(Base64.decode(jwsStr.split("\\.")[1].getBytes(StandardCharsets.UTF_8), Base64.URL_SAFE), StandardCharsets.UTF_8);
try {
final boolean basicIntegrity = new JSONObject(payloadDetail).getBoolean("basicIntegrity");
// Call back the system integrity check result.
callBack.onSuccess(basicIntegrity);
} catch (JSONException e) {
…
}
}
…
}
}
</p>

(2) The following is an example of the system integrity check response:

<p style="line-height: 1.5em;">{
"apkCertificateDigestSha256": [
"osaUtTsdAvezjQBaW3IhN3/fsc6NQ5KwKuAQXcfrxb4="
],
"apkDigestSha256": "vFcmE0uw5s+4tFjXF9rVycxk2xR1rXiZFHuuBFzTVy8=",
"apkPackageName": "com.example.mockthirdapp",
"basicIntegrity": false,
"detail": [
"root",
"unlocked"
],
"nonce": "UjJScmEyNGZWbTV4YTJNZw==",
"timestampMs": 1604048377137,
"advice": "RESTORE_TO_FACTORY_ROM"
}
</p>

(3) If the value of the basicIntegrity field in the check result is false, it means that the system integrity is corrupted. In this case, the app can notify the user of any potential risks.

Conclusion

You can complete the integration by referring to the Preparations on HUAWEI Developers website. Also, you can download the SysIntegrity sample code for both Java and Kotlin from the site. And the sample code for four other functions in addition to SysIntegrity is also provided on the site.

Here is the sample code I wrote for my app. Feel free to take a look for yourself!

My sample code