Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Migrate from safetynet to playintegrity (VO-208) #1438

Merged
merged 2 commits into from
Jan 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/cozy-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@
"cozy-ui": ">=93.1.1",
"react": "^16.7.0",
"react-native": "^0.65.0",
"react-native-google-safetynet": "npm:cozy-react-native-google-safetynet@^1.0.0",
"react-native-inappbrowser-reborn": "^3.5.1",
"react-native-ios11-devicecheck": "https://github.com/cozy/react-native-devicecheck#app-attest-v0.1"
"react-native-ios11-devicecheck": "https://github.com/cozy/react-native-devicecheck#app-attest-v0.1",
"react-native-google-play-integrity": "github:cozy/react-native-google-play-integrity#1.0.1"
},
"sideEffects": false
}
27 changes: 11 additions & 16 deletions packages/cozy-client/src/flagship-certification/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,27 @@ This verification is done by querying an app certificate from the app store (App
- `attestation`: result from the `store certification`
- `challenge`: unique token given to the app by `cozy-stack` that may be encrypted in the `attestation` as a proof of authenticity
- `nonce`: data type used to store the `challenge` token
- `SafetyNet`: Google's implementation of the `store certification`
- `Play Integrity API`: Google's implementation of the `store certification`
- `AppAttest`: Apple's implementation of the `store certification`

## Android certification

Android certification is based on [SafetyNet](https://developer.android.com/training/safetynet/index.html).
Android certification is based on [Play Integrity API](https://developer.android.com/google/play/integrity/overview).

This process requires to query a `challenge` from `cozy-stack` and to use it to init the `store certification` process through `SafetyNet`. Then the received `attestation` is send to `cozy-stack` for verification.
This process requires to query a `challenge` from `cozy-stack` and to use it to init the `store certification` process through `Play Integrity API`. Then the received `attestation` is send to `cozy-stack` for verification.

The resulting `attestation` is in the form of a `JSON Web Signature` that embbed the following `JSON`:
```json
```js
{
"apkCertificateDigestSha256": [
"base64 encoded, SHA-256 hash of the certificate used to sign requesting app="
],
"apkDigestSha256": "kNv83tJLFqwliYQ/6HUPCeGkBzLLCX/nvT+EF3OEB2I=",
"apkPackageName": "com.package.name.of.requesting.app",
"basicIntegrity": true,
"ctsProfileMatch": true,
"evaluationType": "BASIC",
"nonce": "R2Rra24fVm5xa2Mg",
"timestampMs": 9860437986543
requestDetails: { ... }
appIntegrity: { ... }
deviceIntegrity: { ... }
accountDetails: { ... }
environmentDetails: { ... }
}
```

The `attestation`'s content is described in the SafetyNet's documentation: https://developer.android.com/training/safetynet/attestation#use-response-server
The `attestation`'s content is described in the Play Integrity API's documentation: https://developer.android.com/google/play/integrity/verdicts#returned-verdict-format

## iOS certification

Expand Down Expand Up @@ -65,7 +60,7 @@ const client = await initClient(uri, {
clientName: 'YOUR_APP_NAME',
shouldRequireFlagshipPermissions: true,
certificationConfig: {
androidSafetyNetApiKey: 'YOUR_GOOGLE_SAFETY_NET_API_KEY'
cloudProjectNumber: 'YOUR_CLOUD_PROJECT_NUMBER'
}
},
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const getStackChallenge = async client => {
*/
const giveAppAttestationToStack = async (appAttestation, nonce, client) => {
try {
const { platform, attestation, keyId } = appAttestation
const { platform, attestation, keyId, issuer } = appAttestation

const stackClient = client.getStackClient()

Expand All @@ -51,7 +51,8 @@ const giveAppAttestationToStack = async (appAttestation, nonce, client) => {
platform: platform,
attestation: attestation,
challenge: nonce,
keyId: keyId
keyId: keyId,
issuer: issuer
},
{
headers: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const mockCorrectStoreApiRequest = () => {

const mockCorrectCertificationConfig = () => {
return {
androidSafetyNetApiKey: 'SOME_ANDROID_SAFETY_NET_API_KEY'
cloudProjectNumber: 'SOME_CLOUD_PROJECT_NUMBER'
}
}

Expand Down Expand Up @@ -90,7 +90,7 @@ describe('certifyFlagship', () => {
)

expect(getAppAttestationFromStore).toHaveBeenCalledWith('SOME_NONCE', {
androidSafetyNetApiKey: 'SOME_ANDROID_SAFETY_NET_API_KEY'
cloudProjectNumber: 'SOME_CLOUD_PROJECT_NUMBER'
})

expect(client.stackClient.fetchJSON).toHaveBeenCalledWith(
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('certifyFlagship', () => {
)

expect(getAppAttestationFromStore).toHaveBeenCalledWith('SOME_NONCE', {
androidSafetyNetApiKey: 'SOME_ANDROID_SAFETY_NET_API_KEY'
cloudProjectNumber: 'SOME_CLOUD_PROJECT_NUMBER'
})

expect(console.warn).toHaveBeenCalledWith(
Expand Down Expand Up @@ -201,7 +201,7 @@ describe('certifyFlagship', () => {
)

expect(getAppAttestationFromStore).toHaveBeenCalledWith('SOME_NONCE', {
androidSafetyNetApiKey: 'SOME_ANDROID_SAFETY_NET_API_KEY'
cloudProjectNumber: 'SOME_CLOUD_PROJECT_NUMBER'
})

expect(client.stackClient.fetchJSON).toHaveBeenCalledWith(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//@ts-ignore next-line
import RNGoogleSafetyNet from 'react-native-google-safetynet'
//@ts-ignore next-line - this is a react-native module that is not installed in the monorepo, only in the consumer app
import PlayIntegrity from 'react-native-google-play-integrity'

/**
* Retrieve the app's attestation from the Google Play store
Expand All @@ -13,18 +13,19 @@ export const getAppAttestationFromStore = async (
certificationConfig
) => {
try {
const attestationResult = await RNGoogleSafetyNet.sendAttestationRequestJWT(
const integrityToken = await PlayIntegrity.requestIntegrityToken(
nonce,
certificationConfig.androidSafetyNetApiKey
certificationConfig.cloudProjectNumber
)

return {
platform: 'android',
attestation: attestationResult
attestation: integrityToken,
issuer: 'playintegrity'
}
} catch (e) {
throw new Error(
'[FLAGSHIP_CERTIFICATION] Something went wrong while requesting an attestation from Google Safetynet:\n' +
'[FLAGSHIP_CERTIFICATION] Something went wrong while requesting an attestation from Google Play Integrity API:\n' +
e.message
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
* @property {string} platform
* @property {jws|base64string} attestation
* @property {string} [keyId]
* @property {string} [issuer]
*/

/**
* @typedef {object} CertificationConfig - Configuration to access the stores certification API
* @property {string} androidSafetyNetApiKey
* @property {string} cloudProjectNumber
*/

export default {}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ export type AttestationResult = {
platform: string;
attestation: jws | base64string;
keyId?: string;
issuer?: string;
};
/**
* - Configuration to access the stores certification API
*/
export type CertificationConfig = {
androidSafetyNetApiKey: string;
cloudProjectNumber: string;
};
Loading