From b8e81db317e2785ead6f9563d3702365c679af1f Mon Sep 17 00:00:00 2001 From: eum602 Date: Fri, 22 Sep 2023 00:07:44 -0500 Subject: [PATCH 1/6] fix: add additional fields mapped from DDCCCoreDataSet to Verifiable Credential --- src/constants/disease.code.mapper.ts | 17 +++ src/constants/errorMessages.ts | 5 +- .../verifiable-credential/ddcc.credential.ts | 20 +-- .../verifiable-credentials/ddcc.format.ts | 56 ++++++--- .../verifiable.credentials.service.ts | 115 +++++++++++++----- 5 files changed, 158 insertions(+), 55 deletions(-) create mode 100644 src/constants/disease.code.mapper.ts diff --git a/src/constants/disease.code.mapper.ts b/src/constants/disease.code.mapper.ts new file mode 100644 index 0000000..5031099 --- /dev/null +++ b/src/constants/disease.code.mapper.ts @@ -0,0 +1,17 @@ +// reference: http://lacpass.create.cl:8089/ValueSet-ddcc-vaccines.html +export const DISEASE_LIST: Map = new Map(); + +DISEASE_LIST.set('RA01', 'COVID-19'); +DISEASE_LIST.set('1D47', 'Yellow fever'); +DISEASE_LIST.set('1F03', 'Measles'); +DISEASE_LIST.set('1F03.0', 'Measles without complication'); +DISEASE_LIST.set('1F03.1', 'Measles complicated by encephalitis'); +DISEASE_LIST.set('1F03.2', 'Measles complicated by meningitis'); +DISEASE_LIST.set('1F03.Y', 'Measles with other complications'); +DISEASE_LIST.set('1C81', 'Acute poliomyelitis'); +DISEASE_LIST.set('XN9S3', 'Yellow fever virus'); +DISEASE_LIST.set('XN186', 'Measles virus'); +DISEASE_LIST.set('XN3M0', 'Poliovirus'); +DISEASE_LIST.set('XN6KZ', 'Wild poliovirus type 1'); +DISEASE_LIST.set('XN9CF', 'Wild poliovirus type 2'); +DISEASE_LIST.set('XN97R', 'Wild poliovirus type 3'); diff --git a/src/constants/errorMessages.ts b/src/constants/errorMessages.ts index 7255ae8..7657aa7 100644 --- a/src/constants/errorMessages.ts +++ b/src/constants/errorMessages.ts @@ -37,6 +37,7 @@ export enum ErrorsMessages { VACCINATION_MISSING_ATTRIBUTE = 'No vaccination attribute was found', COUNTRY_MISSING_ATTRIBUTE = 'No country attribute was found', VACCINE_MISSING_ATTRIBUTE = 'No vaccine attribute was found', + BRAND_MISSING_ATTRIBUTE = 'No brand attribute was found', PLAIN_MESSAGE_SIGNING_ERROR = 'There was an error while trying to sign plain message', CANONICALIZE_ERROR = 'An error occurred while trying to canonicalize message', VM_NOT_FOUND = 'Verification method was not found', @@ -49,9 +50,7 @@ export enum ErrorsMessages { INVALID_CONTENT_ATTRIBUTE = 'The specified content attribute is invalid', INVALID_ATTACHMENT_ATTRIBUTE = 'The specified attachment attribute is invalid', DDCCCOREDATASET_NOT_FOUND = 'No ddcCoredataSet was found', - DDCCCOREDATASET_PARSE_ERROR = 'The specified ddcCoredataSet could not be parsed', - // eslint-disable-next-line max-len - DDCCCOREDATASET_ATTRIBUTE_NOT_FOUND = 'ddccCoreDataSet attribute inside parsed data' + DDCCCOREDATASET_PARSE_ERROR = 'The specified ddcCoredataSet could not be parsed' } export const Errors = { diff --git a/src/interfaces/verifiable-credential/ddcc.credential.ts b/src/interfaces/verifiable-credential/ddcc.credential.ts index 06d9ebf..d6a165c 100644 --- a/src/interfaces/verifiable-credential/ddcc.credential.ts +++ b/src/interfaces/verifiable-credential/ddcc.credential.ts @@ -16,9 +16,9 @@ export interface VaccineRecipient { type: string[] | string; id: string; name: string; - birthDate: string; - identifier: string; - gender: string; + birthDate?: string; + identifier?: string; + gender?: string; } export interface ImageObject { @@ -32,8 +32,10 @@ export interface ImageObject { export interface Vaccine { type: string[] | string; - atcCode: string; - medicinalProductName: string; + atcCode: string; // vaccine code + medicinalProductName: string; // brand "mapped" code + marketingAuthorizationHolder?: string; // maholder code + disease?: string; // disease "mapped" code } export interface IDDCCCredentialSubject { @@ -41,8 +43,12 @@ export interface IDDCCCredentialSubject { batchNumber: string; countryOfVaccination: string; dateOfVaccination: string; - administeringCentre: string; - order: string; + administeringCentre?: string; + nextVaccinationDate?: string; // nextDose + order: string; // dose + totalDoses?: string; // totalDoses + validFrom?: string; // validFrom + healthProfessional?: string; // healthProfessional recipient: VaccineRecipient; vaccine: Vaccine; image: ImageObject; diff --git a/src/services/verifiable-credentials/ddcc.format.ts b/src/services/verifiable-credentials/ddcc.format.ts index 69b9922..96574eb 100644 --- a/src/services/verifiable-credentials/ddcc.format.ts +++ b/src/services/verifiable-credentials/ddcc.format.ts @@ -1,5 +1,11 @@ import { Type } from 'class-transformer'; -import { IsDefined, IsNumber, IsString, ValidateNested } from 'class-validator'; +import { + IsDefined, + IsNumber, + IsOptional, + IsString, + ValidateNested +} from 'class-validator'; export class Identifier { @IsString() @@ -19,35 +25,48 @@ export class DDCCCertificate { issuer!: Issuer; } -export class Vaccine { - @IsString() - code!: string; -} -export class Country { - @IsString() - code!: string; -} - -export class Brand { +export class CodeSystem { @IsString() code!: string; } export class Vaccination { - @Type(() => Vaccine) - vaccine!: Vaccine; + @Type(() => CodeSystem) + vaccine!: CodeSystem; @IsString() date!: string; @IsNumber() dose!: number; - @Type(() => Country) - country!: Country; + @Type(() => CodeSystem) + country!: CodeSystem; @IsString() + @IsOptional() centre!: string; - @Type(() => Brand) - brand!: Brand; + @IsString() + @IsOptional() + nextDose!: string; + @Type(() => CodeSystem) + brand!: CodeSystem; @IsString() lot!: string; + @IsOptional() + @Type(() => CodeSystem) + @ValidateNested() + maholder!: CodeSystem; + @IsOptional() + @Type(() => CodeSystem) + @ValidateNested() + disease!: CodeSystem; + @IsOptional() + @IsString() + totalDoses!: string; + @IsOptional() + @IsString() + validFrom!: string; + @IsOptional() + @Type(() => Identifier) + @ValidateNested() + practitioner!: Identifier; } export class DDCCFormatValidator { @@ -60,10 +79,13 @@ export class DDCCFormatValidator { @IsString() name!: string; @IsString() + @IsOptional() birthDate!: string; @IsString() + @IsOptional() identifier!: string; @IsString() + @IsOptional() sex!: string; } diff --git a/src/services/verifiable-credentials/verifiable.credentials.service.ts b/src/services/verifiable-credentials/verifiable.credentials.service.ts index 0e65d18..0f6ade5 100644 --- a/src/services/verifiable-credentials/verifiable.credentials.service.ts +++ b/src/services/verifiable-credentials/verifiable.credentials.service.ts @@ -26,10 +26,10 @@ import { DidDocumentService } from '@services/did/did.document.service'; import { BadRequestError } from 'routing-controllers'; import { ErrorsMessages } from '../../constants/errorMessages'; import { - Country, + CodeSystem, DDCCFormatValidator, DDCCCoreDataSet, - Vaccine + Identifier } from './ddcc.format'; import { validateOrReject } from 'class-validator'; import canonicalize from 'canonicalize'; @@ -43,6 +43,7 @@ import { IDocumentReference } from './iddcc.to.vc'; import { Attachment, Content, DocumentReference } from '@dto/DDCCToVC'; +import { DISEASE_LIST } from '@constants/disease.code.mapper'; @Service() export class VerifiableCredentialService { @@ -384,17 +385,38 @@ export class VerifiableCredentialService { if (!country) { throw new BadRequestError(ErrorsMessages.COUNTRY_MISSING_ATTRIBUTE); } - await this._validateDDCCCoreRequiredDataCountry(country); + await this._validateDDCCCoreCodeSystemAttribute(country); const vaccine = vaccinationData.vaccine; if (!vaccine) { throw new BadRequestError(ErrorsMessages.VACCINE_MISSING_ATTRIBUTE); } - await this._validateDDCCCoreRequiredDataVaccine(vaccine); + await this._validateDDCCCoreCodeSystemAttribute(vaccine); + const brand = vaccinationData.brand; + if (!brand) { + throw new BadRequestError(ErrorsMessages.BRAND_MISSING_ATTRIBUTE); + } + if (vaccinationData.maholder) { + await this._validateDDCCCoreCodeSystemAttribute(vaccinationData.maholder); + } + if (vaccinationData.disease) { + await this._validateDDCCCoreCodeSystemAttribute(vaccinationData.disease); + } + if (vaccinationData.practitioner) { + await this._validateDDCCCoreIdentifierAttribute( + vaccinationData.practitioner + ); + } const ddcc = new DDCCFormatValidator(); - ddcc.birthDate = ddccData.birthDate; - ddcc.identifier = ddccData.identifier; + if (ddcc.birthDate) { + ddcc.birthDate = ddccData.birthDate; + } + if (ddcc.identifier) { + ddcc.identifier = ddccData.identifier; + } ddcc.name = ddccData.name; - ddcc.sex = ddccData.sex; + if (ddcc.sex) { + ddcc.sex = ddccData.sex; + } ddcc.vaccination = ddccData.vaccination; try { await validateOrReject(ddcc); @@ -403,9 +425,9 @@ export class VerifiableCredentialService { } } - async _validateDDCCCoreRequiredDataCountry(country: Country) { - const c = new Country(); - c.code = country.code; + async _validateDDCCCoreCodeSystemAttribute(attribute: CodeSystem) { + const c = new CodeSystem(); + c.code = attribute.code; try { await validateOrReject(c); } catch (err: any) { @@ -413,11 +435,11 @@ export class VerifiableCredentialService { } } - async _validateDDCCCoreRequiredDataVaccine(vaccine: Vaccine) { - const v = new Vaccine(); - v.code = vaccine.code; + async _validateDDCCCoreIdentifierAttribute(attribute: Identifier) { + const c = new Identifier(); + c.value = attribute.value; try { - await validateOrReject(v); + await validateOrReject(c); } catch (err: any) { throw new BadRequestError(err); } @@ -442,9 +464,8 @@ export class VerifiableCredentialService { return { '@context': [ 'https://www.w3.org/2018/credentials/v1', - 'https://w3id.org/vaccination/v1', // eslint-disable-next-line max-len - 'https://credentials-library.lacchain.net/credentials/health/vaccination/v2' + 'https://credentials-library.lacchain.net/credentials/health/vaccination/v3' ], // eslint-disable-next-line quote-props id: randomUUID().toString(), @@ -464,14 +485,12 @@ export class VerifiableCredentialService { batchNumber: '', countryOfVaccination: '', dateOfVaccination: '', - administeringCentre: '', order: '', recipient: { - type: ['VaccineRecipient', 'VaccineRecipientExtension1'], + type: 'VaccineRecipient', id: '', name: '', birthDate: '', - identifier: '', gender: '' }, vaccine: { @@ -485,7 +504,7 @@ export class VerifiableCredentialService { alternateName: 'QRCode', description: // eslint-disable-next-line max-len - 'QR code containing the cryptographic information that certifies the validity of the embedded health related content', + 'QR code containing the DDCCCoreDatSet plus signature', encodingFormat: '', contentUrl: '' } @@ -509,6 +528,7 @@ export class VerifiableCredentialService { ): Promise { const ddccCredential = await this.new(); const ddccData = data.ddccData; + // Vaccination event const vaccination = data.ddccData.vaccination; ddccCredential.issuer = data.issuerDid; ddccCredential.name = ddccData.certificate.issuer.identifier.value; @@ -517,23 +537,62 @@ export class VerifiableCredentialService { ddccCredential.credentialSubject.countryOfVaccination = vaccination.country.code; ddccCredential.credentialSubject.dateOfVaccination = vaccination.date; - ddccCredential.credentialSubject.administeringCentre = vaccination.centre; + if (vaccination.centre) { + ddccCredential.credentialSubject.administeringCentre = vaccination.centre; + } + if (vaccination.nextDose) { + ddccCredential.credentialSubject.nextVaccinationDate = + vaccination.nextDose; + } + if (vaccination.totalDoses) { + ddccCredential.credentialSubject.totalDoses = vaccination.totalDoses; + } + if (vaccination.validFrom) { + ddccCredential.credentialSubject.nextVaccinationDate = + vaccination.validFrom; + } ddccCredential.credentialSubject.order = vaccination.dose.toString(); + // recipient ddccCredential.credentialSubject.recipient.id = data.receiverDid; ddccCredential.credentialSubject.recipient.name = ddccData.name; - ddccCredential.credentialSubject.recipient.birthDate = ddccData.birthDate; - ddccCredential.credentialSubject.recipient.identifier = ddccData.identifier; - ddccCredential.credentialSubject.recipient.gender = ddccData.sex; + if (ddccData.birthDate) { + ddccCredential.credentialSubject.recipient.birthDate = ddccData.birthDate; + } + if (ddccData.identifier) { + ddccCredential.credentialSubject.recipient.identifier = + ddccData.identifier; + } + if (ddccData.sex) { + ddccCredential.credentialSubject.recipient.gender = ddccData.sex; + } + // vaccine ddccCredential.credentialSubject.vaccine.atcCode = ddccData.vaccination.vaccine.code; const medicinalProductName = MEDICINAL_PRODUCT_NAMES.get( ddccData.vaccination.brand.code ); - if (!medicinalProductName) { - throw new BadRequestError(ErrorsMessages.BRAND_CODE_NOT_FOUND); - } ddccCredential.credentialSubject.vaccine.medicinalProductName = - medicinalProductName; + medicinalProductName + ? medicinalProductName + : ddccData.vaccination.brand.code; + if (ddccData.vaccination.maholder && ddccData.vaccination.maholder.code) { + ddccCredential.credentialSubject.vaccine.marketingAuthorizationHolder = + ddccData.vaccination.maholder.code; + } + if (ddccData.vaccination.disease && ddccData.vaccination.disease.code) { + const mappedDisease = DISEASE_LIST.get(ddccData.vaccination.disease.code); + ddccCredential.credentialSubject.vaccine.disease = mappedDisease + ? mappedDisease + : ddccData.vaccination.disease.code; + } + if ( + ddccData.vaccination.practitioner && + ddccData.vaccination.practitioner.value + ) { + ddccCredential.credentialSubject.healthProfessional = + ddccData.vaccination.practitioner.value; + } + // Image ddccCredential.credentialSubject.image.encodingFormat = attachment.contentType; ddccCredential.credentialSubject.image.contentUrl = attachment.data; From 06f2707e871187c557ab8fb7932be0b537fffddd Mon Sep 17 00:00:00 2001 From: eum602 Date: Fri, 22 Sep 2023 08:55:02 -0500 Subject: [PATCH 2/6] fix: treat DDCCCoreDataSet 'certificate' as an optional value --- .../verifiable-credentials/ddcc.format.ts | 14 +++++- .../verifiable.credentials.service.ts | 45 ++++++++++++++++--- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/src/services/verifiable-credentials/ddcc.format.ts b/src/services/verifiable-credentials/ddcc.format.ts index 96574eb..8cc077c 100644 --- a/src/services/verifiable-credentials/ddcc.format.ts +++ b/src/services/verifiable-credentials/ddcc.format.ts @@ -16,11 +16,21 @@ export class Issuer { identifier!: Identifier; } +export class Period { + @IsString() + start!: string; + @IsString() + end!: string; +} + export class DDCCCertificate { - // period .. omitted + @IsOptional() + @Type(() => Period) + period!: Period; @Type(() => Identifier) hcid!: Identifier; - // version .. omitted + @IsString() + version!: string; @Type(() => Issuer) issuer!: Issuer; } diff --git a/src/services/verifiable-credentials/verifiable.credentials.service.ts b/src/services/verifiable-credentials/verifiable.credentials.service.ts index 0f6ade5..c7a82c1 100644 --- a/src/services/verifiable-credentials/verifiable.credentials.service.ts +++ b/src/services/verifiable-credentials/verifiable.credentials.service.ts @@ -171,7 +171,6 @@ export class VerifiableCredentialService { imageContent: IContent, qrDescription: string ): Promise { - // TODO: send credentials const ddccCredential = await this.assembleDDCCCredential( ddccCoreDataSet, imageContent.attachment, @@ -182,8 +181,7 @@ export class VerifiableCredentialService { ddccCredential, issuerDid )) as IDDCCVerifiableCredential; - // TODO: send ddcc credential through secure relay server - const message = JSON.stringify(ddccVerifiableCredential); // TODO: stringify VC + const message = JSON.stringify(ddccVerifiableCredential); const authAddress = await this.getAuthAddressFromDid(issuerDid); const keyExchangePublicKey = await this.getOrSetOrCreateKeyExchangePublicKeyFromDid(issuerDid); @@ -528,11 +526,46 @@ export class VerifiableCredentialService { ): Promise { const ddccCredential = await this.new(); const ddccData = data.ddccData; - // Vaccination event + // Vaccination certificate const vaccination = data.ddccData.vaccination; ddccCredential.issuer = data.issuerDid; - ddccCredential.name = ddccData.certificate.issuer.identifier.value; - ddccCredential.identifier = ddccData.certificate.hcid.value; + const certificate = ddccData.certificate; + if ( + certificate && + certificate.issuer && + certificate.issuer.identifier && + certificate.issuer.identifier.value + ) { + ddccCredential.name = ddccData.certificate.issuer.identifier.value; + } + + if (certificate && certificate.period && certificate.period.start) { + try { + ddccCredential.issuanceDate = new Date( + certificate.period.start + ).toJSON(); + } catch (e) { + this.log.info( + 'invalid certificate start date, defaulting to current date' + ); + } + } + + if (certificate && certificate.period && certificate.period.end) { + try { + ddccCredential.expirationDate = new Date( + certificate.period.end + ).toJSON(); + } catch (e) { + this.log.info('invalid certificate end date, leaving it blank'); + } + } + + if (ddccData.certificate.hcid.value) { + ddccCredential.identifier = ddccData.certificate.hcid.value; + } + + // Vaccination event ddccCredential.credentialSubject.batchNumber = vaccination.lot; ddccCredential.credentialSubject.countryOfVaccination = vaccination.country.code; From 8547299052ad143d8e2d0e0d3fde6dba6c68a2b3 Mon Sep 17 00:00:00 2001 From: eum602 Date: Sat, 23 Sep 2023 10:05:18 -0500 Subject: [PATCH 3/6] feat: add onchain proof of existence for emitted verifiable credentials --- .example.env | 3 +- .example.env.dev | 3 +- package.json | 2 +- src/config/index.ts | 3 +- src/constants/errorMessages.ts | 10 +- .../lacchain/verification.registry.abi.ts | 938 ++++++++++++++++++ src/constants/verification.registry.ts | 2 +- src/interfaces/ethereum/transaction.ts | 14 + src/interfaces/lacchain/misc.ts | 3 + .../verifiable-credential/delivery.ts | 3 + src/services/external/did-lac/did-service.ts | 23 +- .../key-manager/key-manager.service.ts | 51 +- src/services/lacchain-ethers.ts | 94 ++ .../secure.relay.service.ts | 5 +- .../verifiable.credentials.service.ts | 83 +- .../verification.registry.base.ts | 96 ++ .../verification.registry.ts | 214 ++++ 17 files changed, 1517 insertions(+), 30 deletions(-) create mode 100644 src/constants/lacchain/verification.registry.abi.ts create mode 100644 src/interfaces/ethereum/transaction.ts create mode 100644 src/interfaces/lacchain/misc.ts create mode 100644 src/interfaces/verifiable-credential/delivery.ts create mode 100644 src/services/lacchain-ethers.ts create mode 100644 src/services/verifiable-credentials/verification.registry.base.ts create mode 100644 src/services/verifiable-credentials/verification.registry.ts diff --git a/.example.env b/.example.env index 4513c36..595d9cb 100644 --- a/.example.env +++ b/.example.env @@ -73,6 +73,7 @@ EMAIL_TRANSPORTER = AWS # KEY_MANAGER_DID_JWT = /did-jwt/generate # KEY_MANAGER_DID_COMM_ENCRYPT = /didcomm/x25519/encrypt # KEY_MANAGER_SECP256K1_PLAIN_MESSAGE_SIGN = /secp256k1/sign/plain-message +# KEY_MANAGER_SECP256K1_SIGN_LACCHAIN_TRANSACTION=/secp256k1/sign/lacchain-tx ## Chain Of trust @@ -111,4 +112,4 @@ NODE_ADDRESS = 0xad730de8c4bfc3d845f7ce851bcf2ea17c049585 # CHAIN_OF_TRUST_CONTRACT_ADDRESS = '0x25a64325d73cB7226EBcC390600ccB6a7557e4f1' # Mandatory. Update this value accordinly ## verification registry -# VERIFICATION_REGISTRY_CONTRACT_ADDRESS = '0xcd438C44caf4346EaA44ff47825c6C34Ce73a616' # optional, just in case you are willing to use another verification registry \ No newline at end of file +# VERIFICATION_REGISTRY_CONTRACT_ADDRESS = '0xF17Da8641771c0196318515b662b0C00132C4163' # optional, just in case you are willing to use another verification registry \ No newline at end of file diff --git a/.example.env.dev b/.example.env.dev index 63f705c..47b9fe6 100644 --- a/.example.env.dev +++ b/.example.env.dev @@ -72,6 +72,7 @@ EMAIL_TRANSPORTER = AWS # KEY_MANAGER_DID_JWT = /did-jwt/generate # KEY_MANAGER_DID_COMM_ENCRYPT = /didcomm/x25519/encrypt # KEY_MANAGER_SECP256K1_PLAIN_MESSAGE_SIGN = /secp256k1/sign/plain-message +# KEY_MANAGER_SECP256K1_SIGN_LACCHAIN_TRANSACTION=/secp256k1/sign/lacchain-tx ## Chain Of trust @@ -110,4 +111,4 @@ NODE_ADDRESS = 0xad730de8c4bfc3d845f7ce851bcf2ea17c049585 # CHAIN_OF_TRUST_CONTRACT_ADDRESS = '0x25a64325d73cB7226EBcC390600ccB6a7557e4f1' # Mandatory. Update this value accordinly ## verification registry -# VERIFICATION_REGISTRY_CONTRACT_ADDRESS = '0xcd438C44caf4346EaA44ff47825c6C34Ce73a616' # optional, just in case you are willing to use another verification registry \ No newline at end of file +# VERIFICATION_REGISTRY_CONTRACT_ADDRESS = '0xF17Da8641771c0196318515b662b0C00132C4163' # optional, just in case you are willing to use another verification registry \ No newline at end of file diff --git a/package.json b/package.json index ed2f611..581f2b8 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "class-validator-jsonschema": "^2.2.0", "cors": "^2.8.5", "dotenv": "^16.0.3", - "ethers": "^6.3.0", + "ethers": "^5.6.5", "express": "^4.17.3", "express-formidable": "^1.2.0", "express-rate-limit": "^6.3.0", diff --git a/src/config/index.ts b/src/config/index.ts index c84389c..3939f28 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -2,10 +2,10 @@ import { VERIFICATION_REGISTRY_CONTRACT_ADDRESSES } from '@constants/verification.registry'; import { randomUUID } from 'crypto'; import { config } from 'dotenv'; -import { isAddress } from 'ethers'; import { LogLevel } from 'typescript-logging'; import { Log4TSProvider } from 'typescript-logging-log4ts-style'; import { version } from 'package.json'; +import { isAddress } from 'ethers/lib/utils'; config({ path: `.env.${process.env.ENV || 'dev'}` }); @@ -150,6 +150,7 @@ export const { KEY_MANAGER_DID_JWT, KEY_MANAGER_DID_COMM_ENCRYPT, KEY_MANAGER_SECP256K1_PLAIN_MESSAGE_SIGN, + KEY_MANAGER_SECP256K1_SIGN_LACCHAIN_TRANSACTION, SECURE_RELAY_MESSAGE_DELIVERER_BASE_URL, SECURE_RELAY_MESSAGE_DELIVERER_SEND } = process.env; diff --git a/src/constants/errorMessages.ts b/src/constants/errorMessages.ts index 7657aa7..f0f890a 100644 --- a/src/constants/errorMessages.ts +++ b/src/constants/errorMessages.ts @@ -17,6 +17,8 @@ export enum ErrorsMessages { PASSWORD_ERROR = 'Property password must be longer than or equal to 6 characters', // HTTP STANDARD MESSAGES INTERNAL_SERVER_ERROR = 'Internal Server Error', + // eslint-disable-next-line max-len + INDEPENDENT_MISCONFIGURATION_ERROR = 'Service is expected to be configured as independent service but critical variables are missing', BAD_REQUEST_ERROR = 'Bad request error', USER_ALREADY_EXISTS = 'A user with this email is already registered', CREATE_DID_ERROR = 'An internal server error occurred while trying to create a new did', @@ -50,7 +52,13 @@ export enum ErrorsMessages { INVALID_CONTENT_ATTRIBUTE = 'The specified content attribute is invalid', INVALID_ATTACHMENT_ATTRIBUTE = 'The specified attachment attribute is invalid', DDCCCOREDATASET_NOT_FOUND = 'No ddcCoredataSet was found', - DDCCCOREDATASET_PARSE_ERROR = 'The specified ddcCoredataSet could not be parsed' + DDCCCOREDATASET_PARSE_ERROR = 'The specified ddcCoredataSet could not be parsed', + // eslint-disable-next-line max-len + LACCHAIN_CONTRACT_TRANSACTION_ERROR = 'There was an error, there may be an issue with the params you are sending', + // eslint-disable-next-line max-len + CHAIN_ID_FROM_DID_NOT_SUPPORTED = 'The chain id extracted from the passed DID is not supported', + // eslint-disable-next-line max-len + SIGN_TRANSACTION_ERROR = 'An error occurred while trying to sign transaction against external service' } export const Errors = { diff --git a/src/constants/lacchain/verification.registry.abi.ts b/src/constants/lacchain/verification.registry.abi.ts new file mode 100644 index 0000000..224f605 --- /dev/null +++ b/src/constants/lacchain/verification.registry.abi.ts @@ -0,0 +1,938 @@ +export const VERIFICATION_REGISTRY_ABI = [ + { + inputs: [ + { + internalType: 'address', + name: 'trustedForwarderAddress', + type: 'address' + }, + { + internalType: 'address', + name: 'didRegistry', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + } + ], + stateMutability: 'nonpayable', + type: 'constructor' + }, + { + inputs: [], + name: 'InvalidShortString', + type: 'error' + }, + { + inputs: [ + { + internalType: 'string', + name: 'str', + type: 'string' + } + ], + name: 'StringTooLong', + type: 'error' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'address', + name: 'by', + type: 'address' + }, + { + indexed: true, + internalType: 'address', + name: 'didRegistry', + type: 'address' + }, + { + indexed: false, + internalType: 'bool', + name: 'status', + type: 'bool' + } + ], + name: 'DidRegistryChange', + type: 'event' + }, + { + anonymous: false, + inputs: [], + name: 'EIP712DomainChanged', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + }, + { + indexed: true, + internalType: 'address', + name: 'by', + type: 'address' + }, + { + indexed: false, + internalType: 'bool', + name: 'status', + type: 'bool' + } + ], + name: 'NewDelegateTypeChange', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + indexed: true, + internalType: 'address', + name: 'by', + type: 'address' + }, + { + indexed: false, + internalType: 'uint256', + name: 'iat', + type: 'uint256' + }, + { + indexed: false, + internalType: 'uint256', + name: 'exp', + type: 'uint256' + } + ], + name: 'NewIssuance', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + indexed: true, + internalType: 'address', + name: 'by', + type: 'address' + }, + { + indexed: false, + internalType: 'bool', + name: 'isOnHold', + type: 'bool' + }, + { + indexed: false, + internalType: 'uint256', + name: 'currentTime', + type: 'uint256' + } + ], + name: 'NewOnHoldChange', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + indexed: true, + internalType: 'address', + name: 'by', + type: 'address' + }, + { + indexed: false, + internalType: 'uint256', + name: 'iat', + type: 'uint256' + }, + { + indexed: false, + internalType: 'uint256', + name: 'exp', + type: 'uint256' + } + ], + name: 'NewRevocation', + type: 'event' + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + indexed: true, + internalType: 'address', + name: 'by', + type: 'address' + }, + { + indexed: false, + internalType: 'uint256', + name: 'exp', + type: 'uint256' + } + ], + name: 'NewUpdate', + type: 'event' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + } + ], + name: 'addDelegateType', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'didRegistryAddress', + type: 'address' + } + ], + name: 'addDidRegistry', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [], + name: 'defaultDelegateType', + outputs: [ + { + internalType: 'bytes32', + name: '', + type: 'bytes32' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'defaultDidRegistry', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + }, + { + internalType: 'bytes32', + name: '', + type: 'bytes32' + } + ], + name: 'didDelegateTypes', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + name: 'didRegistries', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [], + name: 'eip712Domain', + outputs: [ + { + internalType: 'bytes1', + name: 'fields', + type: 'bytes1' + }, + { + internalType: 'string', + name: 'name', + type: 'string' + }, + { + internalType: 'string', + name: 'version', + type: 'string' + }, + { + internalType: 'uint256', + name: 'chainId', + type: 'uint256' + }, + { + internalType: 'address', + name: 'verifyingContract', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'salt', + type: 'bytes32' + }, + { + internalType: 'uint256[]', + name: 'extensions', + type: 'uint256[]' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'issuer', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + } + ], + name: 'getDetails', + outputs: [ + { + internalType: 'uint256', + name: 'iat', + type: 'uint256' + }, + { + internalType: 'uint256', + name: 'exp', + type: 'uint256' + }, + { + internalType: 'bool', + name: 'onHold', + type: 'bool' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'identity', + type: 'address' + } + ], + name: 'getDidRegistry', + outputs: [ + { + internalType: 'address', + name: '', + type: 'address' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'issuer', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + } + ], + name: 'isValidCredential', + outputs: [ + { + internalType: 'bool', + name: 'value', + type: 'bool' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + } + ], + name: 'isValidDelegateType', + outputs: [ + { + internalType: 'bool', + name: '', + type: 'bool' + } + ], + stateMutability: 'view', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'uint256', + name: 'exp', + type: 'uint256' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + } + ], + name: 'issue', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'uint256', + name: 'exp', + type: 'uint256' + } + ], + name: 'issueByDelegate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'uint256', + name: 'exp', + type: 'uint256' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'uint8', + name: 'sigV', + type: 'uint8' + }, + { + internalType: 'bytes32', + name: 'sigR', + type: 'bytes32' + }, + { + internalType: 'bytes32', + name: 'sigS', + type: 'bytes32' + } + ], + name: 'issueByDelegateSigned', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'uint256', + name: 'exp', + type: 'uint256' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'uint8', + name: 'sigV', + type: 'uint8' + }, + { + internalType: 'bytes32', + name: 'sigR', + type: 'bytes32' + }, + { + internalType: 'bytes32', + name: 'sigS', + type: 'bytes32' + } + ], + name: 'issueByDelegateWithCustomDelegateTypeSigned', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'uint256', + name: 'exp', + type: 'uint256' + } + ], + name: 'issueByDelegateWithCustomType', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'uint256', + name: 'exp', + type: 'uint256' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'uint8', + name: 'sigV', + type: 'uint8' + }, + { + internalType: 'bytes32', + name: 'sigR', + type: 'bytes32' + }, + { + internalType: 'bytes32', + name: 'sigS', + type: 'bytes32' + } + ], + name: 'issueSigned', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'bool', + name: 'onHoldStatus', + type: 'bool' + } + ], + name: 'onHoldByDelegate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'bool', + name: 'onHoldStatus', + type: 'bool' + } + ], + name: 'onHoldByDelegateWithCustomType', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'bool', + name: 'onHoldStatus', + type: 'bool' + } + ], + name: 'onHoldChange', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + } + ], + name: 'removeDelegateType', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [], + name: 'removeDidRegistry', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + } + ], + name: 'revoke', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + } + ], + name: 'revokeByDelegate', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'uint8', + name: 'sigV', + type: 'uint8' + }, + { + internalType: 'bytes32', + name: 'sigR', + type: 'bytes32' + }, + { + internalType: 'bytes32', + name: 'sigS', + type: 'bytes32' + } + ], + name: 'revokeByDelegateSigned', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'uint8', + name: 'sigV', + type: 'uint8' + }, + { + internalType: 'bytes32', + name: 'sigR', + type: 'bytes32' + }, + { + internalType: 'bytes32', + name: 'sigS', + type: 'bytes32' + } + ], + name: 'revokeByDelegateWithCustomDelegateTypeSigned', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'delegateType', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + } + ], + name: 'revokeByDelegateWithCustomType', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + }, + { + internalType: 'uint8', + name: 'sigV', + type: 'uint8' + }, + { + internalType: 'bytes32', + name: 'sigR', + type: 'bytes32' + }, + { + internalType: 'bytes32', + name: 'sigS', + type: 'bytes32' + } + ], + name: 'revokeSigned', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [ + { + internalType: 'bytes32', + name: 'digest', + type: 'bytes32' + }, + { + internalType: 'uint256', + name: 'exp', + type: 'uint256' + }, + { + internalType: 'address', + name: 'identity', + type: 'address' + } + ], + name: 'update', + outputs: [], + stateMutability: 'nonpayable', + type: 'function' + }, + { + inputs: [], + name: 'version', + outputs: [ + { + internalType: 'uint16', + name: '', + type: 'uint16' + } + ], + stateMutability: 'view', + type: 'function' + } +]; diff --git a/src/constants/verification.registry.ts b/src/constants/verification.registry.ts index 3e59da3..54f7196 100644 --- a/src/constants/verification.registry.ts +++ b/src/constants/verification.registry.ts @@ -2,5 +2,5 @@ export const VERIFICATION_REGISTRY_CONTRACT_ADDRESSES: Map = new Map(); VERIFICATION_REGISTRY_CONTRACT_ADDRESSES.set( '0x9e55c', - '0xcd438C44caf4346EaA44ff47825c6C34Ce73a616' + '0xF17Da8641771c0196318515b662b0C00132C4163' ); diff --git a/src/interfaces/ethereum/transaction.ts b/src/interfaces/ethereum/transaction.ts new file mode 100644 index 0000000..7689e88 --- /dev/null +++ b/src/interfaces/ethereum/transaction.ts @@ -0,0 +1,14 @@ +export interface INoSpecifiedSenderTransaction { + to: string; + data: string; +} + +export interface IEthereumTransactionResponse { + txHash: string; +} + +export interface ITransaction { + from: string; + to: string; + data: string; +} diff --git a/src/interfaces/lacchain/misc.ts b/src/interfaces/lacchain/misc.ts new file mode 100644 index 0000000..aa83641 --- /dev/null +++ b/src/interfaces/lacchain/misc.ts @@ -0,0 +1,3 @@ +export interface IDidController { + controller: string; +} diff --git a/src/interfaces/verifiable-credential/delivery.ts b/src/interfaces/verifiable-credential/delivery.ts new file mode 100644 index 0000000..d4636c6 --- /dev/null +++ b/src/interfaces/verifiable-credential/delivery.ts @@ -0,0 +1,3 @@ +export interface IDelivery { + deliveryId: string; +} diff --git a/src/services/external/did-lac/did-service.ts b/src/services/external/did-lac/did-service.ts index 5689038..808eb8b 100644 --- a/src/services/external/did-lac/did-service.ts +++ b/src/services/external/did-lac/did-service.ts @@ -26,12 +26,13 @@ import { InternalServerError } from 'routing-controllers'; import { ErrorsMessages } from '../../../constants/errorMessages'; import fetch from 'node-fetch'; import FormData from 'form-data'; +import { IDidController } from 'src/interfaces/lacchain/misc'; @Service() export class DidServiceLac1 { // did public createDid: () => Promise; - public getController: (did: string) => Promise; + public getController: (did: string) => Promise; public decodeDid: (did: string) => Promise; // jwk attribute @@ -123,10 +124,12 @@ export class DidServiceLac1 { return (await result.json()) as DidType; } - private async getControllerByLib(did: string): Promise { - return (await this.didService?.getController(did)) as any; + private async getControllerByLib(did: string): Promise { + return (await this.didService?.getController(did)) as IDidController; } - private async getControllerByExternalService(did: string): Promise { + private async getControllerByExternalService( + did: string + ): Promise { const result = await fetch( `${IDENTITY_MANAGER_BASE_URL}${DID_LAC1_CONTROLLER}/${did}`, { @@ -141,7 +144,7 @@ export class DidServiceLac1 { console.log(await result.text()); throw new InternalServerError(ErrorsMessages.GET_DID_CONTROLLER_ERROR); } - return (await result.json()) as any; + return (await result.json()) as IDidController; } private async decodeDidByLib(did: string): Promise { @@ -170,6 +173,11 @@ export class DidServiceLac1 { formData: any, x509Cert: Express.Multer.File ): Promise { + if (!this.didService) { + throw new InternalServerError( + ErrorsMessages.INDEPENDENT_MISCONFIGURATION_ERROR + ); + } return await this.didService?.rawAddAttributeFromX509Certificate( formData, x509Cert @@ -203,6 +211,11 @@ export class DidServiceLac1 { formData: any, x509Cert: Express.Multer.File ): Promise { + if (!this.didService) { + throw new InternalServerError( + ErrorsMessages.INDEPENDENT_MISCONFIGURATION_ERROR + ); + } return this.didService?.rawRevokeAttributeFromX509Certificate( formData, x509Cert diff --git a/src/services/external/key-manager/key-manager.service.ts b/src/services/external/key-manager/key-manager.service.ts index 31158d0..68c94c9 100644 --- a/src/services/external/key-manager/key-manager.service.ts +++ b/src/services/external/key-manager/key-manager.service.ts @@ -5,7 +5,8 @@ import { IS_CLIENT_DEPENDENT_SERVICE, KEY_MANAGER_BASE_URL, log4TSProvider, - KEY_MANAGER_SECP256K1_PLAIN_MESSAGE_SIGN + KEY_MANAGER_SECP256K1_PLAIN_MESSAGE_SIGN, + KEY_MANAGER_SECP256K1_SIGN_LACCHAIN_TRANSACTION } from '../../../config'; import { Service } from 'typedi'; import { ErrorsMessages } from '../../../constants/errorMessages'; @@ -16,7 +17,10 @@ import { IDidCommToEncryptData, ISignPlainMessageByAddress, ISecp256k1SignatureMessageResponse, - Secp256k1GenericSignerService + Secp256k1GenericSignerService, + ISignedTransaction, + ILacchainTransaction, + Secp256k1SignLacchainTransactionService } from 'lacchain-key-manager'; @Service() @@ -30,6 +34,11 @@ export class KeyManagerService { public secpSignPlainMessage: ( message: ISignPlainMessageByAddress ) => Promise; + public signLacchainTransaction: ( + lacchainTransaction: ILacchainTransaction + ) => Promise; + // eslint-disable-next-line max-len + private secp256k1SignLacchainTransactionService: Secp256k1SignLacchainTransactionService | null; log = log4TSProvider.getLogger('IdentityManagerService'); constructor() { if (IS_CLIENT_DEPENDENT_SERVICE !== 'true') { @@ -46,6 +55,11 @@ export class KeyManagerService { const U = require('lacchain-key-manager').Secp256k1GenericSignerServiceDb; this.secp256k1GenericSignerService = new U(); + + this.signLacchainTransaction = this.signLacchainTransactionByLib; + const V = + require('lacchain-key-manager').Secp256k1SignLacchainTransactionServiceDb; + this.secp256k1SignLacchainTransactionService = new V(); } else { this.log.info('Configuring key manager as external service connection'); this.didJwtService = null; @@ -54,6 +68,10 @@ export class KeyManagerService { this.didCommEncryptService = null; this.secpSignPlainMessage = this.secpSignPlainMessageByExternalService; this.secp256k1GenericSignerService = null; + + this.secp256k1SignLacchainTransactionService = null; + this.signLacchainTransaction = + this.signLacchainTransactionByExternalService; } } private async createDidJwtByLib(didJwt: IDidJwt): Promise { @@ -70,6 +88,14 @@ export class KeyManagerService { return await this.secp256k1GenericSignerService?.signPlainMessage(message); } + async signLacchainTransactionByLib( + lacchainTransaction: ILacchainTransaction + ): Promise { + return this.secp256k1SignLacchainTransactionService?.signEthereumBasedTransaction( + lacchainTransaction + ); + } + private async createDidJwtByExternalService( didJwt: IDidJwt ): Promise { @@ -132,4 +158,25 @@ export class KeyManagerService { } return (await result.json()) as any; } + + async signLacchainTransactionByExternalService( + lacchainTransaction: ILacchainTransaction + ): Promise { + const result = await fetch( + `${KEY_MANAGER_BASE_URL}${KEY_MANAGER_SECP256K1_SIGN_LACCHAIN_TRANSACTION}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(lacchainTransaction) + } + ); + console.log('status', result.status); + if (result.status !== 200) { + console.log(await result.text()); + throw new InternalServerError(ErrorsMessages.SIGN_TRANSACTION_ERROR); + } + return (await result.json()) as ISignedTransaction; // todo: check type in this return + } } diff --git a/src/services/lacchain-ethers.ts b/src/services/lacchain-ethers.ts new file mode 100644 index 0000000..bc97625 --- /dev/null +++ b/src/services/lacchain-ethers.ts @@ -0,0 +1,94 @@ +import { log4TSProvider } from '../config'; +import { GasModelProvider, GasModelSigner } from '@lacchain/gas-model-provider'; +import { BigNumber, ethers } from 'ethers'; +import { BadRequestError } from 'routing-controllers'; +import { TransactionRequest } from '@ethersproject/abstract-provider'; +import { ErrorsMessages } from '../constants/errorMessages'; +import { + IEthereumTransactionResponse, + INoSpecifiedSenderTransaction, + ITransaction +} from 'src/interfaces/ethereum/transaction'; +import { KeyManagerService } from './external/key-manager/key-manager.service'; + +export class LacchainLib { + log = log4TSProvider.getLogger('lacchainUtils'); + private nodeAddress: string; + private readonly provider: ethers.providers.Provider; + private rpcUrl: string; + private keyManagerService: KeyManagerService; + constructor(nodeAddress: string, rpcUrl: string) { + this.nodeAddress = nodeAddress; + this.rpcUrl = rpcUrl; + this.provider = new GasModelProvider(this.rpcUrl); + this.keyManagerService = new KeyManagerService(); + } + async signAndSend(tx: ITransaction): Promise { + const voidSigner = new ethers.VoidSigner(tx.from, this.provider); + // Gas Limit is set to avoid failures + const fullyPopulatedTransactionRequest = + await voidSigner.populateTransaction({ ...tx, gasLimit: 4700000 }); + const f = fullyPopulatedTransactionRequest.gasPrice; + const s = BigNumber.from(f); + fullyPopulatedTransactionRequest.gasPrice = s.toHexString(); + const signedTx = await this.keyManagerService.signLacchainTransaction({ + fullyPopulatedTransactionRequest, + signerAddress: tx.from, + nodeAddress: this.nodeAddress, + expiration: Math.floor(Date.now() / 1000) + 86400 * 4 // 4 days + }); + const txResponse = await this.provider.sendTransaction( + signedTx.signedTransaction + ); + this.log.info('waiting for transaction response...'); + try { + await txResponse.wait(); + } catch (err: any) { + throw new BadRequestError( + ErrorsMessages.LACCHAIN_CONTRACT_TRANSACTION_ERROR + ); + } + this.log.info('Transaction successfully sent, txHash', txResponse.hash); + return { txHash: txResponse.hash }; + } + async signRandomlyAndSend( + tx: INoSpecifiedSenderTransaction + ): Promise { + const wallet = ethers.Wallet.createRandom(); + const voidSigner = new ethers.VoidSigner(wallet.address, this.provider); + // Gas Limit is set to avoid failures + const tx1 = { + from: wallet.address, + to: tx.to, + data: tx.data, + gasLimit: 4700000 + }; + const fullyPopulatedTransactionRequest = + await voidSigner.populateTransaction(tx1); + const f = fullyPopulatedTransactionRequest.gasPrice; + const s = BigNumber.from(f); + fullyPopulatedTransactionRequest.gasPrice = s.toHexString(); + + const expiration = Math.floor(Date.now() / 1000) + 86400 * 4; // 4 days + const gasModelWallet = new GasModelSigner( + wallet.privateKey, + this.provider, + this.nodeAddress, + expiration + ); + const signedTransaction = await gasModelWallet.signTransaction( + fullyPopulatedTransactionRequest as TransactionRequest + ); + const txResponse = await this.provider.sendTransaction(signedTransaction); + this.log.info('waiting for blockchain response...'); + try { + await txResponse.wait(); + } catch (err: any) { + throw new BadRequestError( + ErrorsMessages.LACCHAIN_CONTRACT_TRANSACTION_ERROR + ); + } + this.log.info('Transaction sent, txHash', txResponse.hash); + return { txHash: txResponse.hash }; + } +} diff --git a/src/services/secure-relay-service/secure.relay.service.ts b/src/services/secure-relay-service/secure.relay.service.ts index 5c4e747..85add07 100644 --- a/src/services/secure-relay-service/secure.relay.service.ts +++ b/src/services/secure-relay-service/secure.relay.service.ts @@ -12,6 +12,7 @@ import { log4TSProvider } from '../../config'; import fetch from 'node-fetch'; +import { IDelivery } from 'src/interfaces/verifiable-credential/delivery'; const JWT_ENCODING_ALGORITHM = 'ES256K'; const DID_DOC_KEY_AGREEMENT_KEYWORD = 'X25519KeyAgreementKey2019'; @@ -36,7 +37,7 @@ export class SecureRelayService { recipientDid: string, message: string, exp = Math.floor(Date.now() / 1000 + 3600 * 24) // one day - ): Promise { + ): Promise { const didJwtParams: IDidJwt = { subDid, aud: SECURE_RELAY_SERVICE_DID, @@ -129,7 +130,7 @@ export class SecureRelayService { private async sendDataThroughSecureRelayMessageDeliverer( token: string, message: any - ): Promise<{ deliveryId: string }> { + ): Promise { const result = await fetch( `${SECURE_RELAY_MESSAGE_DELIVERER_BASE_URL}${SECURE_RELAY_MESSAGE_DELIVERER_SEND}`, { diff --git a/src/services/verifiable-credentials/verifiable.credentials.service.ts b/src/services/verifiable-credentials/verifiable.credentials.service.ts index c7a82c1..1a24626 100644 --- a/src/services/verifiable-credentials/verifiable.credentials.service.ts +++ b/src/services/verifiable-credentials/verifiable.credentials.service.ts @@ -16,7 +16,6 @@ import { } from 'src/interfaces/verifiable-credential/ddcc.credential'; import crypto from 'crypto'; import { Service } from 'typedi'; -import { ethers, keccak256 } from 'ethers'; import { CHAIN_ID, log4TSProvider, @@ -44,6 +43,9 @@ import { } from './iddcc.to.vc'; import { Attachment, Content, DocumentReference } from '@dto/DDCCToVC'; import { DISEASE_LIST } from '@constants/disease.code.mapper'; +import { computeAddress, keccak256 } from 'ethers/lib/utils'; +import { VerificationRegistry } from './verification.registry'; +import { IEthereumTransactionResponse } from 'src/interfaces/ethereum/transaction'; @Service() export class VerifiableCredentialService { @@ -70,12 +72,14 @@ export class VerifiableCredentialService { > = new Map(); private didServiceLac1: DidServiceLac1; private keyManager: KeyManagerService; + private verificationRegistryService: VerificationRegistry; constructor() { this.secureRelayService = new SecureRelayService(); this.didServiceLac1 = new DidServiceLac1(); this.keyManager = new KeyManagerService(); this.domain = this.encode(); this.didDocumentService = new DidDocumentService(); + this.verificationRegistryService = new VerificationRegistry(); } async transformAndSend(ddccToVc: IDDCCToVC): Promise { const foundDocumentReference = ddccToVc.bundle.entry.find( @@ -171,6 +175,7 @@ export class VerifiableCredentialService { imageContent: IContent, qrDescription: string ): Promise { + // ): Promise<{ deliveryId: string; txHash: string | null }> { const ddccCredential = await this.assembleDDCCCredential( ddccCoreDataSet, imageContent.attachment, @@ -185,13 +190,59 @@ export class VerifiableCredentialService { const authAddress = await this.getAuthAddressFromDid(issuerDid); const keyExchangePublicKey = await this.getOrSetOrCreateKeyExchangePublicKeyFromDid(issuerDid); - return this.secureRelayService.sendData( + // proof of existence + let issueTxResponse: IEthereumTransactionResponse | null = null; + // TODO: add environment varible to configure PoE behavior + try { + issueTxResponse = await this.addProofOfExistence( + issuerDid, + ddccCredential + ); + } catch (e) { + this.log.info('Error adding proof of existence', e); + } + const sentData = await this.secureRelayService.sendData( issuerDid, authAddress, keyExchangePublicKey, receiverDid, message ); + return { + deliveryId: sentData.deliveryId, + txHash: issueTxResponse ? issueTxResponse.txHash : null + }; + } + /** + * Leaves a proof of existence. Resolves the controller of the issuer did and signs + * the proof of existence with its associated private key + * @param {string} issuerDid + * @param {ICredential} credentialData + */ + async addProofOfExistence( + issuerDid: string, + credentialData: ICredential + ): Promise { + const credentialHash = this.computeCredentialHash(credentialData); + let expiration = 0; + if (credentialData && credentialData.expirationDate) { + const d = new Date(credentialData.expirationDate).getTime(); + if (d < new Date().getTime()) { + this.log.info( + // eslint-disable-next-line max-len + 'Credential is expired, setting onchain expiration date to zero => never expires' + ); + } else { + expiration = Math.floor( + new Date(credentialData.expirationDate).getTime() / 1000 + ); + } + } + return this.verificationRegistryService.verifyAndIssueSigned( + issuerDid, + credentialHash, + expiration + ); } async getOrSetOrCreateKeyExchangePublicKeyFromDid( did: string @@ -253,7 +304,7 @@ export class VerifiableCredentialService { const hexPubKey = '0x' + assertionPublicKey.publicKeyBuffer.toString('hex'); const messageRequest: ISignPlainMessageByAddress = { - address: ethers.computeAddress(hexPubKey), + address: computeAddress(hexPubKey), messageHash: '0x' + crypto.createHash('sha256').update('Proof').digest('hex') }; @@ -311,10 +362,7 @@ export class VerifiableCredentialService { 'secp256k1' )?.find(el => { const hexPubKey = '0x' + el.publicKeyBuffer.toString('hex'); - return ( - ethers.computeAddress(hexPubKey) === - ethers.computeAddress(newAssertionKeyHex) - ); + return computeAddress(hexPubKey) === computeAddress(newAssertionKeyHex); }); if (foundAssertionPublicKey) { const hexPubKey = Buffer.from( @@ -334,7 +382,7 @@ export class VerifiableCredentialService { const buffAuthPublicKey = Buffer.from( await this.getOrSetAuthPublicKey(issuerDid) ); - return ethers.computeAddress('0x' + buffAuthPublicKey.toString('hex')); + return computeAddress('0x' + buffAuthPublicKey.toString('hex')); } async getOrSetAuthPublicKey(did: string): Promise { @@ -653,17 +701,22 @@ export class VerifiableCredentialService { return { ...credential, proof }; } - async getIType1ProofAssertionMethodTemplate( - credentialData: ICredential, - issuerDid: string - ): Promise { + computeCredentialHash(credentialData: ICredential) { const credentialDataString = canonicalize(credentialData); if (!credentialDataString) { throw new BadRequestError(ErrorsMessages.CANONICALIZE_ERROR); } - const credentialHash = + return ( '0x' + - crypto.createHash('sha256').update(credentialDataString).digest('hex'); + crypto.createHash('sha256').update(credentialDataString).digest('hex') + ); + } + + async getIType1ProofAssertionMethodTemplate( + credentialData: ICredential, + issuerDid: string + ): Promise { + const credentialHash = this.computeCredentialHash(credentialData); const assertionKey = await this.getOrSetOrCreateAssertionPublicKeyFromDid( issuerDid, 'secp256k1' @@ -672,7 +725,7 @@ export class VerifiableCredentialService { ? assertionKey.hexPubKey : '0x' + assertionKey.hexPubKey; const messageRequest: ISignPlainMessageByAddress = { - address: ethers.computeAddress(hexPubKey), + address: computeAddress(hexPubKey), messageHash: credentialHash }; const proofValueResponse = await this.keyManager.secpSignPlainMessage( diff --git a/src/services/verifiable-credentials/verification.registry.base.ts b/src/services/verifiable-credentials/verification.registry.base.ts new file mode 100644 index 0000000..e4b7335 --- /dev/null +++ b/src/services/verifiable-credentials/verification.registry.base.ts @@ -0,0 +1,96 @@ +// eslint-disable-next-line max-len +import { VERIFICATION_REGISTRY_ABI } from '../../constants/lacchain/verification.registry.abi'; +import { log4TSProvider } from '../../config'; +import { LacchainLib } from '@services/lacchain-ethers'; +import { Signature, Wallet, ethers } from 'ethers'; +import { Interface } from 'ethers/lib/utils'; +import { + IEthereumTransactionResponse, + INoSpecifiedSenderTransaction, + ITransaction +} from 'src/interfaces/ethereum/transaction'; +import { Service } from 'typedi'; +import { GasModelProvider, GasModelSigner } from '@lacchain/gas-model-provider'; + +@Service() +export class VerificationRegistryBase { + private readonly lacchainLib: LacchainLib; + log = log4TSProvider.getLogger('VerificationRegistryBaseInterface'); + private verificationRegistryAddress: string; + private abstractContractInterface: ethers.Contract; + constructor( + verificationRegistryAddress: string, + rpcUrl: string, + nodeAddress: string + ) { + this.lacchainLib = new LacchainLib(nodeAddress, rpcUrl); + this.verificationRegistryAddress = verificationRegistryAddress; + const key = Wallet.createRandom().privateKey; + const provider = this.configureProvider(rpcUrl, key, nodeAddress); + this.abstractContractInterface = new ethers.Contract( + this.verificationRegistryAddress, + VERIFICATION_REGISTRY_ABI, + provider + ); + } + async getDidRegistry(identity: string): Promise { + return this.abstractContractInterface.getDidRegistry(identity); + } + async setDidRegistry( + from: string, + didRegistryAddress: string + ): Promise { + const methodName = 'addDidRegistry'; + const methodSignature = [ + `function ${methodName}(address didRegistryAddress)` + ]; + const methodInterface = new Interface(methodSignature); + const encodedData = methodInterface.encodeFunctionData(methodName, [ + didRegistryAddress + ]); + const tx: ITransaction = { + from, + to: this.verificationRegistryAddress, + data: encodedData + }; + return this.lacchainLib.signAndSend(tx); + } + async issueSigned( + digest: string, + exp: number, + issuerAddress: string, + sig: Signature + ): Promise { + const methodName = 'issueSigned'; + const methodSignature = [ + `function ${methodName}(bytes32 digest, uint256 exp, + address identity, + uint8 sigV, + bytes32 sigR, + bytes32 sigS) external` + ]; + const methodInterface = new Interface(methodSignature); + const encodedData = methodInterface.encodeFunctionData(methodName, [ + digest, + exp, + issuerAddress, + sig.v, + sig.r, + sig.s + ]); + const tx: INoSpecifiedSenderTransaction = { + to: this.verificationRegistryAddress, + data: encodedData + }; + return this.lacchainLib.signRandomlyAndSend(tx); + } + private configureProvider( + rpcUrl: string, + privateKey: string, + nodeAddress: string, + expiration = Math.floor(Date.now() / 1000) + 86400 * 1080 + ): GasModelSigner { + const provider = new GasModelProvider(rpcUrl); + return new GasModelSigner(privateKey, provider, nodeAddress, expiration); + } +} diff --git a/src/services/verifiable-credentials/verification.registry.ts b/src/services/verifiable-credentials/verification.registry.ts new file mode 100644 index 0000000..0e430c7 --- /dev/null +++ b/src/services/verifiable-credentials/verification.registry.ts @@ -0,0 +1,214 @@ +import { + arrayify, + defaultAbiCoder, + keccak256, + solidityPack, + splitSignature, + toUtf8Bytes +} from 'ethers/lib/utils'; +import { + CHAIN_ID, + log4TSProvider, + resolveVerificationRegistryContractAddress +} from '../../config'; +import { Service } from 'typedi'; +import { ISignPlainMessageByAddress } from 'lacchain-key-manager'; +import { DidServiceLac1 } from '../external/did-lac/did-service'; +import { KeyManagerService } from '../external/key-manager/key-manager.service'; +import { BadRequestError } from 'routing-controllers'; +import { ErrorsMessages } from '../../constants/errorMessages'; +import { VerificationRegistryBase } from './verification.registry.base'; +import { getNodeAddress, getRpcUrl } from 'lacchain-trust/dist/src/config'; +import { IEthereumTransactionResponse } from 'src/interfaces/ethereum/transaction'; + +@Service() +export class VerificationRegistry { + log = log4TSProvider.getLogger('VerificationRegistryService'); + private EIP712ContractName = 'VerificationRegistry'; + private chainId = CHAIN_ID; + private verificationRegistryAddress: string; + private TYPE_HASH = keccak256( + toUtf8Bytes( + // eslint-disable-next-line max-len + 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)' + ) + ); + private inMemoryDidRegistriesInVerificationRegistry: Map = + new Map(); + private _hashedName; + private _hashedVersion = keccak256(toUtf8Bytes('1')); + private domainSeparator: string; + private ISSUE_TYPEHASH: string; + private didServiceLac1: DidServiceLac1; + private keyManager: KeyManagerService; + private verificationRegistryBase: VerificationRegistryBase; + + constructor() { + this.verificationRegistryAddress = + resolveVerificationRegistryContractAddress(); + this._hashedName = keccak256(toUtf8Bytes(this.EIP712ContractName)); + const eds = defaultAbiCoder.encode( + ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address'], + [ + this.TYPE_HASH, + this._hashedName, + this._hashedVersion, + this.chainId, + this.verificationRegistryAddress + ] + ); + this.domainSeparator = keccak256(eds); + this.ISSUE_TYPEHASH = keccak256( + toUtf8Bytes('Issue(bytes32 digest, uint256 exp, address identity)') + ); // OK -> 0xaaf414ba23a8cfcf004a7f75188441e59666f98d85447b5665cf04052d8e2bc3 + this.didServiceLac1 = new DidServiceLac1(); + this.keyManager = new KeyManagerService(); + const rpcUrl = getRpcUrl(); + const nodeAddress = getNodeAddress(); + this.verificationRegistryBase = new VerificationRegistryBase( + this.verificationRegistryAddress, + rpcUrl, + nodeAddress + ); + this.chainId = CHAIN_ID; + } + + async verifyAndIssueSigned( + issuerDid: string, + digest: string, + exp: number + ): Promise { + // decode did and get main address and controller address + // TODO: check didRegistry is in the same chain as configured in the service + const { address, didRegistryAddress, chainId } = + await this.didServiceLac1.decodeDid(issuerDid); + // since transaction is being sent via signed, the controller must + // be verified in the did registry + // so decoded didRegistry MUST be in the same chain as configured on start up + if (chainId !== this.chainId) { + throw new BadRequestError(ErrorsMessages.CHAIN_ID_FROM_DID_NOT_SUPPORTED); + } + // make sure didRegistry is set otherwise set it up + // TODO: add in memory map + if ( + this.inMemoryDidRegistriesInVerificationRegistry.get(issuerDid) !== + didRegistryAddress + ) { + const retrievedVerificationRegistry = + await this.verificationRegistryBase.getDidRegistry(address); + if (retrievedVerificationRegistry == didRegistryAddress) { + this.log.info( + 'DidRegistry in memory set (issuerDid) => (didRegistry): ', + issuerDid, + ' =>', + didRegistryAddress + ); + this.inMemoryDidRegistriesInVerificationRegistry.set( + issuerDid, + didRegistryAddress + ); + } else { + const txResponse = await this.verificationRegistryBase.setDidRegistry( + address, + didRegistryAddress + ); + this.log.info( + 'did registry set for did: ', + issuerDid, + 'in verification registry:', + didRegistryAddress, + 'txHash', + txResponse.txHash + ); + } + } + return this.issueSigned(issuerDid, digest, exp); + } + + /** + * + * @param {string} issuerDid + * @param {string} digest + * @param {string} exp - unix timestamp number indicating the time in the + * future where the attestation will be considered invalid. If zero, the + * attestation won't expire + */ + async issueSigned( + issuerDid: string, + digest: string, + exp: number + ): Promise { + // decode did and get main address and controller address + // TODO: check didRegistry is in the same chain as configured in the service + const { address } = await this.didServiceLac1.decodeDid(issuerDid); + // make sure didRegistry is set otherwise set it up + + const controllerAddressResponse = await this.didServiceLac1.getController( + issuerDid + ); + + const { typeDataHash } = this.getTypedDataHashForIssue( + address, + digest, + exp + ); + + const messageRequest: ISignPlainMessageByAddress = { + address: controllerAddressResponse.controller, + messageHash: typeDataHash + }; + try { + const { signature } = await this.keyManager.secpSignPlainMessage( + messageRequest + ); + const sig = splitSignature(signature); + return this.verificationRegistryBase.issueSigned( + digest, + exp, + address, + sig + ); + } catch (e: any) { + if (e) { + this.log.info( + // eslint-disable-next-line max-len + 'There was an error while trying to sign the message for issuance in verification registry', + e + ); + } + throw new BadRequestError(ErrorsMessages.INTERNAL_SERVER_ERROR); + } + } + + /** + * + * @param {string} issuerAddress + * @param {string} digest - hex string of 32 bytes + * @param {number} exp - unix timestamp number indicating the time in the + * future where the attestation will be considered invalid. If zero, the + * attestation won't expire + * @return { { typeDataHash: string, digest: string, exp: number } } + */ + getTypedDataHashForIssue( + issuerAddress: string, + digest: string, + exp: number + ): { typeDataHash: string; digest: string; exp: number } { + // 1. Build struct data hash + const encodedMessage = defaultAbiCoder.encode( + ['bytes32', 'bytes32', 'uint256', 'address'], + [this.ISSUE_TYPEHASH, digest, exp, issuerAddress] + ); + const structHash = keccak256(arrayify(encodedMessage)); + + // 2.2 Build type data hash + // Inputs: structHash and domainSeparator + const typeData = solidityPack( + // pack with no padding + ['bytes1', 'bytes1', 'bytes32', 'bytes32'], + [0x19, 0x01, this.domainSeparator, structHash] + ); + const typeDataHash = keccak256(typeData); + return { typeDataHash, exp, digest }; + } +} From d66b573f5d2948a2b0a51b7b16a1be71b15703e5 Mon Sep 17 00:00:00 2001 From: eum602 Date: Sat, 23 Sep 2023 11:31:38 -0500 Subject: [PATCH 4/6] add configuration variable to set PoE Mode --- .example.env | 1 + .example.env.dev | 1 + src/config/index.ts | 24 +++++++++++++++++ src/constants/errorMessages.ts | 2 ++ src/constants/poe.ts | 5 ++++ .../verifiable.credentials.service.ts | 27 +++++++++++++------ 6 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 src/constants/poe.ts diff --git a/.example.env b/.example.env index 595d9cb..84a8238 100644 --- a/.example.env +++ b/.example.env @@ -112,4 +112,5 @@ NODE_ADDRESS = 0xad730de8c4bfc3d845f7ce851bcf2ea17c049585 # CHAIN_OF_TRUST_CONTRACT_ADDRESS = '0x25a64325d73cB7226EBcC390600ccB6a7557e4f1' # Mandatory. Update this value accordinly ## verification registry +# PROOF_OF_EXISTENCE_MODE = "ENABLED_NOT_THROWABLE" # options: "STRICT", "DISABLED", by default "ENABLED_NOT_THROWABLE" # VERIFICATION_REGISTRY_CONTRACT_ADDRESS = '0xF17Da8641771c0196318515b662b0C00132C4163' # optional, just in case you are willing to use another verification registry \ No newline at end of file diff --git a/.example.env.dev b/.example.env.dev index 47b9fe6..1f7f3ed 100644 --- a/.example.env.dev +++ b/.example.env.dev @@ -111,4 +111,5 @@ NODE_ADDRESS = 0xad730de8c4bfc3d845f7ce851bcf2ea17c049585 # CHAIN_OF_TRUST_CONTRACT_ADDRESS = '0x25a64325d73cB7226EBcC390600ccB6a7557e4f1' # Mandatory. Update this value accordinly ## verification registry +# PROOF_OF_EXISTENCE_MODE = "ENABLED_NOT_THROWABLE" # options: "STRICT", "DISABLED", by default "ENABLED_NOT_THROWABLE" # VERIFICATION_REGISTRY_CONTRACT_ADDRESS = '0xF17Da8641771c0196318515b662b0C00132C4163' # optional, just in case you are willing to use another verification registry \ No newline at end of file diff --git a/src/config/index.ts b/src/config/index.ts index 3939f28..55d0a0d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -6,6 +6,7 @@ import { LogLevel } from 'typescript-logging'; import { Log4TSProvider } from 'typescript-logging-log4ts-style'; import { version } from 'package.json'; import { isAddress } from 'ethers/lib/utils'; +import { ProofOfExistenceMode } from '@constants/poe'; config({ path: `.env.${process.env.ENV || 'dev'}` }); @@ -35,6 +36,29 @@ export const getChainId = (): string => { export const CHAIN_ID = getChainId(); +export const resolveProofOfExistenceMode = () => { + const pOEValue = process.env.PROOF_OF_EXISTENCE_MODE; + let mode: ProofOfExistenceMode; + if (!pOEValue || pOEValue === 'ENABLED_NOT_THROWABLE') { + mode = ProofOfExistenceMode.ENABLED_NOT_THROWABLE; + } else if (pOEValue === 'STRICT') { + mode = ProofOfExistenceMode.STRICT; + } else if (pOEValue === 'DISABLED') { + mode = ProofOfExistenceMode.DISABLED; + } else { + log.error( + 'Invalid option for PROOF_OF_EXISTENCE_MODE environment variable, found', + pOEValue, + '. Exiting ...' + ); + process.exit(1); + } + log.info(`Setting Proof Existence Mode to', ${mode} for`, pOEValue); + return mode; +}; + +export const PROOF_OF_EXISTENCE_MODE = resolveProofOfExistenceMode(); + export const resolveVerificationRegistryContractAddress = ( verificationRegistryContractAddress = process.env .VERIFICATION_REGISTRY_CONTRACT_ADDRESS diff --git a/src/constants/errorMessages.ts b/src/constants/errorMessages.ts index f0f890a..8f30255 100644 --- a/src/constants/errorMessages.ts +++ b/src/constants/errorMessages.ts @@ -19,6 +19,8 @@ export enum ErrorsMessages { INTERNAL_SERVER_ERROR = 'Internal Server Error', // eslint-disable-next-line max-len INDEPENDENT_MISCONFIGURATION_ERROR = 'Service is expected to be configured as independent service but critical variables are missing', + // eslint-disable-next-line max-len + PROOF_OF_EXISTENCE_FAILED = 'There was an error while attempting to register a Proof of existence', BAD_REQUEST_ERROR = 'Bad request error', USER_ALREADY_EXISTS = 'A user with this email is already registered', CREATE_DID_ERROR = 'An internal server error occurred while trying to create a new did', diff --git a/src/constants/poe.ts b/src/constants/poe.ts new file mode 100644 index 0000000..4fe8081 --- /dev/null +++ b/src/constants/poe.ts @@ -0,0 +1,5 @@ +export enum ProofOfExistenceMode { + ENABLED_NOT_THROWABLE, + STRICT, + DISABLED +} diff --git a/src/services/verifiable-credentials/verifiable.credentials.service.ts b/src/services/verifiable-credentials/verifiable.credentials.service.ts index 1a24626..62c2dfd 100644 --- a/src/services/verifiable-credentials/verifiable.credentials.service.ts +++ b/src/services/verifiable-credentials/verifiable.credentials.service.ts @@ -18,11 +18,12 @@ import crypto from 'crypto'; import { Service } from 'typedi'; import { CHAIN_ID, + PROOF_OF_EXISTENCE_MODE, log4TSProvider, resolveVerificationRegistryContractAddress } from '../../config'; import { DidDocumentService } from '@services/did/did.document.service'; -import { BadRequestError } from 'routing-controllers'; +import { BadRequestError, InternalServerError } from 'routing-controllers'; import { ErrorsMessages } from '../../constants/errorMessages'; import { CodeSystem, @@ -46,6 +47,7 @@ import { DISEASE_LIST } from '@constants/disease.code.mapper'; import { computeAddress, keccak256 } from 'ethers/lib/utils'; import { VerificationRegistry } from './verification.registry'; import { IEthereumTransactionResponse } from 'src/interfaces/ethereum/transaction'; +import { ProofOfExistenceMode } from '@constants/poe'; @Service() export class VerifiableCredentialService { @@ -73,6 +75,7 @@ export class VerifiableCredentialService { private didServiceLac1: DidServiceLac1; private keyManager: KeyManagerService; private verificationRegistryService: VerificationRegistry; + private proofOfExistenceMode = PROOF_OF_EXISTENCE_MODE; constructor() { this.secureRelayService = new SecureRelayService(); this.didServiceLac1 = new DidServiceLac1(); @@ -193,14 +196,22 @@ export class VerifiableCredentialService { // proof of existence let issueTxResponse: IEthereumTransactionResponse | null = null; // TODO: add environment varible to configure PoE behavior - try { - issueTxResponse = await this.addProofOfExistence( - issuerDid, - ddccCredential - ); - } catch (e) { - this.log.info('Error adding proof of existence', e); + if (this.proofOfExistenceMode !== ProofOfExistenceMode.DISABLED) { + try { + issueTxResponse = await this.addProofOfExistence( + issuerDid, + ddccCredential + ); + } catch (e) { + this.log.info('Error adding proof of existence', e); + if (this.proofOfExistenceMode === ProofOfExistenceMode.STRICT) { + throw new InternalServerError( + ErrorsMessages.PROOF_OF_EXISTENCE_FAILED + ); + } + } } + const sentData = await this.secureRelayService.sendData( issuerDid, authAddress, From 9f282e3ebf887abc44375ff6f7061cb14481469b Mon Sep 17 00:00:00 2001 From: eum602 Date: Sat, 23 Sep 2023 13:03:46 -0500 Subject: [PATCH 5/6] docs: update documentation for credential sending request --- docs/Credential-Sending.md | 141 +++++++++++++----- .../verifiable.credentials.service.ts | 3 +- 2 files changed, 108 insertions(+), 36 deletions(-) diff --git a/docs/Credential-Sending.md b/docs/Credential-Sending.md index 4cd2c32..0965820 100644 --- a/docs/Credential-Sending.md +++ b/docs/Credential-Sending.md @@ -6,41 +6,114 @@ api_url=http://localhost:3010 # Set LACPass API url ``` -2. Send DDCC data through to patient wallet +2. Send DDCC data to patient wallet ```sh -## input variables -path_to_qr=../qr-code-examples/qr-example-1 # you should point to the public pem certificate that represents the signing certificate used to sign -issuer_did="did:lac1:1iT4kYaSKhpM7BFB75ZxYF7V3uTRAeWfPvwhFZXJQj8WrJakCczSatqNVvKZTnsD3uMz" -receiver_did="did:lac1:1iT5hMy9wbHfnd7C7QJCsQEiF7PusFngyCu2YqgLmCNJPQX77Z8WaXG6cwQtC4czY74w" #TODO: use -country_code="CL" -vaccine_code="J07BB04" -date="1998-06-04" -dose=1 -center='Vaccination Site' -brand_code='XM4YL8' -lot='PO1234' - -#patient -birthDate='1996-08-12' -name='John Doe' -identifier='UY/CU353467' -sex='male' - -country='{"code": '\"$country_code\"'}' -vaccine='{"code": '\"$vaccine_code\"'}' -brand='{"code": '\"$brand_code\"'}' -vaccination='{"date": '\"$date\"', "dose": '$dose',"country": '$country', "center": '\"$center\"' ,"vaccine": '$vaccine', "brand": '$brand', "lot": '\"$lot\"'}' -ddccData='{"vaccination":'$vaccination', "birthDate": '\"$birthDate\"', "name": '\"$name\"', "identifier": '\"$identifier\"', "sex": '\"$sex\"'}' - -echo 'sending data: ...' -echo $ddccData | jq - -## TODO: add additional fields - -# process -send_ddcc_vc_url="$api_url"/api/v1/verifiable-credential/ddcc/send -data='{"issuerDid":'\"$issuer_did\"', "receiverDid":'\"$receiver_did\"', "ddccData": '$ddccData'}' -curl -X 'POST' ${send_ddcc_vc_url} -H 'accept: application/json' -F qrCode=@$path_to_qr -F data="$data" +curl -X 'POST' \ + 'http://localhost:3010/api/v1/verifiable-credential/ddcc/send' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "bundle": { + "resourceType": "Bundle", + "id": "4ca19732-837a-48a3-8059-f98acee1ed73", + "meta": { + "versionId": "2", + "lastUpdated": "2023-08-21T19:28:47.673+00:00", + "source": "#0QGP9sLFOsQcEoOG" + }, + "identifier": { + "system": "http://worldhealthorgnaization.github.io/ddcc/Document", + "value": "5ca19732-837a-48a3-8059-f98bcee1ed73" + }, + "type": "document", + "timestamp": "2023-08-21T19:28:45.964Z", + "link": [ + { + "relation": "publication", + "url": "urn:HCID:6624" + } + ], + "entry": [ + { + "fullUrl": "urn:uuid:351be87f-e802-4b94-8cfd-46e81aa2cd5b", + "resource": { + "resourceType": "DocumentReference", + "meta": { + "profile": [ + "http://worldhealthorganization.github.io/ddcc/StructureDefinition/DDCCDocumentReferenceQR" + ] + }, + "status": "current", + "type": { + "coding": [ + { + "system": "http://worldhealthorganization.github.io/ddcc/CodeSystem/DDCC-QR-Type-CodeSystem", + "code": "who", + "display": "WHO DDCC" + } + ] + }, + "subject": { + "reference": "urn:uuid:28a20344-6f4c-4cc3-adfa-2fdfb59cdeff" + }, + "authenticator": { + "reference": "urn:uuid:56e370bb-9f09-345c-b0a5-3c76422a1491" + }, + "description": "WHO QR code for COVID 19 Vaccine Certificate", + "content": [ + { + "attachment": { + "contentType": "application/json", + "data": "ewogICAgICAicmVzb3VyY2VUe...yIsCiAgICAgICJzZXgiIDogIm1hbGUiCiAgICB9Cg==" + }, + "format": { + "system": "http://worldhealthorganization.github.io/ddcc/CodeSystem/DDCC-QR-Format-CodeSystem", + "code": "serialized" + } + }, + { + "attachment": { + "contentType": "image/png", + "data": "iVBORw0KUV...ORK5CYII=" + }, + "format": { + "system": "http://worldhealthorganization.github.io/ddcc/CodeSystem/DDCC-QR-Format-CodeSystem", + "code": "image" + } + }, + { + "attachment": { + "contentType": "application/pdf", + "data": "JVBERi0xLjcW5kc3RyZWFtCmVuZG9iagoKOCAwIG9i...HN0cmVhbQplbmRvYmoKCnN0YXJ0eHJlZgoyNDMxOAolJUVPRg==" + }, + "format": { + "system": "http://worldhealthorganization.github.io/ddcc/CodeSystem/DDCC-QR-Format-CodeSystem", + "code": "pdf" + } + } + ] + } + } + ], + "signature": { + "type": [ + { + "system": "urn:iso-astm:E1762-95:2013", + "code": "1.2.840.10065.1.12.1.5" + } + ], + "when": "2023-08-22T19:38:45.964Z", + "who": { + "identifier": { + "value": "Some Identifier" + } + }, + "data": "prOxII3XzrdsOihKp...AN+wAV6m5RxmTdGfUJQkmdXXrVKEw7xl/Q+E+nLcO6NcAKuD+QhGPc0w==" + } +}, + "issuerDid": "did:lac1:1iT5NSDvBrkYQ9oDtGAdeyYjwDDJLGKbEY4RGzG253RpyEMjiEURhgRTw96qnTfcqNpa", + "receiverDid": "did:lac1:1iT5QTdhkxWeZALaQMMhwsDzYZmbmE2dD3UZZ1LtdY7BzH6vZEta3AzsJD7RoRjaRkrB" +}' ``` diff --git a/src/services/verifiable-credentials/verifiable.credentials.service.ts b/src/services/verifiable-credentials/verifiable.credentials.service.ts index 62c2dfd..acbc465 100644 --- a/src/services/verifiable-credentials/verifiable.credentials.service.ts +++ b/src/services/verifiable-credentials/verifiable.credentials.service.ts @@ -177,8 +177,7 @@ export class VerifiableCredentialService { ddccCoreDataSet: DDCCCoreDataSet, imageContent: IContent, qrDescription: string - ): Promise { - // ): Promise<{ deliveryId: string; txHash: string | null }> { + ): Promise<{ deliveryId: string; txHash: string | null }> { const ddccCredential = await this.assembleDDCCCredential( ddccCoreDataSet, imageContent.attachment, From 51bf77367be3334c9b1b4e6e13faacc9d224bce8 Mon Sep 17 00:00:00 2001 From: eum602 Date: Sat, 23 Sep 2023 13:07:24 -0500 Subject: [PATCH 6/6] chore: changelog for release 0.0.7 --- CHANGELOG.md | 14 ++++++++++++++ package.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bb86e2..9c570cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +### 0.0.7 + +* Add PoE for emitted credentials. +* Add configuration variable `PROOF_OF_EXISTENCE_MODE` to set PoE mode: + * DISABLED: Proof of existence is disabled, in the respose the field TxHash is set to null + * STRICT: Proof of existence must strictly succeed otherwise the request throws and in the respose the field TxHash is set to null, otherwise that field will have a valid transaction hash + * ENABLED_NOT_THROWABLE: If Proof of existence fails the request does not throw but in the respose the field TxHash is set to null, otherwise that field will have a valid transaction hash +* Updates verification registry to '0xF17Da8641771c0196318515b662b0C00132C4163' which by default uses +didRegistry: 0x43dE0954a2c83A415d82b9F31705B969b5856003 +* Considers certificate period fields (if defined) as the verifiable credential issuance/expiration dates +* Add additional fields +* validates mandatory and optional DDCCCoreDataSet fields +* Downgrades ethers to version 5.6.5 since it was needed to use GasModel Library. + ### 0.0.6 * add additional codes for "brand" field used to transform DDCCCoreDataSeet to Verifiable Credential. diff --git a/package.json b/package.json index 581f2b8..ac17606 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lacpass-client", - "version": "0.0.6", + "version": "0.0.7", "description": "Rest api for lacpass Client", "license": "MIT", "scripts": {