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: [SIW-1043] Add CBOR decoding assertion function on iOS side #10

Merged
merged 6 commits into from
May 29, 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
2 changes: 1 addition & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 8 additions & 17 deletions backend/src/verifyAssertion.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as cbor from 'cbor';
import { createHash, createVerify } from 'crypto';

export type VerifyAssertionParams = {
Expand Down Expand Up @@ -45,47 +44,39 @@ 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');
}

// 5. 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.
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');
}
Expand Down
8 changes: 6 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ PODS:
- glog
- RCT-Folly (= 2022.05.16.00)
- React-Core
- SwiftCBOR
- RCT-Folly (2022.05.16.00):
- boost
- DoubleConversion
Expand Down Expand Up @@ -1118,6 +1119,7 @@ PODS:
- RNSha256 (1.4.10):
- React-Core
- SocketRocket (0.6.1)
- SwiftCBOR (0.4.7)
- Yoga (1.14.0)

DEPENDENCIES:
Expand Down Expand Up @@ -1212,6 +1214,7 @@ SPEC REPOS:
- libevent
- OpenSSL-Universal
- SocketRocket
- SwiftCBOR

EXTERNAL SOURCES:
boost:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
28 changes: 17 additions & 11 deletions example/src/IosApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
getAttestation,
isAttestationServiceAvailable,
generateHardwareSignatureWithAssertion,
decodeAssertion,
type IntegrityError,
} from '@pagopa/io-react-native-integrity';
import { BACKEND_ADDRESS } from '@env';
Expand Down Expand Up @@ -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 () => {
Expand Down
16 changes: 10 additions & 6 deletions ios/IoReactNativeIntegrity.mm
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
54 changes: 54 additions & 0 deletions ios/IoReactNativeIntegrity.swift
Original file line number Diff line number Diff line change
@@ -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")
LazyAfternoons marked this conversation as resolved.
Show resolved Hide resolved
}

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.
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
}
}

Expand Down
5 changes: 4 additions & 1 deletion pagopa-io-react-native-integrity.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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
16 changes: 15 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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<string> {
return IoReactNativeIntegrity.decodeAssertion(assertion);
}

/**
* ANDROID ONLY
* Checks whether Google Play Services is available on the device or not.
Expand Down
Loading