From ef2d6db86d26c579ea161f722e3d155d9f8cd9db Mon Sep 17 00:00:00 2001 From: Mario Perrotta Date: Thu, 7 Mar 2024 18:35:35 +0100 Subject: [PATCH 01/64] feat: add ios app attestation --- .gitignore | 1 + .../project.pbxproj | 32 +++- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + example/ios/Podfile.lock | 10 +- example/package.json | 4 +- example/src/App.tsx | 143 +++++++++++++++-- example/src/components/ButtonWithLoader.tsx | 40 +++++ ios/IntegrityError.swift | 54 +++++++ ios/IoReactNativeIntegrity.mm | 12 +- ios/IoReactNativeIntegrity.swift | 146 +++++++++++++++++- pagopa-io-react-native-integrity.podspec | 2 +- src/__tests__/index.test.tsx | 63 +++++++- src/index.tsx | 25 ++- yarn.lock | 9 ++ 14 files changed, 519 insertions(+), 30 deletions(-) create mode 100644 example/ios/IoReactNativeIntegrityExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/src/components/ButtonWithLoader.tsx create mode 100644 ios/IntegrityError.swift diff --git a/.gitignore b/.gitignore index d3b53df..f559641 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ DerivedData *.ipa *.xcuserstate project.xcworkspace +.xcode.env.local # Android/IJ # diff --git a/example/ios/IoReactNativeIntegrityExample.xcodeproj/project.pbxproj b/example/ios/IoReactNativeIntegrityExample.xcodeproj/project.pbxproj index c1e3241..2d61c3b 100644 --- a/example/ios/IoReactNativeIntegrityExample.xcodeproj/project.pbxproj +++ b/example/ios/IoReactNativeIntegrityExample.xcodeproj/project.pbxproj @@ -413,12 +413,13 @@ baseConfigurationReference = 5B7EB9410499542E8C5724F5 /* Pods-IoReactNativeIntegrityExample-IoReactNativeIntegrityExampleTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; + DEVELOPMENT_TEAM = M2X5YQ4BJ7; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); INFOPLIST_FILE = IoReactNativeIntegrityExampleTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -441,8 +442,9 @@ buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; COPY_PHASE_STRIP = NO; + DEVELOPMENT_TEAM = M2X5YQ4BJ7; INFOPLIST_FILE = IoReactNativeIntegrityExampleTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -466,8 +468,10 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = M2X5YQ4BJ7; ENABLE_BITCODE = NO; INFOPLIST_FILE = IoReactNativeIntegrityExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -493,7 +497,9 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = M2X5YQ4BJ7; INFOPLIST_FILE = IoReactNativeIntegrityExample/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -544,7 +550,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -560,7 +566,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", @@ -572,6 +578,7 @@ ); MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", @@ -579,7 +586,13 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + USE_HERMES = true; }; name = Debug; }; @@ -616,7 +629,7 @@ COPY_PHASE_STRIP = YES; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; - "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = ""; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = i386; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -625,7 +638,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.4; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = ( /usr/lib/swift, "$(inherited)", @@ -636,6 +649,7 @@ "\"$(inherited)\"", ); MTL_ENABLE_DEBUG_INFO = NO; + OTHER_CFLAGS = "$(inherited)"; OTHER_CPLUSPLUSFLAGS = ( "$(OTHER_CFLAGS)", "-DFOLLY_NO_CONFIG", @@ -643,7 +657,13 @@ "-DFOLLY_USE_LIBCPP=1", "-DFOLLY_CFG_NO_COROUTINES=1", ); + OTHER_LDFLAGS = ( + "$(inherited)", + " ", + ); + REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + USE_HERMES = true; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/example/ios/IoReactNativeIntegrityExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/IoReactNativeIntegrityExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/IoReactNativeIntegrityExample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 5f68d5a..404bd9d 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1115,6 +1115,8 @@ PODS: - React-jsi (= 0.73.5) - React-logger (= 0.73.5) - React-perflogger (= 0.73.5) + - RNSha256 (1.4.10): + - React-Core - SocketRocket (0.6.1) - Yoga (1.14.0) @@ -1192,6 +1194,7 @@ DEPENDENCIES: - React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`) - React-utils (from `../node_modules/react-native/ReactCommon/react/utils`) - ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`) + - RNSha256 (from `../node_modules/react-native-sha256`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -1308,6 +1311,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon/react/utils" ReactCommon: :path: "../node_modules/react-native/ReactCommon" + RNSha256: + :path: "../node_modules/react-native-sha256" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -1330,7 +1335,7 @@ SPEC CHECKSUMS: hermes-engine: 1d1835b2cc54c381909d94d1b3c8e0a2f1a94a0e libevent: 4049cae6c81cdb3654a443be001fb9bdceff7913 OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c - pagopa-io-react-native-integrity: 429be694f02e0b01ad2a707938edac3de34a7110 + pagopa-io-react-native-integrity: f6f51e45065ccc7e3116be86069ff04c7cfe1be7 RCT-Folly: 7169b2b1c44399c76a47b5deaaba715eeeb476c0 RCTRequired: 2544c0f1081a5fa12e108bb8cb40e5f4581ccd87 RCTTypeSafety: 50efabe2b115c11ed03fbf3fd79e2f163ddb5d7c @@ -1372,8 +1377,9 @@ SPEC CHECKSUMS: React-runtimescheduler: 814b644a5f456c7df1fba7bcd9914707152527c6 React-utils: 987a4526a2fc0acdfaf87888adfe0bf9d0452066 ReactCommon: 2947b0bffd82ea0e58ca7928881152d4c6dae9af + RNSha256: e1bc64e9e50b293d5282bb4caa1b2043931f1c9d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - Yoga: 9e6a04eacbd94f97d94577017e9f23b3ab41cf6c + Yoga: a716eea57d0d3430219c0a5a233e1e93ee931eb7 PODFILE CHECKSUM: fd9c38ef526dee311429a7a1ecc7347bb228ae7f diff --git a/example/package.json b/example/package.json index 0e7a370..7dc435f 100644 --- a/example/package.json +++ b/example/package.json @@ -10,8 +10,10 @@ "build:ios": "cd ios && xcodebuild -workspace IoReactNativeIntegrityExample.xcworkspace -scheme IoReactNativeIntegrityExample -configuration Debug -sdk iphonesimulator CC=clang CPLUSPLUS=clang++ LD=clang LDPLUSPLUS=clang++ GCC_OPTIMIZATION_LEVEL=0 GCC_PRECOMPILE_PREFIX_HEADER=YES ASSETCATALOG_COMPILER_OPTIMIZATION=time DEBUG_INFORMATION_FORMAT=dwarf COMPILER_INDEX_STORE_ENABLE=NO" }, "dependencies": { + "buffer": "^6.0.3", "react": "18.2.0", - "react-native": "0.73.5" + "react-native": "0.73.5", + "react-native-sha256": "^1.4.10" }, "devDependencies": { "@babel/core": "^7.20.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index 71a5d0f..537208e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,19 +1,130 @@ import * as React from 'react'; +import { Buffer } from 'buffer'; +import { sha256 } from 'react-native-sha256'; +import { StyleSheet, SafeAreaView, Text, ScrollView } from 'react-native'; +import { + generateHardwareKey, + getAttestation, + isAttestationServiceAvailable, + generateHardwareSignatureWithAssertion, +} from '@pagopa/io-react-native-integrity'; +import ButtonWithLoader from './components/ButtonWithLoader'; -import { StyleSheet, View, Text } from 'react-native'; -import { multiply } from '@pagopa/io-react-native-integrity'; +const challenge = 'challengeFromServer'; export default function App() { - const [result, setResult] = React.useState(); + const [hardwareKeyTag, setHardwareKeyTag] = React.useState< + string | undefined + >(''); + const [attestation, setAttestation] = React.useState(''); + const [decodedAttestation, setDecodedAttestation] = React.useState< + string | undefined + >(''); + const [isServiceAvailable, setIsServiceAvailable] = + React.useState(false); + const [debugLog, setDebugLog] = React.useState('.. >'); React.useEffect(() => { - multiply(3, 7).then(setResult); + isAttestationServiceAvailable() + .then((result) => { + setIsServiceAvailable(result); + }) + .catch((error) => { + setDebugLog(error); + }); }, []); + const getHardwareKey = async () => { + if (hardwareKeyTag === '' || hardwareKeyTag === undefined) { + setHardwareKeyTag(undefined); + const hardwareKey = await generateHardwareKey(); + setHardwareKeyTag(hardwareKey); + setDebugLog(hardwareKey); + } else { + setDebugLog(hardwareKeyTag); + } + }; + + const requestAttestation = async () => { + setAttestation(undefined); + if (hardwareKeyTag) { + const result = await getAttestation(challenge, hardwareKeyTag); + setAttestation(result); + setDebugLog(result); + } + }; + + const decodeAttestation = () => { + // decode attestation from base64 to string + setDecodedAttestation(undefined); + if (attestation) { + const attestationDecoded = Buffer.from(attestation, 'base64').toString( + 'hex' + ); + setDecodedAttestation(attestationDecoded); + setDebugLog(attestationDecoded); + } + }; + + const getHardwareSignatureWithAssertion = async () => { + // this is a mocked jwk for ephimeral public key, in a production + // environment the ephimeral key must be generated by the client + // every time a WTE must be required + const jwk = { + crv: 'P-256', + kty: 'EC', + x: '4HNptI-xr2pjyRJKGMnz4WmdnQD_uJSq4R95Nj98b44', + y: 'LIZnSB39vFJhYgS3k7jXE4r3-CoGFQwZtPBIRqpNlrg', + kid: 'vbeXJksM45xphtANnCiG6mCyuU4jfGNzopGuKvogg9c', + }; + + const clientData = { + challenge: challenge, + jwk: jwk, + }; + + if (hardwareKeyTag) { + const clientDataHash = await sha256(JSON.stringify(clientData)); + + const result = await generateHardwareSignatureWithAssertion( + clientDataHash, + hardwareKeyTag + ); + setDebugLog(result); + } + }; + return ( - - Result: {result} - + + Integrity Check Demo App + {isServiceAvailable ? ( + <> + getHardwareKey()} + loading={hardwareKeyTag === undefined} + /> + requestAttestation()} + loading={attestation === undefined} + /> + decodeAttestation()} + loading={decodedAttestation === undefined} + /> + getHardwareSignatureWithAssertion()} + loading={decodedAttestation === undefined} + /> + + ) : null} + + {debugLog} + + ); } @@ -21,11 +132,19 @@ const styles = StyleSheet.create({ container: { flex: 1, alignItems: 'center', - justifyContent: 'center', }, - box: { - width: 60, - height: 60, - marginVertical: 20, + h1: { + fontWeight: 'bold', + fontSize: 32, + textAlign: 'center', + marginTop: 50, + marginBottom: 50, + }, + debug: { + width: '100%', + height: 300, + position: 'absolute', + bottom: 0, + backgroundColor: '#eaeaea', }, }); diff --git a/example/src/components/ButtonWithLoader.tsx b/example/src/components/ButtonWithLoader.tsx new file mode 100644 index 0000000..26a65ef --- /dev/null +++ b/example/src/components/ButtonWithLoader.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { ActivityIndicator, Button, StyleSheet, View } from 'react-native'; + +type Props = { + /** + * The title of the button + */ + title: string; + /** + * The action to perform when the button is pressed + */ + onPress: () => void; + /** + * Whether the button is in a loading state + */ + loading?: boolean; +}; + +/** + * A button component with a loader that can be shown when the button is in a loading state. + * The loader is a spinner showed at the right of the button title. + */ +const ButtonWithLoader = (props: Props) => { + return ( + +