From e4a6c871380d2fe3bd2483520ff6d5a7877c4fb0 Mon Sep 17 00:00:00 2001 From: James Chien Date: Tue, 21 Nov 2023 22:41:13 +0800 Subject: [PATCH] fix: WIP: signature generation FIXME: potential extra CR character \r prependded before newline in signed_metadata FIXME: hardcoded CameraSource.Camera FIXME: Proof tests --- src/app/shared/collector/collector.service.ts | 36 +++++++++------ ...pto-api-signature-provider.service.spec.ts | 12 ++--- ...b-crypto-api-signature-provider.service.ts | 16 +++---- .../dia-backend-asset-repository.service.ts | 24 ++++++---- .../shared/repositories/proof/proof.spec.ts | 12 ++--- src/app/shared/repositories/proof/proof.ts | 44 ++++++++++++------- src/app/utils/nit/nit.ts | 13 +++--- 7 files changed, 92 insertions(+), 65 deletions(-) diff --git a/src/app/shared/collector/collector.service.ts b/src/app/shared/collector/collector.service.ts index a22f2a822..c257f535e 100644 --- a/src/app/shared/collector/collector.service.ts +++ b/src/app/shared/collector/collector.service.ts @@ -1,18 +1,18 @@ import { Injectable } from '@angular/core'; import { CameraSource } from '@capacitor/camera'; +import { generateIntegritySha } from '../../utils/nit/nit'; import { MediaStore } from '../media/media-store/media-store.service'; import { Assets, - getSerializedSortedSignedMessage, Proof, - Signatures, - SignedMessage, + ProofMetadata, + SignResult, Truth, + getSerializedSortedProofMetadata, } from '../repositories/proof/proof'; import { FactsProvider } from './facts/facts-provider'; import { CaptureAppWebCryptoApiSignatureProvider } from './signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { SignatureProvider } from './signature/signature-provider'; -import { generateIntegritySha } from '../../utils/nit/nit'; @Injectable({ providedIn: 'root', @@ -34,10 +34,18 @@ export class CollectorService { async generateSignature(proof: Proof, source: CameraSource) { const recorder = CaptureAppWebCryptoApiSignatureProvider.recorderFor(source); - const signedMessage = await proof.generateSignedMessage(recorder); - const signatures = await this.signMessage(signedMessage, source); + const proofMetadata = await proof.generateProofMetadata(recorder); + const { signatures, integritySha } = await this.signProofMetadata( + proofMetadata, + source + ); + console.log( + 'sign message ProofMetadata', + await getSerializedSortedProofMetadata(proofMetadata) + ); + console.log('generated signatures', signatures); + console.log('generated integritySha', integritySha); proof.setSignatures(signatures); - const integritySha = await generateIntegritySha(signedMessage); proof.setIntegritySha(integritySha); return proof; } @@ -59,20 +67,20 @@ export class CollectorService { }; } - private async signMessage( - message: SignedMessage, + private async signProofMetadata( + proofMetadata: ProofMetadata, source: CameraSource - ): Promise { - const serializedSortedSignedMessage = - getSerializedSortedSignedMessage(message); - return Object.fromEntries( + ): Promise { + const integritySha = await generateIntegritySha(proofMetadata); + const signatures = Object.fromEntries( await Promise.all( [...this.signatureProviders].map(async provider => [ provider.idFor(source), - await provider.provide(serializedSortedSignedMessage), + await provider.provide(integritySha), ]) ) ); + return { signatures, integritySha }; } addFactsProvider(provider: FactsProvider) { diff --git a/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts index 0c1effc84..85ec76823 100644 --- a/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts +++ b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.spec.ts @@ -3,9 +3,9 @@ import { defer } from 'rxjs'; import { concatMapTo } from 'rxjs/operators'; import { sortObjectDeeplyByKey } from '../../../../utils/immutable/immutable'; import { - isSignature, + ProofMetadata, RecorderType, - SignedMessage, + isSignature, } from '../../../repositories/proof/proof'; import { SharedTestingModule } from '../../../shared-testing.module'; import { CaptureAppWebCryptoApiSignatureProvider } from './capture-app-web-crypto-api-signature-provider.service'; @@ -60,7 +60,7 @@ describe('CaptureAppWebCryptoApiSignatureProvider', () => { }); it('should provide signature', async () => { - const signedMessage: SignedMessage = { + const ProofMetadata: ProofMetadata = { spec_version: '', recorder: RecorderType.Capture, created_at: 0, @@ -69,10 +69,10 @@ describe('CaptureAppWebCryptoApiSignatureProvider', () => { caption: '', information: {}, }; - const serializedSortedSignedMessage = JSON.stringify( - sortObjectDeeplyByKey(signedMessage as any).toJSON() + const serializedSortedProofMetadata = JSON.stringify( + sortObjectDeeplyByKey(ProofMetadata as any).toJSON() ); - const signature = await provider.provide(serializedSortedSignedMessage); + const signature = await provider.provide(serializedSortedProofMetadata); expect(isSignature(signature)).toBeTrue(); }); diff --git a/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts index 34e18e27c..654ca6e23 100644 --- a/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts +++ b/src/app/shared/collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service.ts @@ -1,9 +1,7 @@ import { Injectable } from '@angular/core'; import { CameraSource } from '@capacitor/camera'; -import { - createEthAccount, - loadEthAccount, -} from '../../../../utils/crypto/crypto'; +import { createEthAccount } from '../../../../utils/crypto/crypto'; +import { signWithIntegritySha } from '../../../../utils/nit/nit'; import { PreferenceManager } from '../../../preference-manager/preference-manager.service'; import { RecorderType, Signature } from '../../../repositories/proof/proof'; import { SignatureProvider } from '../signature-provider'; @@ -72,12 +70,14 @@ export class CaptureAppWebCryptoApiSignatureProvider } } - async provide(serializedSortedSignedTargets: string): Promise { + async provide(signMessage: string): Promise { await this.initialize(); - const account = loadEthAccount(await this.getPrivateKey()); - const sign = account.sign(serializedSortedSignedTargets); + const signature = await signWithIntegritySha( + signMessage, + await this.getPrivateKey() + ); const publicKey = await this.getPublicKey(); - return { signature: sign.signature, publicKey }; + return { signature, publicKey }; } async getPublicKey() { diff --git a/src/app/shared/dia-backend/asset/dia-backend-asset-repository.service.ts b/src/app/shared/dia-backend/asset/dia-backend-asset-repository.service.ts index 97d48a0ce..5465b249f 100644 --- a/src/app/shared/dia-backend/asset/dia-backend-asset-repository.service.ts +++ b/src/app/shared/dia-backend/asset/dia-backend-asset-repository.service.ts @@ -4,6 +4,7 @@ import { HttpParams, } from '@angular/common/http'; import { Injectable } from '@angular/core'; +import { CameraSource } from '@capacitor/camera'; import { BehaviorSubject, ReplaySubject, @@ -28,6 +29,7 @@ import { import { base64ToBlob } from '../../../utils/encoding/encoding'; import { MimeType, toExtension } from '../../../utils/mime-type'; import { VOID$, isNonNullable } from '../../../utils/rx-operators/rx-operators'; +import { CaptureAppWebCryptoApiSignatureProvider } from '../../collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { Tuple } from '../../database/table/table'; import { OldSignature, @@ -38,7 +40,7 @@ import { } from '../../repositories/proof/old-proof-adapter'; import { Proof, - getSerializedSortedSignedMessage, + getSerializedSortedProofMetadata, } from '../../repositories/proof/proof'; import { DiaBackendAuthService } from '../auth/dia-backend-auth.service'; import { PaginatedResponse } from '../pagination'; @@ -386,11 +388,14 @@ async function buildFormDataToCreateAsset(proof: Proof) { const formData = new FormData(); const info = await getSortedProofInformation(proof); - const signedMessage = await proof.generateSignedMessage(); - const serializedSignedMessage = - getSerializedSortedSignedMessage(signedMessage); + const recorder = CaptureAppWebCryptoApiSignatureProvider.recorderFor( + CameraSource.Camera // FIXME: should read actual CameraSource + ); + const proofMetadata = await proof.generateProofMetadata(recorder); + const serializedSortedProofMetadata = + getSerializedSortedProofMetadata(proofMetadata); formData.set('meta', JSON.stringify(info)); - formData.set('signed_metadata', serializedSignedMessage); + formData.set('signed_metadata', serializedSortedProofMetadata); formData.set('signature', JSON.stringify(getOldSignatures(proof))); const fileBase64 = Object.keys(await proof.getAssets())[0]; @@ -408,10 +413,11 @@ async function buildFormDataToCreateAsset(proof: Proof) { async function buildFormDataToUpdateSignature(proof: Proof) { const formData = new FormData(); - const signedMessage = await proof.generateSignedMessage(); - const serializedSignedMessage = - getSerializedSortedSignedMessage(signedMessage); - formData.set('signed_metadata', serializedSignedMessage); + + const ProofMetadata = await proof.generateProofMetadata(); + const serializedSortedProofMetadata = + getSerializedSortedProofMetadata(ProofMetadata); + formData.set('signed_metadata', serializedSortedProofMetadata); formData.set('signature', JSON.stringify(getOldSignatures(proof))); return formData; } diff --git a/src/app/shared/repositories/proof/proof.spec.ts b/src/app/shared/repositories/proof/proof.spec.ts index 5491fd248..b7f9d63e3 100644 --- a/src/app/shared/repositories/proof/proof.spec.ts +++ b/src/app/shared/repositories/proof/proof.spec.ts @@ -11,12 +11,12 @@ import { Assets, DefaultFactId, Facts, - getSerializedSortedSignedMessage, - isFacts, - isSignature, Proof, Signatures, Truth, + getSerializedSortedProofMetadata, + isFacts, + isSignature, } from './proof'; describe('Proof', () => { @@ -270,9 +270,9 @@ describe('Proof utils', () => { it('should get serialized sorted SignedTargets', async () => { proof = await Proof.from(mediaStore, ASSETS, TRUTH); - const signedMessage = await proof.generateSignedMessage(); - const expected = `{"asset_mime_type":"${ASSET1_MIMETYPE}","caption":"","created_at":${TIMESTAMP},"device_name":"${DEVICE_NAME_VALUE1}","information":{"device.device_name":"${DEVICE_NAME_VALUE2}","device.humidity":0.8,"geolocation.geolocation_latitude":${GEOLOCATION_LATITUDE2},"geolocation.geolocation_longitude":${GEOLOCATION_LONGITUDE2}},"location_latitude":${GEOLOCATION_LATITUDE1},"location_longitude":${GEOLOCATION_LONGITUDE1},"proof_hash":"${ASSET1_SHA256SUM}","recorder":"Capture","spec_version":"2.0.0"}`; - expect(getSerializedSortedSignedMessage(signedMessage)).toEqual(expected); + const ProofMetadata = await proof.generateProofMetadata(); + const expected = `{\n "asset_mime_type": "${ASSET1_MIMETYPE}",\n "caption": "",\n "created_at": ${TIMESTAMP},\n "device_name": "${DEVICE_NAME_VALUE1}",\n "information": {\n "device.device_name": "${DEVICE_NAME_VALUE2}",\n "device.humidity": 0.8,\n "geolocation.geolocation_latitude": ${GEOLOCATION_LATITUDE2},\n "geolocation.geolocation_longitude": ${GEOLOCATION_LONGITUDE2}},\n "location_latitude": ${GEOLOCATION_LATITUDE1},\n "location_longitude": ${GEOLOCATION_LONGITUDE1},\n "proof_hash": "${ASSET1_SHA256SUM}",\n "recorder": "Capture",\n "spec_version":"2.0.0"\n}`; + expect(getSerializedSortedProofMetadata(ProofMetadata)).toEqual(expected); }); }); diff --git a/src/app/shared/repositories/proof/proof.ts b/src/app/shared/repositories/proof/proof.ts index 72b0ff2e9..d6425c132 100644 --- a/src/app/shared/repositories/proof/proof.ts +++ b/src/app/shared/repositories/proof/proof.ts @@ -1,10 +1,13 @@ +import { CameraSource } from '@capacitor/camera'; import { snakeCase } from 'lodash'; import { defer, iif, of } from 'rxjs'; import { concatMap } from 'rxjs/operators'; import { sha256WithString } from '../../../utils/crypto/crypto'; import { sortObjectDeeplyByKey } from '../../../utils/immutable/immutable'; import { MimeType } from '../../../utils/mime-type'; +import { generateIntegritySha } from '../../../utils/nit/nit'; import { isNonNullable } from '../../../utils/rx-operators/rx-operators'; +import { CaptureAppWebCryptoApiSignatureProvider } from '../../collector/signature/capture-app-web-crypto-api-signature-provider/capture-app-web-crypto-api-signature-provider.service'; import { Tuple } from '../../database/table/table'; import { MediaStore, @@ -221,10 +224,10 @@ export class Proof { * - https://app.asana.com/0/0/1204012493522134/1204289040001270/f * @returns A promise that resolves to the generated signed message */ - async generateSignedMessage( + async generateProofMetadata( recorder: RecorderType = RecorderType.Capture - ): Promise { - const signedMessage: SignedMessage = { + ): Promise { + const ProofMetadata: ProofMetadata = { spec_version: SIGNATURE_VERSION, recorder: recorder, created_at: this.truth.timestamp, @@ -236,7 +239,7 @@ export class Proof { caption: '', information: this.getInformation(), }; - return signedMessage; + return ProofMetadata; } /** @@ -255,18 +258,19 @@ export class Proof { } async isVerified() { - const signedMessage: SignedMessage = await this.generateSignedMessage(); - const serializedSortedSignedMessage = - getSerializedSortedSignedMessage(signedMessage); + // FIXME: Read CameraSource + const recorder = CaptureAppWebCryptoApiSignatureProvider.recorderFor( + CameraSource.Camera + ); + const proofMetadata: ProofMetadata = await this.generateProofMetadata( + recorder + ); + const integritySha = await generateIntegritySha(proofMetadata); const results = await Promise.all( Object.entries(this.signatures).map(([id, signature]) => Proof.signatureProviders .get(id) - ?.verify( - serializedSortedSignedMessage, - signature.signature, - signature.publicKey - ) + ?.verify(integritySha, signature.signature, signature.publicKey) ) ); return results.every(result => result); @@ -347,6 +351,11 @@ export const enum FactCategory { GEOLOCATION = 'geolocation', } +export interface SignResult extends Tuple { + readonly signatures: Signatures; + readonly integritySha: string; +} + export interface Signatures extends Tuple { readonly [id: string]: Signature; } @@ -384,8 +393,13 @@ export function getSerializedSortedSignedTargets(signedTargets: SignedTargets) { return JSON.stringify(sortObjectDeeplyByKey(signedTargets as any).toJSON()); } -export function getSerializedSortedSignedMessage(signedMessage: SignedMessage) { - return JSON.stringify(sortObjectDeeplyByKey(signedMessage as any).toJSON()); +export function getSerializedSortedProofMetadata(ProofMetadata: ProofMetadata) { + const indent = 2; + return JSON.stringify( + sortObjectDeeplyByKey(ProofMetadata as any).toJSON(), + null, + indent + ); } interface SignatureVerifier { @@ -410,7 +424,7 @@ export interface IndexedProofView extends Tuple { * The new signed message schema as discussed in * https://github.com/numbersprotocol/capture-lite/issues/779 */ -export interface SignedMessage { +export interface ProofMetadata { spec_version: string; recorder: RecorderType; created_at: number; diff --git a/src/app/utils/nit/nit.ts b/src/app/utils/nit/nit.ts index d1abb7f9b..e8cf9d402 100644 --- a/src/app/utils/nit/nit.ts +++ b/src/app/utils/nit/nit.ts @@ -1,15 +1,14 @@ import { ethers, sha256 } from 'ethers'; import { - SignedMessage, - getSerializedSortedSignedMessage, + ProofMetadata, + getSerializedSortedProofMetadata, } from '../../shared/repositories/proof/proof'; -export async function generateIntegritySha(message: SignedMessage) { - const serializedSignedMessage = getSerializedSortedSignedMessage(message); +export async function generateIntegritySha(proofMetadata: ProofMetadata) { + const serializedSortedProofMetadata = + getSerializedSortedProofMetadata(proofMetadata); - // eslint-disable-next-line @typescript-eslint/no-magic-numbers - const data = JSON.stringify(JSON.stringify(serializedSignedMessage, null, 2)); - const dataBytes = ethers.toUtf8Bytes(data); + const dataBytes = ethers.toUtf8Bytes(serializedSortedProofMetadata); /** * WORKAROUND: