diff --git a/README.md b/README.md index 4b3f891..2e908c5 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,6 @@ A (Key Attestation)[https://developer.android.com/privacy-and-security/security- During key attestation, a key pair is generated along with its certificate chain hich can be used to verify the properties of that key pair. If the device supports hardware-level key attestation, the root certificate of the chain is signed using an attestation root key protected by the device's hardware-backed keystore. - ### `isPlayServicesAvailable` Returns a boolean value indicating whether the Play Services are available on the device or not. @@ -64,6 +63,7 @@ try { Requests an integrity token which is then attached to the request to be protected. It must be called AFTER `prepareIntegrityToken` has been called and resolved successfully. +The token is a base64 encoded string. ```ts try { @@ -140,6 +140,20 @@ try { } ``` +### `decodeAttestation` + +Decodes the CBOR encoded attestation returned by the `getAttestation` method. Returns an object containing `signature` and `authenticatorData` as base64 encoded strings. + +```ts +try { + const decoded = await decodeAttestation(attestation); + console.log(decoded); +} catch (e) { + const error = e as IntegrityError; + console.log(JSON.stringify(error)); +} +``` + ## Types | TypeName | Description | @@ -176,3 +190,7 @@ MIT --- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob) + +``` + +``` diff --git a/backend/src/index.ts b/backend/src/index.ts index ed416bb..c9b670a 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -87,9 +87,9 @@ app.post(`/attest/verify`, (req, res) => { */ app.post(`/assertion/verify`, (req, res) => { try { - const { hardwareKeyTag, assertion } = req.body; + const { hardwareKeyTag } = req.body; - if (hardwareKeyTag === undefined || assertion === undefined) { + if (hardwareKeyTag === undefined) { throw new Error('Invalid authentication'); } @@ -102,7 +102,8 @@ app.post(`/assertion/verify`, (req, res) => { } const result = verifyAssertion({ - assertion: Buffer.from(assertion, 'base64').toString('utf-8'), + signature: req.body.signature, + authenticatorData: req.body.authenticatorData, payload: req.body.payload, publicKey: attestation.publicKey, bundleIdentifier: BUNDLE_IDENTIFIER, diff --git a/backend/src/verifyAssertion.ts b/backend/src/verifyAssertion.ts index e9482e9..041711e 100644 --- a/backend/src/verifyAssertion.ts +++ b/backend/src/verifyAssertion.ts @@ -1,7 +1,8 @@ import { createHash, createVerify } from 'crypto'; export type VerifyAssertionParams = { - assertion: string; + signature: string; + authenticatorData: string; payload: Buffer; publicKey: string; bundleIdentifier: string; @@ -16,7 +17,8 @@ export type VerifyAssertionParams = { */ const verifyAssertion = (params: VerifyAssertionParams) => { const { - assertion, + signature, + authenticatorData, payload, publicKey, bundleIdentifier, @@ -32,8 +34,12 @@ const verifyAssertion = (params: VerifyAssertionParams) => { throw new Error('teamIdentifier is required'); } - if (!assertion) { - throw new Error('assertion is required'); + if (!signature) { + throw new Error('signature is required'); + } + + if (!authenticatorData) { + throw new Error('authenticatorData is required'); } if (!payload) { @@ -44,11 +50,8 @@ const verifyAssertion = (params: VerifyAssertionParams) => { throw new Error('publicKey is required'); } - // 1. Get signature and authenticator data from the assertion. - const { signature, authenticatorData } = JSON.parse(assertion); - - const sign = Buffer.from(signature); - const authData = Buffer.from(authenticatorData); + const sign = Buffer.from(signature, 'base64'); + const authData = Buffer.from(authenticatorData, 'base64'); // 2. Compute the SHA256 hash of the client data, and store it as clientDataHash. const clientDataHash = createHash('sha256').update(payload).digest(); diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 323124b..bb2bd6e 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -73,7 +73,7 @@ PODS: - hermes-engine/Pre-built (0.73.6) - libevent (2.1.12) - OpenSSL-Universal (1.1.1100) - - pagopa-io-react-native-integrity (0.1.0): + - pagopa-io-react-native-integrity (0.2.0): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1338,7 +1338,7 @@ SPEC CHECKSUMS: hermes-engine: 9cecf9953a681df7556b8cc9c74905de8f3293c0 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - pagopa-io-react-native-integrity: 08fadabd6e676e1108a2e2ac0c633ba5cb67f150 + pagopa-io-react-native-integrity: 70f8efc528e6855889233c8dd8d2c14ccb531089 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCTRequired: ca1d7414aba0b27efcfa2ccd37637edb1ab77d96 RCTTypeSafety: 678e344fb976ff98343ca61dc62e151f3a042292 diff --git a/example/src/IosApp.tsx b/example/src/IosApp.tsx index b61cfe6..0529402 100644 --- a/example/src/IosApp.tsx +++ b/example/src/IosApp.tsx @@ -47,10 +47,14 @@ export default function App() { }, []); const getHardwareKey = async () => { - setHardwareKeyTag(undefined); - const hardwareKey = await generateHardwareKey(); - setHardwareKeyTag(hardwareKey); - setDebugLog(hardwareKey); + try { + setHardwareKeyTag(undefined); + const hardwareKey = await generateHardwareKey(); + setHardwareKeyTag(hardwareKey); + setDebugLog(hardwareKey); + } catch (e) { + setDebugLog(`${e}`); + } }; const postRequest = async (endpoint: string, body: object) => { @@ -68,81 +72,88 @@ export default function App() { const getChallengeFromServer = async () => { // contact server run on local at port 3000 to get // the challenge to be used for the attestation - try { - const response = await fetch(`${BACKEND_ADDRESS}/attest/nonce`); - const result = await response.json(); - return result.nonce; - } catch { - return ''; - } + const response = await fetch(`${BACKEND_ADDRESS}/attest/nonce`); + const result = await response.json(); + return result.nonce; }; const requestAttestation = async () => { - setAttestation(undefined); - if (hardwareKeyTag) { - // get challenge from server - const nonce = await getChallengeFromServer(); - setChallenge(nonce); - getAttestation(nonce, hardwareKeyTag) - .then((result) => { - setAttestation(result); - setDebugLog(result); - }) - .catch((error: IntegrityError) => { - setDebugLog(error.message + ':' + JSON.stringify(error.userInfo)); - setAttestation(''); - }); + try { + setAttestation(undefined); + if (hardwareKeyTag) { + // get challenge from server + const nonce = await getChallengeFromServer(); + setChallenge(nonce); + const res = await getAttestation(nonce, hardwareKeyTag); + setAttestation(res); + setDebugLog(res); + } + } catch (e) { + setDebugLog(`${e}`); } }; const verifyAttestation = async () => { - // verify attestation on the server with POST - // and body of challenge, attestation and keyId - const result = await postRequest('attest/verify', { - challenge: challenge, - attestation: attestation, - hardwareKeyTag: hardwareKeyTag, - }); - const response = await result.json(); - setDebugLog(JSON.stringify(response)); - }; - - const verifyAssertion = async () => { - if (assertion) { - // decode CBOR assertion on native side (iOS only) - const decodedAssertion = await decodeAssertion(assertion); + try { // verify attestation on the server with POST // and body of challenge, attestation and keyId - const result = await postRequest('assertion/verify', { + const result = await postRequest('attest/verify', { challenge: challenge, - assertion: decodedAssertion, + attestation: attestation, hardwareKeyTag: hardwareKeyTag, - payload: JSON.stringify({ challenge: challenge, jwk: jwk }), }); const response = await result.json(); setDebugLog(JSON.stringify(response)); - } else { - setDebugLog('No assertion to verify'); + } catch (e) { + setDebugLog(`${e}`); + } + }; + + const verifyAssertion = async () => { + try { + if (assertion) { + // decode CBOR assertion on native side (iOS only) + const decodedAssertion = await decodeAssertion(assertion); + // verify attestation on the server with POST + // and body of challenge, attestation and keyId + const result = await postRequest('assertion/verify', { + challenge: challenge, + signature: decodedAssertion.signature, + authenticatorData: decodedAssertion.authenticatorData, + hardwareKeyTag: hardwareKeyTag, + payload: JSON.stringify({ challenge: challenge, jwk: jwk }), + }); + const response = await result.json(); + setDebugLog(JSON.stringify(response)); + } else { + setDebugLog('No assertion to verify'); + } + } catch (e) { + setDebugLog(`${e}`); } }; const getHardwareSignatureWithAssertion = async () => { - const clientData = { - challenge: challenge, - jwk: jwk, - }; - // Between Android and iOS there is a difference for the generation of hardwareSignature - // and assertion as on iOS both are generated directly from the SDK via generateAssertion - // while on Android the hardwareSignature must be generated via the signature functions and - // the assertion must be retrieved from the backend via an integrityToken. - if (hardwareKeyTag) { - const result = await generateHardwareSignatureWithAssertion( - JSON.stringify(clientData), - hardwareKeyTag - ); - setHsWithAssertion(result); - setDebugLog(result); - setAssertion(result); + try { + const clientData = { + challenge: challenge, + jwk: jwk, + }; + // Between Android and iOS there is a difference for the generation of hardwareSignature + // and assertion as on iOS both are generated directly from the SDK via generateAssertion + // while on Android the hardwareSignature must be generated via the signature functions and + // the assertion must be retrieved from the backend via an integrityToken. + if (hardwareKeyTag) { + const result = await generateHardwareSignatureWithAssertion( + JSON.stringify(clientData), + hardwareKeyTag + ); + setHsWithAssertion(result); + setDebugLog(result); + setAssertion(result); + } + } catch (e) { + setDebugLog(`${e}`); } }; diff --git a/ios/IoReactNativeIntegrity.swift b/ios/IoReactNativeIntegrity.swift index fe78fe2..7d99068 100644 --- a/ios/IoReactNativeIntegrity.swift +++ b/ios/IoReactNativeIntegrity.swift @@ -7,11 +7,6 @@ import SwiftCBOR class IoReactNativeIntegrity: NSObject { private typealias ME = ModuleException - struct DecodedData: Codable { - var signature: [UInt8] - var authenticatorData: [UInt8] - } - enum CBORDecodingError: Error { case invalidFormat(String) } @@ -46,12 +41,13 @@ class IoReactNativeIntegrity: NSObject { case let .byteString(authenticatorDataBytes)? = map["authenticatorData"] else { throw CBORDecodingError.invalidFormat("Expected signature and authenticatorData in the CBOR map") } - - let decodedData = DecodedData(signature: signatureBytes, authenticatorData: authenticatorDataBytes) - let jsonData = try JSONEncoder().encode(decodedData) + let decodedData: [String:String] = [ + "signature": Data(signatureBytes).base64EncodedString(), + "authenticatorData": Data(authenticatorDataBytes).base64EncodedString() + ] - resolve(jsonData.base64EncodedString()) + resolve(decodedData) } catch { ME.decodingAssertionFailed.reject(reject: reject, ("error", error.localizedDescription)) } diff --git a/src/index.tsx b/src/index.tsx index 4c2705e..63a0b8a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -45,6 +45,15 @@ export type IntegrityError = { userInfo: Record; }; +/** + * Type of the decoded attestation. + * Both signature and authenticatorData are base64 encoded. + */ +export type DecodedAttestation = { + signature: string; + authenticatorData: string; +}; + /** * Error when the platform is not supported. */ @@ -157,9 +166,11 @@ export function generateHardwareSignatureWithAssertion( * instance of {@link IntegrityError}. * * @param assertion - the CBOR assertion to be decoded - * @returns - a promise that resolves to a string. + * @returns - a promise that resolves to a {@link DecodedAttestation} which contains base64 encoded signature and authenticatorData. */ -export function decodeAssertion(assertion: string): Promise { +export function decodeAssertion( + assertion: string +): Promise { return IoReactNativeIntegrity.decodeAssertion(assertion); } @@ -204,7 +215,7 @@ export function prepareIntegrityToken( * Integrity token request step for a [Play Integrity standard API request](https://developer.android.com/google/play/integrity/standard). * It requests an integrity token which is then attached to the request to be protected. * It should be called AFTER {@link prepareIntegrityToken} has been called and resolved successfully. - * The React Native + * The token is a base64 encoded string. * @param requestHash a digest of all relevant request parameters (e.g. SHA256) from the user action or server request that is happening. * The max size of this field is 500 bytes. Do not put sensitive information as plain text in this field. * @returns a resolved promise with with the token as payload, rejected otherwise when: