Skip to content

Commit

Permalink
feat: add swiftCBOR (#10)
Browse files Browse the repository at this point in the history
  • Loading branch information
hevelius authored May 29, 2024
1 parent 8be92e9 commit 9960c97
Show file tree
Hide file tree
Showing 8 changed files with 115 additions and 39 deletions.
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")
}

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

0 comments on commit 9960c97

Please sign in to comment.