This is a Proof of Concept (PoC) of a ACA-py controller for mobile application attestation. It has the following features:
- Apple App Attestation
- Android Play Integrity API
- Apple Fraud Detection API
- AppStore Receipt Checking (iOS <14.0)
While this controller can be run as a controller for any ACA-py instance, it was developed using "Traction" as the front end. Any documentation or references should be considered in that context.
- VSCode
- Docker
- Traction >= 0.3.2
- Suitable tool for exposing localhost to the internet:
When run, this program will act as a "controller" to an ACA-py agent. It uses Flux to handle DidComm basic messages, and, when prompted, will use Traction to issue a basic demo Attestation Credential.
First, create a .env
file in the root of your folder by copying env.sample
to .env
, populate the values with your own. For Android Attestation you will need a Google OAuth JSON key in /src
configured for your app.
You will also need to create a schema and credential definition id in your Traction instance and then add it to fixtures/offer.json
, following the format.
For simplicity, this repo comes with a .devContainer
to allow developers to get up-and-running quickly. Use VSCode to restart or start the container. Once the container is running, you'll need to assign redis clusters with the following command run from redis-1 (can be accessed from Docker desktop):
redis-cli --cluster create redis-1:6379 redis-2:6379 redis-3:6379 --cluster-replicas 0
then you can start the controller with the following command:
python src/controller.py
The output should look something like this:
vscode ➜ /work (main) $ python src/controller.py
* Serving Flask app 'controller'
* Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
* Running on http://127.0.0.1:5000
Press CTRL+C to quit
* Restarting with stat
* Debugger is active!
* Debugger PIN: 107-923-082
Flask will expose port 5000
and this is where you need to point whatever tool you have setup to expose localhost, for example, in the case of ngrok
:
npx ngrok http 5000
Finally, whatever public endpoint is provided from, in this case ngrok
needs to be provided to Traction as the controller endpoint. This can be done by going to Settings -> Tenant Profile and entering the URL in the WebHook URL
field.
The general command to deploy this to an OpenShift cluster is:
helm template <RELEASE> ./devops/charts/controller
-f ./devops/charts/controller/values_<ENVIRONMENT>.yaml
--set-string tenant_id=<TENANT_ID> \
--set-string tenant_api_key=<TENANT_API_KEY> \
--set-string traction_legacy_did=<TRACTION_LEGACY_DID> \
--set-file google_oauth_key.json=<PATH_TO_GOOGLE_OAUTH_KEY>| \
oc apply -n <NAMESPACE> -f -
And example command to deploy to the e79518-dev
namespace is:
helm template bcwallet ./devops/charts/controller
-f ./devops/charts/controller/values_dev.yaml
--set-string tenant_id="$TRACTION_TENANT_ID"
--set-string tenant_api_key="$TRACTION_TENANT_API_KEY"
--set-string traction_legacy_did="$TRACTION_LEGACY_DID"
--set-file google_oauth_key.json=./google_oauth_key.json|
oc apply -n e79518-dev -f -
The release name can be anything you want, but it must be unique to the namespace. When deploying to a shared namespace like e79518-dev
, it is recommended use the a meaningful release name that will help reason about what the controller is doing.
If you are deploying to a namespace that already has a controller, you can set your environment variables with the following helpful commands:
export TRACTION_TENANT_API_KEY=$(oc get secret/bcwallet-attestation-controller-traction-creds -o json| jq -r ".data.TRACTION_TENANT_API_KEY"|base64 -d) && \
echo $TRACTION_TENANT_API_KEY
export TRACTION_TENANT_ID=$(oc get secret/bcwallet-attestation-controller-traction-creds -o json| jq -r ".data.TRACTION_TENANT_ID"|base64 -d) && \
echo $TRACTION_TENANT_ID
export TRACTION_LEGACY_DID=$(oc get secret/bcwallet-attestation-controller-traction-creds -o json| jq -r ".data.TRACTION_LEGACY_DID"|base64 -d) && \
echo $TRACTION_LEGACY_DID
https://developer.android.com/google/play/integrity/verdicts#device-integrity-field. You can then distinguish between MEETS_BASIC_INTEGRITY and MEETS_STRONG_INTEGRITY
Here are some interesting packages that may be useful:
https://github.com/invertase/react-native-firebase/tree/main#readme
https://www.npmjs.com/package/@react-native-firebase/app-check
npm i -S @react-native-firebase/app-check
https://github.com/kedros-as/react-native-google-play-integrity
https://www.npmjs.com/package/react-native-google-play-integrity
npm i -S react-native-google-play-integrity
https://github.com/bpofficial/expo-attestation#readme
https://www.npmjs.com/package/expo-attestation
npm i -S expo-attestation
jq --arg content "$(cat fixtures/request_issuance.json | base64)" --arg name "jason" '.content |= $content | .name |= $name' fixtures/basic_message.json
jq --arg content "$(cat fixtures/request_issuance.json | base64)" '.content |= $content' fixtures/basic_message.json|curl -v -X POST -H "Content-Type: application/json" -d @- http://localhost:5000/topic/basicmessages/
jq -s '.[0] * .[1]' source.json target.json
jq --arg content "$(jq -s '.[0] * .[1]' fixtures/chalange_response.json attestation.json | base64)" '.content |= $content' fixtures/basic_message.json| curl -v -X POST -H "Content-Type: application/json" -d @- http://localhost:5000/topic/basicmessages/
-
Use the decoded object, along with the key identifier that your app sends, to perform the following steps:
-
Verify that the x5c array contains the intermediate and leaf certificates for App Attest, starting from the credential certificate in the first data buffer in the array (credcert). Verify the validity of the certificates using Apple’s App Attest root certificate.
-
Create clientDataHash as the SHA256 hash of the one-time challenge your server sends to your app before performing the attestation, and append that hash to the end of the authenticator data (authData from the decoded object).
-
Generate a new SHA256 hash of the composite item to create nonce.
-
Obtain the value of the credCert extension with OID 1.2.840.113635.100.8.2, which is a DER-encoded ASN.1 sequence. Decode the sequence and extract the single octet string that it contains. Verify that the string equals nonce.
-
Create the SHA256 hash of the public key in credCert, and verify that it matches the key identifier from your app.
-
Compute the SHA256 hash of your app’s App ID, and verify that it’s the same as the authenticator data’s RP ID hash.
-
Verify that the authenticator data’s counter field equals 0.
-
Verify that the authenticator data’s aaguid field is either appattestdevelop if operating in the development environment, or appattest followed by seven 0x00 bytes if operating in the production environment.
-
Verify that the authenticator data’s credentialId field is the same as the key identifier.
After successfully completing these steps, you can trust the attestation object.
- Get Integrity Verdict from Google's server via their python client
- Verify the package info matches our app
- Verify the device integrity fields
- Verify the app integrity fields
- Verify the nonce in the verdict payload matches the nonce the controller sent to the device