From 9960c974fbed98abba826d8490b5e02d03c55a66 Mon Sep 17 00:00:00 2001 From: Mario Perrotta Date: Wed, 29 May 2024 14:27:41 +0200 Subject: [PATCH] feat: add swiftCBOR (#10) --- backend/src/index.ts | 2 +- backend/src/verifyAssertion.ts | 25 ++++------- example/ios/Podfile.lock | 8 +++- example/src/IosApp.tsx | 28 +++++++----- ios/IoReactNativeIntegrity.mm | 16 ++++--- ios/IoReactNativeIntegrity.swift | 54 ++++++++++++++++++++++++ pagopa-io-react-native-integrity.podspec | 5 ++- src/index.tsx | 16 ++++++- 8 files changed, 115 insertions(+), 39 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index ae74741..ed416bb 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -102,7 +102,7 @@ app.post(`/assertion/verify`, (req, res) => { } const result = verifyAssertion({ - assertion: assertion, + assertion: Buffer.from(assertion, 'base64').toString('utf-8'), payload: req.body.payload, publicKey: attestation.publicKey, bundleIdentifier: BUNDLE_IDENTIFIER, diff --git a/backend/src/verifyAssertion.ts b/backend/src/verifyAssertion.ts index 1d869b5..e9482e9 100644 --- a/backend/src/verifyAssertion.ts +++ b/backend/src/verifyAssertion.ts @@ -1,4 +1,3 @@ -import * as cbor from 'cbor'; import { createHash, createVerify } from 'crypto'; export type VerifyAssertionParams = { @@ -45,32 +44,24 @@ const verifyAssertion = (params: VerifyAssertionParams) => { throw new Error('publicKey is required'); } - // 1. Decode the assertion from base64 to string - let decodedAssertion; - try { - const decodedAssertionString = Buffer.from(assertion, 'base64').toString( - 'hex' - ); - decodedAssertion = cbor.decodeAllSync(decodedAssertionString)[0]; - } catch (e) { - throw new Error('invalid assertion'); - } - - const { signature, authenticatorData } = decodedAssertion; + // 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); // 2. Compute the SHA256 hash of the client data, and store it as clientDataHash. const clientDataHash = createHash('sha256').update(payload).digest(); // 3. Compute the SHA256 hash of the concatenation of the authenticator // data and the client data hash, and store it as nonce. const nonce = createHash('sha256') - .update(Buffer.concat([authenticatorData, clientDataHash])) + .update(Buffer.concat([authData, clientDataHash])) .digest(); // 4. Verify the signature using the public key and nonce. const verifier = createVerify('SHA256'); verifier.update(nonce); - if (!verifier.verify(publicKey, signature)) { + if (!verifier.verify(publicKey, sign)) { throw new Error('invalid signature'); } @@ -78,14 +69,14 @@ const verifyAssertion = (params: VerifyAssertionParams) => { const appIdHash = createHash('sha256') .update(`${teamIdentifier}.${bundleIdentifier}`) .digest('base64'); - const rpiIdHash = authenticatorData.subarray(0, 32).toString('base64'); + const rpiIdHash = authData.subarray(0, 32).toString('base64'); if (appIdHash !== rpiIdHash) { throw new Error('appId does not match'); } // 6. Verify that the authenticator data’s counter field is larger than the stored signCount. - const nextSignCount = authenticatorData.subarray(33, 37).readInt32BE(); + const nextSignCount = authData.subarray(33, 37).readInt32BE(); if (nextSignCount <= signCount) { throw new Error('invalid signCount'); } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index b31184f..323124b 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -77,6 +77,7 @@ PODS: - glog - RCT-Folly (= 2022.05.16.00) - React-Core + - SwiftCBOR - RCT-Folly (2022.05.16.00): - boost - DoubleConversion @@ -1118,6 +1119,7 @@ PODS: - RNSha256 (1.4.10): - React-Core - SocketRocket (0.6.1) + - SwiftCBOR (0.4.7) - Yoga (1.14.0) DEPENDENCIES: @@ -1212,6 +1214,7 @@ SPEC REPOS: - libevent - OpenSSL-Universal - SocketRocket + - SwiftCBOR EXTERNAL SOURCES: boost: @@ -1335,7 +1338,7 @@ SPEC CHECKSUMS: hermes-engine: 9cecf9953a681df7556b8cc9c74905de8f3293c0 libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - pagopa-io-react-native-integrity: dc1de6afe65dc17df6974a5bdfa68cf354a0b5a5 + pagopa-io-react-native-integrity: 08fadabd6e676e1108a2e2ac0c633ba5cb67f150 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCTRequired: ca1d7414aba0b27efcfa2ccd37637edb1ab77d96 RCTTypeSafety: 678e344fb976ff98343ca61dc62e151f3a042292 @@ -1379,8 +1382,9 @@ SPEC CHECKSUMS: ReactCommon: 2aa35648354bd4c4665b9a5084a7d37097b89c10 RNSha256: e1bc64e9e50b293d5282bb4caa1b2043931f1c9d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 + SwiftCBOR: 465775bed0e8bac7bfb8160bcf7b95d7f75971e4 Yoga: 805bf71192903b20fc14babe48080582fee65a80 PODFILE CHECKSUM: fd9c38ef526dee311429a7a1ecc7347bb228ae7f -COCOAPODS: 1.15.2 +COCOAPODS: 1.14.3 diff --git a/example/src/IosApp.tsx b/example/src/IosApp.tsx index 4de411a..b61cfe6 100644 --- a/example/src/IosApp.tsx +++ b/example/src/IosApp.tsx @@ -5,6 +5,7 @@ import { getAttestation, isAttestationServiceAvailable, generateHardwareSignatureWithAssertion, + decodeAssertion, type IntegrityError, } from '@pagopa/io-react-native-integrity'; import { BACKEND_ADDRESS } from '@env'; @@ -107,17 +108,22 @@ export default function App() { }; const verifyAssertion = async () => { - // verify attestation on the server with POST - // and body of challenge, attestation and keyId - - const result = await postRequest('assertion/verify', { - challenge: challenge, - assertion: assertion, - hardwareKeyTag: hardwareKeyTag, - payload: JSON.stringify({ challenge: challenge, jwk: jwk }), - }); - const response = await result.json(); - setDebugLog(JSON.stringify(response)); + 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, + assertion: decodedAssertion, + hardwareKeyTag: hardwareKeyTag, + payload: JSON.stringify({ challenge: challenge, jwk: jwk }), + }); + const response = await result.json(); + setDebugLog(JSON.stringify(response)); + } else { + setDebugLog('No assertion to verify'); + } }; const getHardwareSignatureWithAssertion = async () => { diff --git a/ios/IoReactNativeIntegrity.mm b/ios/IoReactNativeIntegrity.mm index 87baeaf..4a93688 100644 --- a/ios/IoReactNativeIntegrity.mm +++ b/ios/IoReactNativeIntegrity.mm @@ -3,18 +3,22 @@ @interface RCT_EXTERN_MODULE(IoReactNativeIntegrity, NSObject) RCT_EXTERN_METHOD(isAttestationServiceAvailable:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) + withRejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(generateHardwareKey:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) + withRejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(getAttestation:(NSString)challenge withHardwareKeyTag:(NSString)hardwareKeyTag - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) RCT_EXTERN_METHOD(generateHardwareSignatureWithAssertion:(NSString)clientData withHardwareKeyTag:(NSString)hardwareKeyTag - withResolver:(RCTPromiseResolveBlock)resolve - withRejecter:(RCTPromiseRejectBlock)reject) + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + +RCT_EXTERN_METHOD(decodeAssertion:(NSString)assertion + withResolver:(RCTPromiseResolveBlock)resolve + withRejecter:(RCTPromiseRejectBlock)reject) + (BOOL)requiresMainQueueSetup { diff --git a/ios/IoReactNativeIntegrity.swift b/ios/IoReactNativeIntegrity.swift index 48eb79b..fe78fe2 100644 --- a/ios/IoReactNativeIntegrity.swift +++ b/ios/IoReactNativeIntegrity.swift @@ -1,11 +1,62 @@ import DeviceCheck import CryptoKit +import SwiftCBOR /// A class to interact with the DeviceCheck and CryptoKit frameworks to ensure the integrity of the device and app. @objc(IoReactNativeIntegrity) class IoReactNativeIntegrity: NSObject { private typealias ME = ModuleException + struct DecodedData: Codable { + var signature: [UInt8] + var authenticatorData: [UInt8] + } + + enum CBORDecodingError: Error { + case invalidFormat(String) + } + + /// Decodes a base64 encoded CBOR assertion into a JSON object. + /// - Parameters: + /// - assertion: The CBOR assertion to decode. + /// - Returns: A JSON object representing the decoded assertion. + @objc(decodeAssertion:withResolver:withRejecter:) + func decodeAssertion( + assertion: String, + resolve: @escaping RCTPromiseResolveBlock, + reject: @escaping RCTPromiseRejectBlock + ) -> Void { + // Attempt to decode the CBOR data + do { + // Convert base64 encoded assertion string to Data + guard let data = Data(base64Encoded: assertion) else { + throw CBORDecodingError.invalidFormat("Expected base64 endoded string") + } + + let cborData = [UInt8](data) + let decodedValue = try CBOR.decode(cborData) + + // Ensure the decoded value is a map + guard case let .map(map) = decodedValue else { + throw CBORDecodingError.invalidFormat("Expected CBOR map") + } + + // Extract the signature and authenticatorData from the map + guard case let .byteString(signatureBytes)? = map["signature"], + 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) + + resolve(jsonData.base64EncodedString()) + } catch { + ME.decodingAssertionFailed.reject(reject: reject, ("error", error.localizedDescription)) + } + } + /// Determines if the DeviceCheck App Attestation Service is available on the device. /// - Parameters: /// - resolve: The promise resolve block to call with the result. @@ -170,6 +221,7 @@ class IoReactNativeIntegrity: NSObject { case challengeError = "CHALLANGE_ERROR" case clientDataEncodingError = "CLIENT_DATA_ENCODING_ERROR" case generationAssertionFailed = "GENERATION_ASSERTION_FAILED" + case decodingAssertionFailed = "DECODING_ASSERTION_FAILED" func error(userInfo: [String : Any]? = nil) -> NSError { switch self { @@ -187,6 +239,8 @@ class IoReactNativeIntegrity: NSObject { return NSError(domain: self.rawValue, code: -1, userInfo: userInfo) case .generationAssertionFailed: return NSError(domain: self.rawValue, code: -1, userInfo: userInfo) + case .decodingAssertionFailed: + return NSError(domain: self.rawValue, code: -1, userInfo: userInfo) } } diff --git a/pagopa-io-react-native-integrity.podspec b/pagopa-io-react-native-integrity.podspec index 4878d83..fec4369 100644 --- a/pagopa-io-react-native-integrity.podspec +++ b/pagopa-io-react-native-integrity.podspec @@ -37,5 +37,8 @@ Pod::Spec.new do |s| s.dependency "RCTTypeSafety" s.dependency "ReactCommon/turbomodule/core" end - end + end + + # Other dependencies + s.dependency "SwiftCBOR" end diff --git a/src/index.tsx b/src/index.tsx index 59b8d4b..4c2705e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -24,7 +24,8 @@ type IntegrityErrorCodesIOS = | 'UNSUPPORTED_IOS_VERSION' | 'CHALLANGE_ERROR' | 'CLIENT_DATA_ENCODING_ERROR' - | 'GENERATION_ASSERTION_FAILED'; + | 'GENERATION_ASSERTION_FAILED' + | 'DECODING_ASSERTION_FAILED'; type IntegrityErrorCodesCommon = 'PLATFORM_NOT_SUPPORTED'; @@ -149,6 +150,19 @@ export function generateHardwareSignatureWithAssertion( })(); } +/** + * This function decodes a CBOR encoded iOS assertion. + * + * If it is not possible to decode the assertion, the promise is rejected providing an + * instance of {@link IntegrityError}. + * + * @param assertion - the CBOR assertion to be decoded + * @returns - a promise that resolves to a string. + */ +export function decodeAssertion(assertion: string): Promise { + return IoReactNativeIntegrity.decodeAssertion(assertion); +} + /** * ANDROID ONLY * Checks whether Google Play Services is available on the device or not.