diff --git a/.changeset/metal-carrots-hang.md b/.changeset/metal-carrots-hang.md new file mode 100644 index 0000000000..f1fb6af247 --- /dev/null +++ b/.changeset/metal-carrots-hang.md @@ -0,0 +1,5 @@ +--- +'@credo-ts/core': patch +--- + +feat: mdoc-support diff --git a/demo-openid/src/Holder.ts b/demo-openid/src/Holder.ts index d16cdc7498..09187f59ab 100644 --- a/demo-openid/src/Holder.ts +++ b/demo-openid/src/Holder.ts @@ -5,6 +5,7 @@ import { W3cJwtVerifiableCredential, W3cJsonLdVerifiableCredential, DifPresentationExchangeService, + Mdoc, } from '@credo-ts/core' import { OpenId4VcHolderModule } from '@credo-ts/openid4vc' import { ariesAskar } from '@hyperledger/aries-askar-nodejs' @@ -56,6 +57,8 @@ export class Holder extends BaseAgent> const credential = response.credential if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) { return this.agent.w3cCredentials.storeCredential({ credential }) + } else if (credential instanceof Mdoc) { + return this.agent.mdoc.store(credential) } else { return this.agent.sdJwtVc.store(credential.compact) } diff --git a/demo-openid/src/HolderInquirer.ts b/demo-openid/src/HolderInquirer.ts index f346e951e1..71bec45319 100644 --- a/demo-openid/src/HolderInquirer.ts +++ b/demo-openid/src/HolderInquirer.ts @@ -1,7 +1,7 @@ -import type { SdJwtVcRecord, W3cCredentialRecord } from '@credo-ts/core' +import type { MdocRecord, SdJwtVcRecord, W3cCredentialRecord } from '@credo-ts/core' import type { OpenId4VcSiopResolvedAuthorizationRequest, OpenId4VciResolvedCredentialOffer } from '@credo-ts/openid4vc' -import { DifPresentationExchangeService } from '@credo-ts/core' +import { DifPresentationExchangeService, Mdoc } from '@credo-ts/core' import console, { clear } from 'console' import { textSync } from 'figlet' import { prompt } from 'inquirer' @@ -181,11 +181,16 @@ export class HolderInquirer extends BaseInquirer { } } - private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord) => { + private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord | MdocRecord) => { if (credential.type === 'W3cCredentialRecord') { console.log(greenText(`W3cCredentialRecord with claim format ${credential.credential.claimFormat}`, true)) console.log(JSON.stringify(credential.credential.jsonCredential, null, 2)) console.log('') + } else if (credential.type === 'MdocRecord') { + console.log(greenText(`MdocRecord`, true)) + const namespaces = Mdoc.fromBase64Url(credential.base64Url).namespaces + console.log(JSON.stringify(namespaces, null, 2)) + console.log('') } else { console.log(greenText(`SdJwtVcRecord`, true)) const prettyClaims = this.holder.agent.sdJwtVc.fromCompact(credential.compactSdJwtVc).prettyClaims diff --git a/packages/core/package.json b/packages/core/package.json index 64a08420e2..4a9fc5dcda 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -29,11 +29,15 @@ "@digitalcredentials/jsonld-signatures": "^9.4.0", "@digitalcredentials/vc": "^6.0.1", "@multiformats/base-x": "^4.0.1", - "@noble/hashes": "^1.4.0", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", "@peculiar/asn1-ecc": "^2.3.8", "@peculiar/asn1-schema": "^2.3.8", "@peculiar/asn1-x509": "^2.3.8", "@peculiar/x509": "^1.11.0", + "@protokoll/core": "0.2.27", + "@protokoll/crypto": "0.2.27", + "@protokoll/mdoc-client": "0.2.27", "@sd-jwt/core": "^0.7.0", "@sd-jwt/decode": "^0.7.0", "@sd-jwt/jwt-status-list": "^0.7.0", @@ -54,7 +58,7 @@ "did-resolver": "^4.1.0", "jsonpath": "^1.1.1", "lru_map": "^0.4.1", - "luxon": "^3.3.0", + "luxon": "^3.5.0", "make-error": "^1.3.6", "object-inspect": "^1.10.3", "query-string": "^7.0.1", diff --git a/packages/core/src/agent/AgentModules.ts b/packages/core/src/agent/AgentModules.ts index 5b0727a94b..7ca8b8851f 100644 --- a/packages/core/src/agent/AgentModules.ts +++ b/packages/core/src/agent/AgentModules.ts @@ -10,6 +10,7 @@ import { DidsModule } from '../modules/dids' import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange' import { DiscoverFeaturesModule } from '../modules/discover-features' import { GenericRecordsModule } from '../modules/generic-records' +import { MdocModule } from '../modules/mdoc/MdocModule' import { MessagePickupModule } from '../modules/message-pickup' import { OutOfBandModule } from '../modules/oob' import { ProofsModule } from '../modules/proofs' @@ -137,6 +138,7 @@ function getDefaultAgentModules() { pex: () => new DifPresentationExchangeModule(), sdJwtVc: () => new SdJwtVcModule(), x509: () => new X509Module(), + mdoc: () => new MdocModule(), } as const } diff --git a/packages/core/src/agent/BaseAgent.ts b/packages/core/src/agent/BaseAgent.ts index e982bf81c4..d7bcff0a0c 100644 --- a/packages/core/src/agent/BaseAgent.ts +++ b/packages/core/src/agent/BaseAgent.ts @@ -14,6 +14,7 @@ import { CredentialsApi } from '../modules/credentials' import { DidsApi } from '../modules/dids' import { DiscoverFeaturesApi } from '../modules/discover-features' import { GenericRecordsApi } from '../modules/generic-records' +import { MdocApi } from '../modules/mdoc' import { MessagePickupApi } from '../modules/message-pickup/MessagePickupApi' import { OutOfBandApi } from '../modules/oob' import { ProofsApi } from '../modules/proofs' @@ -53,6 +54,7 @@ export abstract class BaseAgent public readonly basicMessages: BasicMessagesApi + public readonly mdoc: MdocApi public readonly genericRecords: GenericRecordsApi public readonly discovery: DiscoverFeaturesApi public readonly dids: DidsApi @@ -111,6 +113,7 @@ export abstract class BaseAgent(array: T): T { return this.walletWebCrypto.generateRandomValues(array) } + + public digest(algorithm: string, data: ArrayBuffer): ArrayBuffer { + return Hasher.hash(new Uint8Array(data), algorithm) + } } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 1f31a02648..1b98fd78cd 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -64,6 +64,7 @@ export * from './modules/vc' export * from './modules/cache' export * from './modules/dif-presentation-exchange' export * from './modules/sd-jwt-vc' +export * from './modules/mdoc' export { JsonEncoder, JsonTransformer, diff --git a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts index c4ae048a4d..e2925a9898 100644 --- a/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts +++ b/packages/core/src/modules/dif-presentation-exchange/DifPresentationExchangeService.ts @@ -22,7 +22,7 @@ import type { W3CVerifiablePresentation, } from '@sphereon/ssi-types' -import { PEVersion, PEX, Status } from '@sphereon/pex' +import { PEVersion, PEX, PresentationSubmissionLocation, Status } from '@sphereon/pex' import { PartialSdJwtDecodedVerifiableCredential } from '@sphereon/pex/dist/main/lib' import { injectable } from 'tsyringe' @@ -30,6 +30,8 @@ import { Hasher, getJwkFromKey } from '../../crypto' import { CredoError } from '../../error' import { JsonTransformer } from '../../utils' import { DidsApi, getKeyFromVerificationMethod } from '../dids' +import { Mdoc, MdocApi, MdocOpenId4VpSessionTranscriptOptions, MdocRecord } from '../mdoc' +import { MdocDeviceResponse } from '../mdoc/MdocDeviceResponse' import { SdJwtVcApi } from '../sd-jwt-vc' import { ClaimFormat, @@ -152,9 +154,10 @@ export class DifPresentationExchangeService { presentationSubmissionLocation?: DifPresentationExchangeSubmissionLocation challenge: string domain?: string + openid4vp?: MdocOpenId4VpSessionTranscriptOptions } ) { - const { presentationDefinition, domain, challenge } = options + const { presentationDefinition, domain, challenge, openid4vp } = options const presentationSubmissionLocation = options.presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION @@ -173,11 +176,6 @@ export class DifPresentationExchangeService { presentationDefinition as DifPresentationExchangeDefinitionV1 ).input_descriptors.filter((inputDescriptor) => inputDescriptorIds.includes(inputDescriptor.id)) - // Get all the credentials for the presentation - const credentialsForPresentation = presentationToCreate.verifiableCredentials.map((c) => - getSphereonOriginalVerifiableCredential(c.credential) - ) - const presentationDefinitionForSubject: DifPresentationExchangeDefinition = { ...presentationDefinition, input_descriptors: inputDescriptorsForPresentation, @@ -186,72 +184,106 @@ export class DifPresentationExchangeService { submission_requirements: undefined, } - const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( - presentationDefinitionForSubject, - credentialsForPresentation, - this.getPresentationSignCallback(agentContext, presentationToCreate), - { - proofOptions: { - challenge, - domain, - }, - signatureOptions: {}, - presentationSubmissionLocation: - presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, + if (presentationToCreate.claimFormat === ClaimFormat.MsoMdoc) { + if (presentationToCreate.verifiableCredentials.length !== 1) { + throw new DifPresentationExchangeError( + 'Currently a Mdoc presentation can only be created from a single credential' + ) + } + const mdocRecord = presentationToCreate.verifiableCredentials[0].credential + if (!openid4vp) { + throw new DifPresentationExchangeError('Missing openid4vp options for creating MDOC presentation.') } - ) - verifiablePresentationResultsWithFormat.push({ - verifiablePresentationResult, - claimFormat: presentationToCreate.claimFormat, - }) - } + const { deviceResponseBase64Url, presentationSubmission } = await MdocDeviceResponse.openId4Vp(agentContext, { + mdocs: [Mdoc.fromBase64Url(mdocRecord.base64Url)], + presentationDefinition: presentationDefinition, + sessionTranscriptOptions: { + ...openid4vp, + }, + }) - if (verifiablePresentationResultsWithFormat.length === 0) { - throw new DifPresentationExchangeError('No verifiable presentations created') - } + verifiablePresentationResultsWithFormat.push({ + verifiablePresentationResult: { + presentationSubmission: presentationSubmission, + verifiablePresentation: deviceResponseBase64Url, + presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL, + }, + claimFormat: presentationToCreate.claimFormat, + }) + } else { + // Get all the credentials for the presentation + const credentialsForPresentation = presentationToCreate.verifiableCredentials.map((c) => + getSphereonOriginalVerifiableCredential(c.credential) + ) - if (presentationsToCreate.length !== verifiablePresentationResultsWithFormat.length) { - throw new DifPresentationExchangeError('Invalid amount of verifiable presentations created') - } + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinitionForSubject, + credentialsForPresentation, + this.getPresentationSignCallback(agentContext, presentationToCreate), + { + proofOptions: { + challenge, + domain, + }, + signatureOptions: {}, + presentationSubmissionLocation: + presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, + } + ) - const presentationSubmission: DifPresentationExchangeSubmission = { - id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, - definition_id: - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, - descriptor_map: [], - } + verifiablePresentationResultsWithFormat.push({ + verifiablePresentationResult, + claimFormat: presentationToCreate.claimFormat, + }) + } - verifiablePresentationResultsWithFormat.forEach(({ verifiablePresentationResult }, index) => { - const descriptorMap = verifiablePresentationResult.presentationSubmission.descriptor_map.map((d) => { - const descriptor = { ...d } - - // when multiple presentations are submitted, path should be $[0], $[1] - // FIXME: this should be addressed in the PEX/OID4VP lib. - // See https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/62 - if ( - presentationSubmissionLocation === DifPresentationExchangeSubmissionLocation.EXTERNAL && - verifiablePresentationResultsWithFormat.length > 1 - ) { - descriptor.path = `$[${index}]` - } + if (verifiablePresentationResultsWithFormat.length === 0) { + throw new DifPresentationExchangeError('No verifiable presentations created') + } - return descriptor - }) + if (presentationsToCreate.length !== verifiablePresentationResultsWithFormat.length) { + throw new DifPresentationExchangeError('Invalid amount of verifiable presentations created') + } - presentationSubmission.descriptor_map.push(...descriptorMap) - }) + const presentationSubmission: DifPresentationExchangeSubmission = { + id: verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.id, + definition_id: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmission.definition_id, + descriptor_map: [], + } - return { - verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) => - getVerifiablePresentationFromEncoded( - agentContext, - resultWithFormat.verifiablePresentationResult.verifiablePresentation - ) - ), - presentationSubmission, - presentationSubmissionLocation: - verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, + verifiablePresentationResultsWithFormat.forEach(({ verifiablePresentationResult }, index) => { + const descriptorMap = verifiablePresentationResult.presentationSubmission.descriptor_map.map((d) => { + const descriptor = { ...d } + + // when multiple presentations are submitted, path should be $[0], $[1] + // FIXME: this should be addressed in the PEX/OID4VP lib. + // See https://github.com/Sphereon-Opensource/SIOP-OID4VP/issues/62 + if ( + presentationSubmissionLocation === DifPresentationExchangeSubmissionLocation.EXTERNAL && + verifiablePresentationResultsWithFormat.length > 1 + ) { + descriptor.path = `$[${index}]` + } + + return descriptor + }) + + presentationSubmission.descriptor_map.push(...descriptorMap) + }) + + return { + verifiablePresentations: verifiablePresentationResultsWithFormat.map((resultWithFormat) => + getVerifiablePresentationFromEncoded( + agentContext, + resultWithFormat.verifiablePresentationResult.verifiablePresentation + ) + ), + presentationSubmission, + presentationSubmissionLocation: + verifiablePresentationResultsWithFormat[0].verifiablePresentationResult.presentationSubmissionLocation, + } } } @@ -568,10 +600,12 @@ export class DifPresentationExchangeService { private async queryCredentialForPresentationDefinition( agentContext: AgentContext, presentationDefinition: DifPresentationExchangeDefinition - ): Promise> { + ): Promise> { const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository) const w3cQuery: Array> = [] const sdJwtVcQuery: Array> = [] + const mdocQuery: Array> = [] + const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition) if (!presentationDefinitionVersion.version) { @@ -595,6 +629,9 @@ export class DifPresentationExchangeService { w3cQuery.push({ $or: [{ expandedTypes: [schema.uri] }, { contexts: [schema.uri] }, { types: [schema.uri] }], }) + mdocQuery.push({ + docType: inputDescriptor.id, + }) } } } else if (presentationDefinitionVersion.version === PEVersion.v2) { @@ -607,33 +644,33 @@ export class DifPresentationExchangeService { ) } - const allRecords: Array = [] + const allRecords: Array = [] // query the wallet ourselves first to avoid the need to query the pex library for all // credentials for every proof request const w3cCredentialRecords = w3cQuery.length > 0 - ? await w3cCredentialRepository.findByQuery(agentContext, { - $or: w3cQuery, - }) + ? await w3cCredentialRepository.findByQuery(agentContext, { $or: w3cQuery }) : await w3cCredentialRepository.getAll(agentContext) - allRecords.push(...w3cCredentialRecords) const sdJwtVcApi = this.getSdJwtVcApi(agentContext) const sdJwtVcRecords = - sdJwtVcQuery.length > 0 - ? await sdJwtVcApi.findAllByQuery({ - $or: sdJwtVcQuery, - }) - : await sdJwtVcApi.getAll() - + sdJwtVcQuery.length > 0 ? await sdJwtVcApi.findAllByQuery({ $or: sdJwtVcQuery }) : await sdJwtVcApi.getAll() allRecords.push(...sdJwtVcRecords) + const mdocApi = this.getMdocApi(agentContext) + const mdocRecords = mdocQuery.length > 0 ? await mdocApi.findAllByQuery({ $or: mdocQuery }) : await mdocApi.getAll() + allRecords.push(...mdocRecords) + return allRecords } private getSdJwtVcApi(agentContext: AgentContext) { return agentContext.dependencyManager.resolve(SdJwtVcApi) } + + private getMdocApi(agentContext: AgentContext) { + return agentContext.dependencyManager.resolve(MdocApi) + } } diff --git a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts index 70dcf4bbfe..d1c4e17a27 100644 --- a/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts +++ b/packages/core/src/modules/dif-presentation-exchange/models/DifPexCredentialsForRequest.ts @@ -1,5 +1,7 @@ +import type { MdocRecord } from '../../mdoc' import type { SdJwtVcRecord } from '../../sd-jwt-vc' import type { ClaimFormat, W3cCredentialRecord } from '../../vc' +import type { IssuerSignedItem } from '@protokoll/mdoc-client' export interface DifPexCredentialsForRequest { /** @@ -129,8 +131,13 @@ export type SubmissionEntryCredential = type: ClaimFormat.JwtVc | ClaimFormat.LdpVc credentialRecord: W3cCredentialRecord } + | { + type: ClaimFormat.MsoMdoc + credentialRecord: MdocRecord + disclosedPayload: Record + } /** * Mapping of selected credentials for an input descriptor */ -export type DifPexInputDescriptorToCredentials = Record> +export type DifPexInputDescriptorToCredentials = Record> diff --git a/packages/core/src/modules/dif-presentation-exchange/models/index.ts b/packages/core/src/modules/dif-presentation-exchange/models/index.ts index a94c88e6c9..5f78fa8aba 100644 --- a/packages/core/src/modules/dif-presentation-exchange/models/index.ts +++ b/packages/core/src/modules/dif-presentation-exchange/models/index.ts @@ -1,4 +1,6 @@ export * from './DifPexCredentialsForRequest' +import type { Mdoc } from '../../mdoc' +import type { MdocVerifiablePresentation } from '../../mdoc/MdocVerifiablePresentation' import type { SdJwtVc } from '../../sd-jwt-vc' import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc' import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models' @@ -13,5 +15,5 @@ export type DifPresentationExchangeSubmission = PresentationSubmission export { PresentationSubmissionLocation as DifPresentationExchangeSubmissionLocation } // TODO: we might want to move this to another place at some point -export type VerifiablePresentation = W3cVerifiablePresentation | SdJwtVc -export type VerifiableCredential = W3cVerifiableCredential | SdJwtVc +export type VerifiablePresentation = W3cVerifiablePresentation | SdJwtVc | MdocVerifiablePresentation +export type VerifiableCredential = W3cVerifiableCredential | SdJwtVc | Mdoc diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts index 975062de18..38f798e797 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/credentialSelection.ts @@ -8,12 +8,16 @@ import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models' import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode' +import { SubmissionRequirementMatchType } from '@sphereon/pex/dist/main/lib/evaluation/core' import { Rules } from '@sphereon/pex-models' import { default as jp } from 'jsonpath' import { Hasher } from '../../../crypto' import { CredoError } from '../../../error' import { deepEquality } from '../../../utils' +import { MdocRecord } from '../../mdoc' +import { Mdoc } from '../../mdoc/Mdoc' +import { MdocDeviceResponse } from '../../mdoc/MdocDeviceResponse' import { SdJwtVcRecord } from '../../sd-jwt-vc' import { ClaimFormat, W3cCredentialRecord } from '../../vc' import { DifPresentationExchangeError } from '../DifPresentationExchangeError' @@ -24,12 +28,18 @@ export async function getCredentialsForRequest( // PEX instance with hasher defined pex: PEX, presentationDefinition: IPresentationDefinition, - credentialRecords: Array + credentialRecords: Array ): Promise { - const encodedCredentials = credentialRecords.map((c) => getSphereonOriginalVerifiableCredential(c)) - const selectResultsRaw = pex.selectFrom(presentationDefinition, encodedCredentials) + const encodedCredentials = credentialRecords + .filter((c): c is Exclude => c instanceof MdocRecord === false) + .map((c) => getSphereonOriginalVerifiableCredential(c)) - const selectResults = { + const { mdocPresentationDefinition, nonMdocPresentationDefinition } = + MdocDeviceResponse.partitionPresentationDefinition(presentationDefinition) + + const selectResultsRaw = pex.selectFrom(nonMdocPresentationDefinition, encodedCredentials) + + const selectResults: CredentialRecordSelectResults = { ...selectResultsRaw, // Map the encoded credential to their respective w3c credential record verifiableCredential: selectResultsRaw.verifiableCredential?.map((selectedEncoded): SubmissionEntryCredential => { @@ -79,6 +89,44 @@ export async function getCredentialsForRequest( }), } + const mdocRecords = credentialRecords.filter((c) => c instanceof MdocRecord) + for (const mdocInputDescriptor of mdocPresentationDefinition.input_descriptors) { + if (!selectResults.verifiableCredential) selectResults.verifiableCredential = [] + if (!selectResults.matches) selectResults.matches = [] + + const mdocRecordsMatchingId = mdocRecords.filter( + (mdocRecord) => mdocRecord.getTags().docType === mdocInputDescriptor.id + ) + const submissionRequirementMatch: SubmissionRequirementMatch = { + id: mdocInputDescriptor.id, + type: SubmissionRequirementMatchType.InputDescriptor, + name: mdocInputDescriptor.id, + rule: Rules.Pick, + vc_path: [], + } + + for (const mdocRecordMatchingId of mdocRecordsMatchingId) { + selectResults.verifiableCredential.push({ + type: ClaimFormat.MsoMdoc, + credentialRecord: mdocRecordMatchingId, + disclosedPayload: MdocDeviceResponse.limitDisclosureToInputDescriptor({ + mdoc: Mdoc.fromBase64Url(mdocRecordMatchingId.base64Url), + inputDescriptor: mdocInputDescriptor as InputDescriptorV2, + }), + }) + + submissionRequirementMatch.vc_path.push( + `$.verifiableCredential[${selectResults.verifiableCredential.length - 1}]` + ) + } + + if (submissionRequirementMatch.vc_path.length >= 1) { + selectResults.matches.push(submissionRequirementMatch) + } else { + selectResultsRaw.areRequiredCredentialsPresent = 'error' + } + } + const presentationSubmission: DifPexCredentialsForRequest = { requirements: [], areRequirementsSatisfied: false, @@ -104,7 +152,7 @@ export async function getCredentialsForRequest( 'Presentation Definition does not require any credentials. Optional credentials are not included in the presentation submission.' ) } - if (selectResultsRaw.areRequiredCredentialsPresent === 'error') { + if (selectResults.areRequiredCredentialsPresent === 'error') { return presentationSubmission } diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts b/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts index 17c17e01dd..8af5e05a8f 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/presentationsToCreate.ts @@ -1,6 +1,7 @@ import type { SdJwtVcRecord } from '../../sd-jwt-vc' import type { DifPexInputDescriptorToCredentials } from '../models' +import { MdocRecord } from '../../mdoc' import { W3cCredentialRecord, ClaimFormat } from '../../vc' // - the credentials included in the presentation @@ -35,7 +36,22 @@ export interface LdpVpPresentationToCreate { }> // multiple credentials supported for LDP VP } -export type PresentationToCreate = SdJwtVcPresentationToCreate | JwtVpPresentationToCreate | LdpVpPresentationToCreate +export interface MdocPresentationToCreate { + claimFormat: ClaimFormat.MsoMdoc + subjectIds: [] + verifiableCredentials: [ + { + credential: MdocRecord + inputDescriptorId: string + } + ] // only one credential supported for MDOC +} + +export type PresentationToCreate = + | SdJwtVcPresentationToCreate + | JwtVpPresentationToCreate + | LdpVpPresentationToCreate + | MdocPresentationToCreate // FIXME: we should extract supported format form top-level presentation definition, and input_descriptor as well // to make sure the presentation we are going to create is a presentation format supported by the verifier. @@ -71,6 +87,12 @@ export function getPresentationsToCreate(credentialsForInputDescriptor: DifPexIn verifiableCredentials: [{ credential, inputDescriptorId }], }) } + } else if (credential instanceof MdocRecord) { + presentationsToCreate.push({ + claimFormat: ClaimFormat.MsoMdoc, + verifiableCredentials: [{ inputDescriptorId, credential }], + subjectIds: [], + }) } else { // SD-JWT-VC always needs it's own presentation presentationsToCreate.push({ diff --git a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts index 7748ec7d65..a92928e6f5 100644 --- a/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts +++ b/packages/core/src/modules/dif-presentation-exchange/utils/transform.ts @@ -10,6 +10,7 @@ import type { import { CredoError } from '../../../error' import { JsonTransformer } from '../../../utils' +import { MdocVerifiablePresentation } from '../../mdoc/MdocVerifiablePresentation' import { SdJwtVcApi } from '../../sd-jwt-vc' import { W3cCredentialRecord, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation } from '../../vc' @@ -31,6 +32,8 @@ export function getSphereonOriginalVerifiablePresentation( verifiablePresentation instanceof W3cJsonLdVerifiablePresentation ) { return verifiablePresentation.encoded as SphereonOriginalVerifiablePresentation + } else if (verifiablePresentation instanceof MdocVerifiablePresentation) { + throw new CredoError('Mdoc verifiable presentation is not yet supported by Sphereon.') } else { return verifiablePresentation.compact } @@ -49,6 +52,7 @@ export function getVerifiablePresentationFromEncoded( } else if (typeof encodedVerifiablePresentation === 'object' && '@context' in encodedVerifiablePresentation) { return JsonTransformer.fromJSON(encodedVerifiablePresentation, W3cJsonLdVerifiablePresentation) } else { + // TODO: WE NEED TO ADD SUPPORT FOR MDOC VERIFIABLE PRESENTATION throw new CredoError('Unsupported verifiable presentation format') } } diff --git a/packages/core/src/modules/mdoc/Mdoc.ts b/packages/core/src/modules/mdoc/Mdoc.ts new file mode 100644 index 0000000000..182ea2e052 --- /dev/null +++ b/packages/core/src/modules/mdoc/Mdoc.ts @@ -0,0 +1,143 @@ +import type { MdocSignOptions, MdocNameSpaces, MdocVerifyOptions } from './MdocOptions' +import type { AgentContext } from '../../agent' +import type { IssuerSignedDocument } from '@protokoll/mdoc-client' + +import { DeviceSignedDocument, Document, Verifier, cborEncode, parseIssuerSigned } from '@protokoll/mdoc-client' + +import { getJwkFromKey, JwaSignatureAlgorithm } from '../../crypto' +import { X509Certificate, X509ModuleConfig } from '../x509' + +import { TypedArrayEncoder } from './../../utils' +import { getMdocContext } from './MdocContext' +import { MdocError } from './MdocError' + +/** + * This class represents a IssuerSigned Mdoc Document, + * which are the actual credentials being issued to holders. + */ +export class Mdoc { + public base64Url: string + private constructor(private issuerSignedDocument: IssuerSignedDocument) { + const issuerSigned = issuerSignedDocument.prepare().get('issuerSigned') + this.base64Url = TypedArrayEncoder.toBase64URL(cborEncode(issuerSigned)) + } + + public static _interalFromIssuerSignedDocument(issuerSignedDocument: IssuerSignedDocument) { + return new Mdoc(issuerSignedDocument) + } + + public static fromBase64Url(mdocBase64Url: string, expectedDocType?: string): Mdoc { + const issuerSignedDocument = parseIssuerSigned(TypedArrayEncoder.fromBase64(mdocBase64Url), expectedDocType) + return new Mdoc(issuerSignedDocument) + } + + public get docType(): string { + return this.issuerSignedDocument.docType + } + + public get alg(): JwaSignatureAlgorithm { + const algName = this.issuerSignedDocument.issuerSigned.issuerAuth.algName + if (!algName) { + throw new MdocError('Cannot extract the signature algorithm from the mdoc.') + } + if (Object.values(JwaSignatureAlgorithm).includes(algName as JwaSignatureAlgorithm)) { + return algName as JwaSignatureAlgorithm + } + throw new MdocError(`Cannot parse mdoc. The signature algorithm '${algName}' is not supported.`) + } + + public get validityInfo() { + return this.issuerSignedDocument.issuerSigned.issuerAuth.decodedPayload.validityInfo + } + + public get deviceSignedNamespaces(): MdocNameSpaces { + if (this.issuerSignedDocument instanceof DeviceSignedDocument === false) { + throw new MdocError(`Cannot get 'device-namespaces from a IssuerSignedDocument. Must be a DeviceSignedDocument.`) + } + + return this.issuerSignedDocument.allDeviceSignedNamespaces + } + + public get issuerSignedNamespaces(): MdocNameSpaces { + return this.issuerSignedDocument.allIssuerSignedNamespaces + } + + public static async sign(agentContext: AgentContext, options: MdocSignOptions) { + const { docType, validityInfo, namespaces, holderPublicKey, issuerCertificate } = options + const mdocContext = getMdocContext(agentContext) + + const holderPublicJwk = await getJwkFromKey(holderPublicKey) + const document = new Document(docType, mdocContext) + .useDigestAlgorithm('SHA-256') + .addValidityInfo(validityInfo) + .addDeviceKeyInfo({ deviceKey: holderPublicJwk.toJson() }) + + for (const [namespace, namespaceRecord] of Object.entries(namespaces)) { + document.addIssuerNameSpace(namespace, namespaceRecord) + } + + const cert = X509Certificate.fromEncodedCertificate(issuerCertificate) + const issuerKey = await getJwkFromKey(options.issuerKey ?? cert.publicKey) + + const alg = issuerKey.supportedSignatureAlgorithms.find( + (alg): alg is JwaSignatureAlgorithm.ES256 | JwaSignatureAlgorithm.ES384 | JwaSignatureAlgorithm.ES512 => { + return ( + alg === JwaSignatureAlgorithm.ES256 || + alg === JwaSignatureAlgorithm.ES384 || + alg === JwaSignatureAlgorithm.ES512 + ) + } + ) + + if (!alg) { + throw new MdocError( + `Cannot find a suitable JwaSignatureAlgorithm for signing the mdoc. Supported algorithms are 'ES256', 'ES384', 'ES512'. The issuer key supports: ${issuerKey.supportedSignatureAlgorithms.join( + ', ' + )}` + ) + } + + const issuerSignedDocument = await document.sign( + { + issuerPrivateKey: issuerKey.toJson(), + alg, + issuerCertificate, + kid: cert.publicKey.fingerprint, + }, + mdocContext + ) + + return new Mdoc(issuerSignedDocument) + } + + public async verify( + agentContext: AgentContext, + options?: MdocVerifyOptions + ): Promise<{ isValid: true } | { isValid: false; error: string }> { + const trustedCerts = + options?.trustedCertificates ?? agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates + + if (!trustedCerts) { + throw new MdocError('No trusted certificates found. Cannot verify mdoc.') + } + + const mdocContext = getMdocContext(agentContext) + try { + const verifier = new Verifier() + await verifier.verifyIssuerSignature( + { + trustedCertificates: trustedCerts.map((cert) => X509Certificate.fromEncodedCertificate(cert).rawCertificate), + issuerAuth: this.issuerSignedDocument.issuerSigned.issuerAuth, + disableCertificateChainValidation: false, + now: options?.now, + }, + getMdocContext(agentContext) + ) + + await verifier.verifyData({ mdoc: this.issuerSignedDocument }, mdocContext) + return { isValid: true } + } catch (error) { + return { isValid: false, error: error.message } + } + } +} diff --git a/packages/core/src/modules/mdoc/MdocApi.ts b/packages/core/src/modules/mdoc/MdocApi.ts new file mode 100644 index 0000000000..a9e71f302b --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocApi.ts @@ -0,0 +1,75 @@ +import type { MdocSignOptions, MdocVerifyOptions } from './MdocOptions' +import type { MdocRecord } from './repository' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { AgentContext } from '../../agent' +import { injectable } from '../../plugins' + +import { Mdoc } from './Mdoc' +import { MdocService } from './MdocService' + +/** + * @public + */ +@injectable() +export class MdocApi { + private agentContext: AgentContext + private mdocService: MdocService + + public constructor(agentContext: AgentContext, mdocService: MdocService) { + this.agentContext = agentContext + this.mdocService = mdocService + } + + /** + * Create a new Mdoc, with a spcific doctype, namespace, and validity info. + * + * @param options {MdocSignOptions} + * @returns {Promise} + */ + public async create(options: MdocSignOptions) { + return await this.mdocService.createMdoc(this.agentContext, options) + } + + /** + * + * Verify an incoming mdoc. It will check whether everything is valid, but also returns parts of the validation. + * + * For example, you might still want to continue with a flow if not all the claims are included, but the signature is valid. + * + */ + public async verify(mdoc: Mdoc, options: MdocVerifyOptions) { + return await this.mdocService.verifyMdoc(this.agentContext, mdoc, options) + } + + /** + * Create a Mdoc class from a base64url encoded Mdoc Issuer-Signed structure + */ + public fromBase64Url(base64Url: string) { + return Mdoc.fromBase64Url(base64Url) + } + + public async store(issuerSigned: Mdoc) { + return await this.mdocService.store(this.agentContext, issuerSigned) + } + + public async getById(id: string): Promise { + return await this.mdocService.getById(this.agentContext, id) + } + + public async getAll(): Promise> { + return await this.mdocService.getAll(this.agentContext) + } + + public async findAllByQuery(query: Query, queryOptions?: QueryOptions): Promise> { + return await this.mdocService.findByQuery(this.agentContext, query, queryOptions) + } + + public async deleteById(id: string) { + return await this.mdocService.deleteById(this.agentContext, id) + } + + public async update(mdocRecord: MdocRecord) { + return await this.mdocService.update(this.agentContext, mdocRecord) + } +} diff --git a/packages/core/src/modules/mdoc/MdocContext.ts b/packages/core/src/modules/mdoc/MdocContext.ts new file mode 100644 index 0000000000..ce50b862c7 --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocContext.ts @@ -0,0 +1,150 @@ +import type { AgentContext } from '../../agent' +import type { JwkJson } from '../../crypto' +import type { MdocContext, X509Context } from '@protokoll/mdoc-client' + +import { p256 } from '@noble/curves/p256' +import { hkdf } from '@noble/hashes/hkdf' +import { sha256 } from '@noble/hashes/sha2' +import * as x509 from '@peculiar/x509' +import { exportJwk, importX509 } from '@protokoll/crypto' + +import { CredoWebCrypto, getJwkFromJson, Hasher } from '../../crypto' +import { Buffer, TypedArrayEncoder } from '../../utils' +import { X509Certificate, X509Service } from '../x509' + +export const getMdocContext = (agentContext: AgentContext): MdocContext => { + const crypto = new CredoWebCrypto(agentContext) + return { + crypto: { + digest: async (input) => { + const { bytes, digestAlgorithm } = input + return new Uint8Array(crypto.digest(digestAlgorithm, bytes)) + }, + random: (length) => { + return crypto.getRandomValues(new Uint8Array(length)) + }, + calculateEphemeralMacKeyJwk: async (input) => { + const { privateKey, publicKey, sessionTranscriptBytes } = input + const ikm = p256 + .getSharedSecret(TypedArrayEncoder.toHex(privateKey), TypedArrayEncoder.toHex(publicKey), true) + .slice(1) + const salt = Hasher.hash(sessionTranscriptBytes, 'sha-256') + const info = Buffer.from('EMacKey', 'utf-8') + const hk1 = hkdf(sha256, ikm, salt, info, 32) + + return { + key_ops: ['sign', 'verify'], + ext: true, + kty: 'oct', + k: TypedArrayEncoder.toBase64URL(hk1), + alg: 'HS256', + } + }, + }, + + cose: { + mac0: { + sign: async (input) => { + const { jwk, mac0 } = input + const { data } = mac0.getRawSigningData() + return await agentContext.wallet.sign({ + data: Buffer.from(data), + key: getJwkFromJson(jwk as JwkJson).key, + }) + }, + verify: async (input) => { + const { mac0, jwk, options } = input + const { data, signature } = mac0.getRawVerificationData(options) + + return await agentContext.wallet.verify({ + key: getJwkFromJson(jwk as JwkJson).key, + data: Buffer.from(data), + signature: new Buffer(signature), + }) + }, + }, + sign1: { + sign: async (input) => { + const { jwk, sign1 } = input + const { data } = sign1.getRawSigningData() + return await agentContext.wallet.sign({ + data: Buffer.from(data), + key: getJwkFromJson(jwk as JwkJson).key, + }) + }, + verify: async (input) => { + const { sign1, jwk, options } = input + const { data, signature } = sign1.getRawVerificationData(options) + return await agentContext.wallet.verify({ + key: getJwkFromJson(jwk as JwkJson).key, + data: Buffer.from(data), + signature: new Buffer(signature), + }) + }, + }, + }, + + x509: { + getIssuerNameField: (input) => { + const { certificate, field } = input + const x509Certificate = X509Certificate.fromRawCertificate(certificate) + return x509Certificate.getIssuerNameField(field) + }, + getPublicKey: async (input) => { + //const comp = X509Certificate.fromRawCertificate(input.certificate) + //const x = getJwkFromKey(comp.publicKey).toJson() + //////// eslint-disable-next-line @typescript-eslint/no-unused-vars + //return x + + const certificate = new x509.X509Certificate(input.certificate) + const key = await importX509({ + x509: certificate.toString(), + alg: input.alg, + extractable: true, + }) + + // TODO: Key length missmatch + //expected + //{ + //kty: 'EC', + //x: 'OFBq4YMKg4w5fTifsytwBuJf_7E7VhRPXiNm52S3q1E', + //y: 'EyIAXV8gyt5FcRsYHhz4ryz97rjL0uogxHO6jMZr3bg', + //crv: 'P-256' + //} + + //actual + //{ + //kty: 'EC', + //crv: 'P-256', + //x: 'OFBq4YMKg4w5fTifsytwBuJf_7E7VhRPXiNm52S3q1ETIgBdXyDK3kVxGxgeHPiv', + //y: 'LP3uuMvS6iDEc7qMxmvduNeBp_oWscK1x-3_1KKYDayIctdDcpXHi8HcbehAfVIK' + //} + + //const comp = X509Certificate.fromRawCertificate(input.certificate) + //const x = getJwkFromKey(comp.publicKey).toJson() + //// eslint-disable-next-line @typescript-eslint/no-unused-vars + //const { use, ...jwk } = x + //return jwk + + return (await exportJwk({ key })) as JwkJson + }, + + validateCertificateChain: async (input) => { + const certificateChain = input.x5chain.map((cert) => X509Certificate.fromRawCertificate(cert).toString('pem')) + const trustedCertificates = input.trustedCertificates.map((cert) => + X509Certificate.fromRawCertificate(cert).toString('pem') + ) as [string, ...string[]] + + await X509Service.validateCertificateChain(agentContext, { + certificateChain, + trustedCertificates, + }) + }, + getCertificateData: async (input) => { + const { certificate } = input + const x509Certificate = X509Certificate.fromRawCertificate(certificate) + return x509Certificate.getData(crypto) + }, + } satisfies X509Context, + } +} diff --git a/packages/core/src/modules/mdoc/MdocDeviceResponse.ts b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts new file mode 100644 index 0000000000..eab94919dd --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts @@ -0,0 +1,193 @@ +import type { MdocDeviceResponseOpenId4VpOptions, MdocDeviceResponseVerifyOptions } from './MdocOptions' +import type { AgentContext } from '../../agent' +import type { DifPresentationExchangeDefinition } from '../dif-presentation-exchange' +import type { PresentationDefinition } from '@protokoll/mdoc-client' +import type { InputDescriptorV2 } from '@sphereon/pex-models' + +import { + limitDisclosureToInputDescriptor as mdocLimitDisclosureToId, + COSEKey, + DeviceResponse, + MDoc, + parseIssuerSigned, + Verifier, + MDocStatus, +} from '@protokoll/mdoc-client' + +import { getJwkFromKey } from '../../crypto/jose/jwk/transform' +import { CredoError } from '../../error' +import { uuid } from '../../utils/uuid' +import { X509Certificate } from '../x509/X509Certificate' +import { X509ModuleConfig } from '../x509/X509ModuleConfig' + +import { TypedArrayEncoder } from './../../utils' +import { Mdoc } from './Mdoc' +import { getMdocContext } from './MdocContext' +import { MdocError } from './MdocError' + +export class MdocDeviceResponse { + public constructor() {} + + private static assertMdocInputDescriptor(inputDescriptor: InputDescriptorV2) { + if (!inputDescriptor.format || !inputDescriptor.format.mso_mdoc) { + throw new MdocError(`Input descriptor must contain 'mso_mdoc' format property`) + } + + if (!inputDescriptor.format.mso_mdoc.alg) { + throw new MdocError(`Input descriptor mso_mdoc must contain 'alg' property`) + } + + if (!inputDescriptor.constraints?.limit_disclosure || inputDescriptor.constraints.limit_disclosure !== 'required') { + throw new MdocError( + `Input descriptor must contain 'limit_disclosure' constraints property which is set to required` + ) + } + + if (!inputDescriptor.constraints?.fields?.every((field) => field.intent_to_retain !== undefined)) { + throw new MdocError(`Input descriptor must contain 'intent_to_retain' constraints property`) + } + + return { + ...inputDescriptor, + format: { + mso_mdoc: inputDescriptor.format.mso_mdoc, + }, + constraints: { + ...inputDescriptor.constraints, + limit_disclosure: 'required', + fields: (inputDescriptor.constraints.fields ?? []).map((field) => { + return { + ...field, + intent_to_retain: field.intent_to_retain ?? false, + } + }), + }, + } satisfies PresentationDefinition['input_descriptors'][number] + } + + public static partitionPresentationDefinition = (pd: DifPresentationExchangeDefinition) => { + const nonMdocPresentationDefinition: DifPresentationExchangeDefinition = { + ...pd, + input_descriptors: pd.input_descriptors.filter( + (id) => !Object.keys((id as InputDescriptorV2).format ?? {}).includes('mso_mdoc') + ), + } as DifPresentationExchangeDefinition + + const mdocPresentationDefinition = { + ...pd, + format: { mso_mdoc: pd.format?.mso_mdoc }, + input_descriptors: (pd.input_descriptors as InputDescriptorV2[]) + .filter((id) => Object.keys(id.format ?? {}).includes('mso_mdoc')) + .map(this.assertMdocInputDescriptor), + } + + return { mdocPresentationDefinition, nonMdocPresentationDefinition } + } + + private static createPresentationSubmission(input: { + id: string + presentationDefinition: { + id: string + input_descriptors: ReturnType[] + } + }) { + const { id, presentationDefinition } = input + if (presentationDefinition.input_descriptors.length !== 1) { + throw new MdocError('Currently Mdoc Presentation Submissions can only be created for a sigle input descriptor') + } + return { + id, + definition_id: presentationDefinition.id, + descriptor_map: [ + { + id: presentationDefinition.input_descriptors[0].id, + format: 'mso_mdoc', + path: '$', + }, + ], + } + } + + public static limitDisclosureToInputDescriptor(options: { inputDescriptor: InputDescriptorV2; mdoc: Mdoc }) { + const { mdoc } = options + + const inputDescriptor = this.assertMdocInputDescriptor(options.inputDescriptor) + const _mdoc = parseIssuerSigned(TypedArrayEncoder.fromBase64(mdoc.base64Url), mdoc.docType) + return mdocLimitDisclosureToId({ mdoc: _mdoc, inputDescriptor }) + } + + public static async openId4Vp(agentContext: AgentContext, options: MdocDeviceResponseOpenId4VpOptions) { + const { sessionTranscriptOptions } = options + const presentationDefinition = this.partitionPresentationDefinition( + options.presentationDefinition + ).mdocPresentationDefinition + + const issuerSignedDocuments = options.mdocs.map((mdoc) => + parseIssuerSigned(TypedArrayEncoder.fromBase64(mdoc.base64Url), mdoc.docType) + ) + const mdoc = new MDoc(issuerSignedDocuments) + + // TODO: we need to implement this differently. + // TODO: Multiple Mdocs can have different device keys. + const mso = mdoc.documents[0].issuerSigned.issuerAuth.decodedPayload + const deviceKeyInfo = mso.deviceKeyInfo + if (!deviceKeyInfo?.deviceKey) { + throw new CredoError('Device key info is missing') + } + + const publicDeviceJwk = COSEKey.import(deviceKeyInfo.deviceKey).toJWK() + + const deviceResponseBuilder = await DeviceResponse.from(mdoc) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .usingPresentationDefinition(presentationDefinition as any) + .usingSessionTranscriptForOID4VP(sessionTranscriptOptions) + .authenticateWithSignature(publicDeviceJwk, 'ES256') + + for (const [nameSpace, nameSpaceValue] of Object.entries(options.deviceNameSpaces ?? {})) { + deviceResponseBuilder.addDeviceNameSpace(nameSpace, nameSpaceValue) + } + + const deviceResponseMdoc = await deviceResponseBuilder.sign(getMdocContext(agentContext)) + + return { + deviceResponseBase64Url: TypedArrayEncoder.toBase64URL(deviceResponseMdoc.encode()), + presentationSubmission: MdocDeviceResponse.createPresentationSubmission({ + id: 'MdocPresentationSubmission ' + uuid(), + presentationDefinition, + }), + } + } + + public static async verify(agentContext: AgentContext, options: MdocDeviceResponseVerifyOptions) { + const verifier = new Verifier() + const mdocContext = getMdocContext(agentContext) + + const trustedCerts = + options?.trustedCertificates ?? agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates + + if (!trustedCerts) { + throw new MdocError('No trusted certificates found. Cannot verify mdoc.') + } + + const result = await verifier.verifyDeviceResponse( + { + encodedDeviceResponse: TypedArrayEncoder.fromBase64(options.deviceResponse), + ephemeralReaderKey: options.verifierKey ? getJwkFromKey(options.verifierKey).toJson() : undefined, + encodedSessionTranscript: DeviceResponse.calculateSessionTranscriptForOID4VP(options.sessionTranscriptOptions), + trustedCertificates: trustedCerts.map((cert) => X509Certificate.fromEncodedCertificate(cert).rawCertificate), + now: options.now, + }, + mdocContext + ) + + if (result.documentErrors.length > 1) { + throw new MdocError('Device response verification failed.') + } + + if (result.status !== MDocStatus.OK) { + throw new MdocError('Device response verification failed. An unknown error occurred.') + } + + return result.documents.map((doc) => Mdoc._interalFromIssuerSignedDocument(doc)) + } +} diff --git a/packages/core/src/modules/mdoc/MdocError.ts b/packages/core/src/modules/mdoc/MdocError.ts new file mode 100644 index 0000000000..ff9c8e49ab --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocError.ts @@ -0,0 +1,3 @@ +import { CredoError } from '../../error' + +export class MdocError extends CredoError {} diff --git a/packages/core/src/modules/mdoc/MdocModule.ts b/packages/core/src/modules/mdoc/MdocModule.ts new file mode 100644 index 0000000000..98b140581e --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocModule.ts @@ -0,0 +1,32 @@ +import type { Module, DependencyManager } from '../../plugins' + +import { AgentConfig } from '../../agent/AgentConfig' + +import { MdocApi } from './MdocApi' +import { MdocService } from './MdocService' +import { MdocRepository } from './repository' + +/** + * @public + */ +export class MdocModule implements Module { + public readonly api = MdocApi + + /** + * Registers the dependencies of the mdoc module on the dependency manager. + */ + public register(dependencyManager: DependencyManager) { + // Warn about experimental module + dependencyManager + .resolve(AgentConfig) + .logger.warn( + "The 'Mdoc' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @credo-ts packages." + ) + + // Services + dependencyManager.registerSingleton(MdocService) + + // Repositories + dependencyManager.registerSingleton(MdocRepository) + } +} diff --git a/packages/core/src/modules/mdoc/MdocOptions.ts b/packages/core/src/modules/mdoc/MdocOptions.ts new file mode 100644 index 0000000000..8a73a1391b --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocOptions.ts @@ -0,0 +1,56 @@ +import type { Mdoc } from './Mdoc' +import type { Key } from '../../crypto/Key' +import type { DifPresentationExchangeDefinition } from '../dif-presentation-exchange' +import type { ValidityInfo, MdocNameSpaces } from '@protokoll/mdoc-client' + +export type { MdocNameSpaces } from '@protokoll/mdoc-client' + +export type MdocVerifyOptions = { + trustedCertificates?: [string, ...string[]] + now?: Date +} + +export type MdocOpenId4VpSessionTranscriptOptions = { + responseUri: string + clientId: string + verifierGeneratedNonce: string + mdocGeneratedNonce: string +} + +export type MdocDeviceResponseOpenId4VpOptions = { + mdocs: [Mdoc, ...Mdoc[]] + presentationDefinition: DifPresentationExchangeDefinition + deviceNameSpaces?: MdocNameSpaces + sessionTranscriptOptions: MdocOpenId4VpSessionTranscriptOptions +} + +export type MdocDeviceResponseVerifyOptions = { + mdoc: Mdoc + trustedCertificates?: [string, ...string[]] + sessionTranscriptOptions: MdocOpenId4VpSessionTranscriptOptions + /** + * The base64Url-encoded device response string. + */ + deviceResponse: string + + /** + * The private part of the ephemeral key used in the session where the DeviceResponse was obtained. This is only required if the DeviceResponse is using the MAC method for device authentication. + */ + verifierKey?: Key + now?: Date +} + +export type MdocSignOptions = { + // eslint-disable-next-line @typescript-eslint/ban-types + docType: 'org.iso.18013.5.1.mDL' | (string & {}) + validityInfo?: Partial + namespaces: { [namespace: string]: Record } + + /** + * + * The trusted base64-encoded issuer certificate string in the DER-format. + */ + issuerCertificate: string + holderPublicKey: Key + issuerKey?: Key +} diff --git a/packages/core/src/modules/mdoc/MdocService.ts b/packages/core/src/modules/mdoc/MdocService.ts new file mode 100644 index 0000000000..df966f2b30 --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocService.ts @@ -0,0 +1,78 @@ +import type { + MdocSignOptions, + MdocDeviceResponseOpenId4VpOptions, + MdocDeviceResponseVerifyOptions, + MdocVerifyOptions, +} from './MdocOptions' +import type { Query, QueryOptions } from '../../storage/StorageService' + +import { injectable } from 'tsyringe' + +import { AgentContext } from '../../agent' + +import { Mdoc } from './Mdoc' +import { MdocDeviceResponse } from './MdocDeviceResponse' +import { MdocRecord, MdocRepository } from './repository' + +/** + * @internal + */ +@injectable() +export class MdocService { + private MdocRepository: MdocRepository + + public constructor(mdocRepository: MdocRepository) { + this.MdocRepository = mdocRepository + } + + public mdocFromBase64Url(hexEncodedMdoc: string) { + return Mdoc.fromBase64Url(hexEncodedMdoc) + } + + public createMdoc(agentContext: AgentContext, options: MdocSignOptions) { + return Mdoc.sign(agentContext, options) + } + + public async verifyMdoc(agentContext: AgentContext, mdoc: Mdoc, options: MdocVerifyOptions) { + return await mdoc.verify(agentContext, options) + } + + public async createDeviceResponse(agentContext: AgentContext, options: MdocDeviceResponseOpenId4VpOptions) { + return MdocDeviceResponse.openId4Vp(agentContext, options) + } + + public async verifyDeviceResponse(agentContext: AgentContext, options: MdocDeviceResponseVerifyOptions) { + return MdocDeviceResponse.verify(agentContext, options) + } + + public async store(agentContext: AgentContext, mdoc: Mdoc) { + const mdocRecord = new MdocRecord({ mdoc }) + await this.MdocRepository.save(agentContext, mdocRecord) + + return mdocRecord + } + + public async getById(agentContext: AgentContext, id: string): Promise { + return await this.MdocRepository.getById(agentContext, id) + } + + public async getAll(agentContext: AgentContext): Promise> { + return await this.MdocRepository.getAll(agentContext) + } + + public async findByQuery( + agentContext: AgentContext, + query: Query, + queryOptions?: QueryOptions + ): Promise> { + return await this.MdocRepository.findByQuery(agentContext, query, queryOptions) + } + + public async deleteById(agentContext: AgentContext, id: string) { + await this.MdocRepository.deleteById(agentContext, id) + } + + public async update(agentContext: AgentContext, mdocRecord: MdocRecord) { + await this.MdocRepository.update(agentContext, mdocRecord) + } +} diff --git a/packages/core/src/modules/mdoc/MdocVerifiablePresentation.ts b/packages/core/src/modules/mdoc/MdocVerifiablePresentation.ts new file mode 100644 index 0000000000..1637b2aaa7 --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocVerifiablePresentation.ts @@ -0,0 +1,3 @@ +export class MdocVerifiablePresentation { + public constructor(public readonly deviceSignedBase64Url: string) {} +} diff --git a/packages/core/src/modules/mdoc/__tests__/mdoc.deviceResponse.openid4vp.test.ts b/packages/core/src/modules/mdoc/__tests__/mdoc.deviceResponse.openid4vp.test.ts new file mode 100644 index 0000000000..6f0140d1f1 --- /dev/null +++ b/packages/core/src/modules/mdoc/__tests__/mdoc.deviceResponse.openid4vp.test.ts @@ -0,0 +1,305 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { Key as AskarKey, Jwk } from '@hyperledger/aries-askar-nodejs' +import { parseDeviceResponse } from '@protokoll/mdoc-client' + +import { Agent, KeyType } from '../../..' +import { getInMemoryAgentOptions } from '../../../../tests' +import { getJwkFromJson } from '../../../crypto/jose/jwk/transform' +import { Buffer, TypedArrayEncoder } from '../../../utils' +import { Mdoc } from '../Mdoc' +import { MdocDeviceResponse } from '../MdocDeviceResponse' + +const DEVICE_JWK_PUBLIC = { + kty: 'EC', + x: 'iBh5ynojixm_D0wfjADpouGbp6b3Pq6SuFHU3htQhVk', + y: 'oxS1OAORJ7XNUHNfVFGeM8E0RQVFxWA62fJj-sxW03c', + crv: 'P-256', + use: undefined, +} + +const DEVICE_JWK_PRIVATE = { + ...DEVICE_JWK_PUBLIC, + d: 'eRpAZr3eV5xMMnPG3kWjg90Y-bBff9LqmlQuk49HUtA', +} + +export const ISSUER_PRIVATE_KEY_JWK = { + kty: 'EC', + kid: '1234', + x: 'iTwtg0eQbcbNabf2Nq9L_VM_lhhPCq2s0Qgw2kRx29s', + y: 'YKwXDRz8U0-uLZ3NSI93R_35eNkl6jHp6Qg8OCup7VM', + crv: 'P-256', + d: 'o6PrzBm1dCfSwqJHW6DVqmJOCQSIAosrCPfbFJDMNp4', +} + +const ISSUER_CERTIFICATE = `-----BEGIN CERTIFICATE----- +MIICKjCCAdCgAwIBAgIUV8bM0wi95D7KN0TyqHE42ru4hOgwCgYIKoZIzj0EAwIw +UzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMQ8wDQYDVQQHDAZBbGJh +bnkxDzANBgNVBAoMBk5ZIERNVjEPMA0GA1UECwwGTlkgRE1WMB4XDTIzMDkxNDE0 +NTUxOFoXDTMzMDkxMTE0NTUxOFowUzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5l +dyBZb3JrMQ8wDQYDVQQHDAZBbGJhbnkxDzANBgNVBAoMBk5ZIERNVjEPMA0GA1UE +CwwGTlkgRE1WMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiTwtg0eQbcbNabf2 +Nq9L/VM/lhhPCq2s0Qgw2kRx29tgrBcNHPxTT64tnc1Ij3dH/fl42SXqMenpCDw4 +K6ntU6OBgTB/MB0GA1UdDgQWBBSrbS4DuR1JIkAzj7zK3v2TM+r2xzAfBgNVHSME +GDAWgBSrbS4DuR1JIkAzj7zK3v2TM+r2xzAPBgNVHRMBAf8EBTADAQH/MCwGCWCG +SAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAKBggqhkjO +PQQDAgNIADBFAiAJ/Qyrl7A+ePZOdNfc7ohmjEdqCvxaos6//gfTvncuqQIhANo4 +q8mKCA9J8k/+zh//yKbN1bLAtdqPx7dnrDqV3Lg+ +-----END CERTIFICATE-----` + +const PRESENTATION_DEFINITION_1 = { + id: 'mdl-test-all-data', + input_descriptors: [ + { + id: 'org.iso.18013.5.1.mDL', + format: { + mso_mdoc: { + alg: ['EdDSA', 'ES256'], + }, + }, + constraints: { + limit_disclosure: 'required', + fields: [ + { + path: ["$['org.iso.18013.5.1']['family_name']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['given_name']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['birth_date']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['issue_date']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['expiry_date']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['issuing_country']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['issuing_authority']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['issuing_jurisdiction']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['document_number']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['portrait']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['driving_privileges']"], + intent_to_retain: false, + }, + { + path: ["$['org.iso.18013.5.1']['un_distinguishing_sign']"], + intent_to_retain: false, + }, + ], + }, + }, + ], +} + +describe('mdoc device-response openid4vp test', () => { + let deviceResponse: string + let mdoc: Mdoc + let parsedDocument: Mdoc + + const verifierGeneratedNonce = 'abcdefg' + const mdocGeneratedNonce = '123456' + const clientId = 'Cq1anPb8vZU5j5C0d7hcsbuJLBpIawUJIDQRi2Ebwb4' + const responseUri = 'http://localhost:4000/api/presentation_request/dc8999df-d6ea-4c84-9985-37a8b81a82ec/callback' + + let agent: Agent + beforeEach(async () => { + agent = new Agent(getInMemoryAgentOptions('mdoc-test-agent', {})) + await agent.initialize() + + const devicePrivateAskar = AskarKey.fromJwk({ jwk: Jwk.fromJson(DEVICE_JWK_PRIVATE) }) + await agent.context.wallet.createKey({ + keyType: KeyType.P256, + privateKey: Buffer.from(devicePrivateAskar.secretBytes), + }) + + const issuerPrivateAskar = AskarKey.fromJwk({ jwk: Jwk.fromJson(ISSUER_PRIVATE_KEY_JWK) }) + const issuerPrivateKey = await agent.context.wallet.createKey({ + keyType: KeyType.P256, + privateKey: Buffer.from(issuerPrivateAskar.secretBytes), + }) + + // this is the ISSUER side + { + mdoc = await Mdoc.sign(agent.context, { + docType: 'org.iso.18013.5.1.mDL', + validityInfo: { + signed: new Date('2023-10-24'), + validUntil: new Date('2050-10-24'), + }, + holderPublicKey: getJwkFromJson(DEVICE_JWK_PUBLIC).key, + issuerCertificate: ISSUER_CERTIFICATE, + issuerKey: issuerPrivateKey, + namespaces: { + 'org.iso.18013.5.1': { + family_name: 'Jones', + given_name: 'Ava', + birth_date: '2007-03-25', + issue_date: '2023-09-01', + expiry_date: '2028-09-31', + issuing_country: 'US', + issuing_authority: 'NY DMV', + document_number: '01-856-5050', + portrait: 'bstr', + driving_privileges: [ + { + vehicle_category_code: 'C', + issue_date: '2023-09-01', + expiry_date: '2028-09-31', + }, + ], + un_distinguishing_sign: 'tbd-us.ny.dmv', + + sex: 'F', + height: '5\' 8"', + weight: '120lb', + eye_colour: 'brown', + hair_colour: 'brown', + resident_addres: '123 Street Rd', + resident_city: 'Brooklyn', + resident_state: 'NY', + resident_postal_code: '19001', + resident_country: 'US', + issuing_jurisdiction: 'New York', + }, + }, + }) + } + + // This is the Device side + { + const result = await MdocDeviceResponse.openId4Vp(agent.context, { + mdocs: [mdoc], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + presentationDefinition: PRESENTATION_DEFINITION_1 as any, + sessionTranscriptOptions: { + clientId, + responseUri, + verifierGeneratedNonce, + mdocGeneratedNonce, + }, + deviceNameSpaces: { + 'com.foobar-device': { test: 1234 }, + }, + }) + deviceResponse = result.deviceResponseBase64Url + + const parsed = parseDeviceResponse(TypedArrayEncoder.fromBase64(deviceResponse)) + expect(parsed.documents).toHaveLength(1) + parsedDocument = Mdoc._interalFromIssuerSignedDocument(parsed.documents[0]) + } + }) + + it('should be verifiable', async () => { + const res = await MdocDeviceResponse.verify(agent.context, { + deviceResponse, + trustedCertificates: [ISSUER_CERTIFICATE], + mdoc, + sessionTranscriptOptions: { + clientId, + responseUri, + verifierGeneratedNonce, + mdocGeneratedNonce, + }, + }) + expect(res).toHaveLength(1) + }) + + describe('should not be verifiable', () => { + ;[ + [ + 'clientId', + { + clientId: 'wrong', + responseUri, + verifierGeneratedNonce, + mdocGeneratedNonce, + }, + ] as const, + [ + 'responseUri', + { + clientId, + responseUri: 'wrong', + verifierGeneratedNonce, + mdocGeneratedNonce, + }, + ] as const, + [ + 'verifierGeneratedNonce', + { + clientId, + responseUri, + verifierGeneratedNonce: 'wrong', + mdocGeneratedNonce, + }, + ] as const, + [ + 'mdocGeneratedNonce', + { + clientId, + responseUri, + verifierGeneratedNonce, + mdocGeneratedNonce: 'wrong', + }, + ] as const, + ].forEach(([name, values]) => { + it(`with a different ${name}`, async () => { + try { + await MdocDeviceResponse.verify(agent.context, { + mdoc, + trustedCertificates: [ISSUER_CERTIFICATE], + deviceResponse, + sessionTranscriptOptions: { + clientId: values.clientId, + responseUri: values.responseUri, + verifierGeneratedNonce: values.verifierGeneratedNonce, + mdocGeneratedNonce: values.mdocGeneratedNonce, + }, + }) + throw new Error('should not validate with different transcripts') + } catch (error) { + expect((error as Error).message).toMatch( + 'Unable to verify deviceAuth signature (ECDSA/EdDSA): Device signature must be valid' + ) + } + }) + }) + }) + + it('should contain the validity info', () => { + expect(parsedDocument.validityInfo).toBeDefined() + expect(parsedDocument.validityInfo.signed).toEqual(new Date('2023-10-24')) + expect(parsedDocument.validityInfo.validFrom).toEqual(new Date('2023-10-24')) + expect(parsedDocument.validityInfo.validUntil).toEqual(new Date('2050-10-24')) + }) + + it('should contain the device namespaces', () => { + expect(parsedDocument.deviceSignedNamespaces).toEqual({ + 'com.foobar-device': { + test: 1234, + }, + }) + }) +}) diff --git a/packages/core/src/modules/mdoc/__tests__/mdoc.deviceResponse.test.ts b/packages/core/src/modules/mdoc/__tests__/mdoc.deviceResponse.test.ts new file mode 100644 index 0000000000..5609c93ff0 --- /dev/null +++ b/packages/core/src/modules/mdoc/__tests__/mdoc.deviceResponse.test.ts @@ -0,0 +1,82 @@ +import { Optionality } from '@sphereon/pex-models' + +import { Agent, KeyType, X509Service } from '../../..' +import { getInMemoryAgentOptions } from '../../../../tests' +import { Mdoc } from '../Mdoc' +import { MdocDeviceResponse } from '../MdocDeviceResponse' + +describe('mdoc device-response test', () => { + const agent = new Agent(getInMemoryAgentOptions('mdoc-test-agent', {})) + beforeEach(async () => { + await agent.initialize() + }) + + test('can limit the disclosure', async () => { + const holderKey = await agent.context.wallet.createKey({ + keyType: KeyType.P256, + }) + const issuerKey = await agent.context.wallet.createKey({ + keyType: KeyType.P256, + }) + + const currentDate = new Date() + currentDate.setDate(currentDate.getDate() - 1) + const nextDay = new Date(currentDate) + nextDay.setDate(currentDate.getDate() + 2) + + const selfSignedCertificate = await X509Service.createSelfSignedCertificate(agent.context, { + key: issuerKey, + notBefore: currentDate, + notAfter: nextDay, + extensions: [], + }) + + const issuerCertificate = selfSignedCertificate.toString('pem') + + const mdoc = await Mdoc.sign(agent.context, { + docType: 'org.iso.18013.5.1.mDL', + holderPublicKey: holderKey, + namespaces: { + hello: { + world: 'from-mdoc', + secret: 'value', + nicer: 'dicer', + }, + }, + issuerCertificate, + }) + + const limitedDisclosedPayload = MdocDeviceResponse.limitDisclosureToInputDescriptor({ + mdoc, + inputDescriptor: { + id: mdoc.docType, + format: { + mso_mdoc: { + alg: ['ES256'], + }, + }, + constraints: { + limit_disclosure: Optionality.Required, + fields: [ + { + path: ["$['hello']['world']"], + intent_to_retain: true, + }, + { + path: ["$['hello']['nicer']"], + intent_to_retain: false, + }, + ], + }, + }, + }) + + expect(Object.keys(limitedDisclosedPayload)).toHaveLength(1) + expect(limitedDisclosedPayload.hello).toBeDefined() + expect(limitedDisclosedPayload.hello).toHaveLength(2) + expect(limitedDisclosedPayload.hello[0].elementIdentifier).toEqual('world') + expect(limitedDisclosedPayload.hello[0].elementValue).toEqual('from-mdoc') + expect(limitedDisclosedPayload.hello[1].elementIdentifier).toEqual('nicer') + expect(limitedDisclosedPayload.hello[1].elementValue).toEqual('dicer') + }) +}) diff --git a/packages/core/src/modules/mdoc/__tests__/mdoc.fixtures.ts b/packages/core/src/modules/mdoc/__tests__/mdoc.fixtures.ts new file mode 100644 index 0000000000..2ec6c27298 --- /dev/null +++ b/packages/core/src/modules/mdoc/__tests__/mdoc.fixtures.ts @@ -0,0 +1,65 @@ +export const sprindFunkeTestVectorBase64Url = + 'omppc3N1ZXJBdXRohEOhASahGCGCWQJ4MIICdDCCAhugAwIBAgIBAjAKBggqhkjOPQQDAjCBiDELMAkGA1UEBhMCREUxDzANBgNVBAcMBkJlcmxpbjEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxETAPBgNVBAsMCFQgQ1MgSURFMTYwNAYDVQQDDC1TUFJJTkQgRnVua2UgRVVESSBXYWxsZXQgUHJvdG90eXBlIElzc3VpbmcgQ0EwHhcNMjQwNTMxMDgxMzE3WhcNMjUwNzA1MDgxMzE3WjBsMQswCQYDVQQGEwJERTEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxCjAIBgNVBAsMAUkxMjAwBgNVBAMMKVNQUklORCBGdW5rZSBFVURJIFdhbGxldCBQcm90b3R5cGUgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOFBq4YMKg4w5fTifsytwBuJf_7E7VhRPXiNm52S3q1ETIgBdXyDK3kVxGxgeHPivLP3uuMvS6iDEc7qMxmvduKOBkDCBjTAdBgNVHQ4EFgQUiPhCkLErDXPLW2_J0WVeghyw-mIwDAYDVR0TAQH_BAIwADAOBgNVHQ8BAf8EBAMCB4AwLQYDVR0RBCYwJIIiZGVtby5waWQtaXNzdWVyLmJ1bmRlc2RydWNrZXJlaS5kZTAfBgNVHSMEGDAWgBTUVhjAiTjoDliEGMl2Yr-ru8WQvjAKBggqhkjOPQQDAgNHADBEAiAbf5TzkcQzhfWoIoyi1VN7d8I9BsFKm1MWluRph2byGQIgKYkdrNf2xXPjVSbjW_U_5S5vAEC5XxcOanusOBroBbVZAn0wggJ5MIICIKADAgECAhQHkT1BVm2ZRhwO0KMoH8fdVC_vaDAKBggqhkjOPQQDAjCBiDELMAkGA1UEBhMCREUxDzANBgNVBAcMBkJlcmxpbjEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxETAPBgNVBAsMCFQgQ1MgSURFMTYwNAYDVQQDDC1TUFJJTkQgRnVua2UgRVVESSBXYWxsZXQgUHJvdG90eXBlIElzc3VpbmcgQ0EwHhcNMjQwNTMxMDY0ODA5WhcNMzQwNTI5MDY0ODA5WjCBiDELMAkGA1UEBhMCREUxDzANBgNVBAcMBkJlcmxpbjEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxETAPBgNVBAsMCFQgQ1MgSURFMTYwNAYDVQQDDC1TUFJJTkQgRnVua2UgRVVESSBXYWxsZXQgUHJvdG90eXBlIElzc3VpbmcgQ0EwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARgbN3AUOdzv4qfmJsC8I4zyR7vtVDGp8xzBkvwhogD5YJE5wJ-Zj-CIf3aoyu7mn-TI6K8TREL8ht0w428OhTJo2YwZDAdBgNVHQ4EFgQU1FYYwIk46A5YhBjJdmK_q7vFkL4wHwYDVR0jBBgwFoAU1FYYwIk46A5YhBjJdmK_q7vFkL4wEgYDVR0TAQH_BAgwBgEB_wIBADAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwIDRwAwRAIgYSbvCRkoe39q1vgx0WddbrKufAxRPa7XfqB22XXRjqECIG5MWq9Vi2HWtvHMI_TFZkeZAr2RXLGfwY99fbsQjPOzWQRD2BhZBD6mZ2RvY1R5cGV3ZXUuZXVyb3BhLmVjLmV1ZGkucGlkLjFndmVyc2lvbmMxLjBsdmFsaWRpdHlJbmZvo2ZzaWduZWTAdDIwMjQtMDgtMTJUMTQ6NDk6NDJaaXZhbGlkRnJvbcB0MjAyNC0wOC0xMlQxNDo0OTo0MlpqdmFsaWRVbnRpbMB0MjAyNC0wOC0yNlQxNDo0OTo0MlpsdmFsdWVEaWdlc3RzoXdldS5ldXJvcGEuZWMuZXVkaS5waWQuMbYAWCC9r57n5-m6adygJJlxp5_XTAl8HplzngbppD01aqPm4QFYIBLw6crU9ONkaf7fkBFsRjQKzo_VrKPAYe0Bb0tmyz7ZAlggEqFksnSgEwJX8YahuUBuvjCvahAGnaBdk2qujSqpv-sDWCDkvVFFIzukd7PLRl0u7BdQ7QsgotN9HW-UuU9blZIdtwRYIEJwqwvBhBbZ-Fn37z3QeUVSHOj1GqOXKZwQVHgaLjQmBVggOXbVdn-8O8EMQAu8eSjFEx65vwWi45S0IXqUv-KKpNIGWCAhC1Ro14maILGe6rslCRA92TGb5UOl69fgsxRZhKSpNwdYIJxlHX2GIjrDAcK7PtJW12fWsEuE-tcSRzy0PeXgKeoTCFggV2cGcdPY4TCE4ZmITgB_yI2Qp4yqAX5N1IsEGa3DvboJWCCc5-atr9b0EiIMu8sJokyJT-Fj7SUBOYdXBRNkXiUUlApYIF2AAJGSEH4IIKpOJOaOlctv53zEEZ8ox7v7FlGPu9NKC1ggIYD5k0hQ7Ps2qlA-rxFqn4126RNG8u6n5Y12Q8B6hOUMWCAGyyMXqo_TEDOV6P0ZboHklUEPnUSPhppLnQ9RrcVzaA1YIGVLx6dVsoryOaNIrzkoVy8_MuiEQsUM3DSaBXxz6UIKDlggvuphScOPS5u1wrfY6OdoMHfetBfv_cSLcABmhS1p1nYPWCCTl-eeiWAugnVjip2YHOILjltZA7-R5jI9ciCTDpzk5hBYICm2XSJ5h_zorI6Dy5AlEcWPDU3FMg-d4KkYwddDFAT_EVggT0gN3xQ65XLUqpTkOzW7rCWhM29p0fmccFriFcjZRNUSWCAto46b8l3wCNN474AO6KlAxb6-_qvXLUip_hDrxPL0cxNYINs6L4scl4zD1VEzcBpiSoy-lrqiBT65I9pdq3S75N46FFggHxExDDKOkKN2GhItHmzyTmpS4xiQ0Tkw_nDr__Rg0FUVWCCuTGT6-ihKu-E3He_M9Yxz5sYg30g5AMLrt5dFk2y9Am1kZXZpY2VLZXlJbmZvoWlkZXZpY2VLZXmkAQIgASFYIG8Ne8ve1xptY9p_0JJfTnao3ZyarzfWHmbBHPsQydSPIlggpAG-y7pm3b_QWGdCXVg5ZkMRQXQYpkIe5hAIIrVTTytvZGlnZXN0QWxnb3JpdGhtZ1NIQS0yNTZYQBVjlI0_abnOyOetdAQDwMDpqDuYYAWu3GpNwf0nH5qRf7wzIcu6d-OwczpsV67r8cMPFn-SL_gk1sxaK8t-gY9qbmFtZVNwYWNlc6F3ZXUuZXVyb3BhLmVjLmV1ZGkucGlkLjGW2BhYVqRmcmFuZG9tUHEhN2kuf12OYaKToDuH4P9oZGlnZXN0SUQAbGVsZW1lbnRWYWx1ZWJERXFlbGVtZW50SWRlbnRpZmllcnByZXNpZGVudF9jb3VudHJ52BhYXaRmcmFuZG9tUCgO1EKXxF6VNoulQ66hh3VoZGlnZXN0SUQBbGVsZW1lbnRWYWx1ZWU1MTE0N3FlbGVtZW50SWRlbnRpZmllcnRyZXNpZGVudF9wb3N0YWxfY29kZdgYWFSkZnJhbmRvbVAnBBAzmk2aU5CQkQsWS_kWaGRpZ2VzdElEAmxlbGVtZW50VmFsdWUZB8BxZWxlbWVudElkZW50aWZpZXJuYWdlX2JpcnRoX3llYXLYGFhPpGZyYW5kb21QNYLVHLGTsQVyobJnPCPo2GhkaWdlc3RJRANsZWxlbWVudFZhbHVl9XFlbGVtZW50SWRlbnRpZmllcmthZ2Vfb3Zlcl8xONgYWFakZnJhbmRvbVAwW0rn-cCj5HLb4wU3mFUgaGRpZ2VzdElEBGxlbGVtZW50VmFsdWVlS8OWTE5xZWxlbWVudElkZW50aWZpZXJtcmVzaWRlbnRfY2l0edgYWE-kZnJhbmRvbVDTjniWHGc-zyDfFcOugtzlaGRpZ2VzdElEBWxlbGVtZW50VmFsdWX1cWVsZW1lbnRJZGVudGlmaWVya2FnZV9vdmVyXzE22BhYYqRmcmFuZG9tUJJ1bPoRCPNn5MFSIlwl5hRoZGlnZXN0SUQGbGVsZW1lbnRWYWx1ZW9IRUlERVNUUkFTU0UgMTdxZWxlbWVudElkZW50aWZpZXJvcmVzaWRlbnRfc3RyZWV02BhYWKRmcmFuZG9tUGx2TWa2eitcmEGxaX_gpFZoZGlnZXN0SUQHbGVsZW1lbnRWYWx1ZWoxOTg0LTAxLTI2cWVsZW1lbnRJZGVudGlmaWVyamJpcnRoX2RhdGXYGFhspGZyYW5kb21QNwbzolJxH5q6muxF60rQvWhkaWdlc3RJRAhsZWxlbWVudFZhbHVlomV2YWx1ZWJERWtjb3VudHJ5TmFtZWdHZXJtYW55cWVsZW1lbnRJZGVudGlmaWVya25hdGlvbmFsaXR52BhYT6RmcmFuZG9tUO-yJI9Dsuyiae8wPe1cvpdoZGlnZXN0SUQJbGVsZW1lbnRWYWx1ZfVxZWxlbWVudElkZW50aWZpZXJrYWdlX292ZXJfMjHYGFhVpGZyYW5kb21QGfpq4ykqTt_fDPVmEQShq2hkaWdlc3RJRApsZWxlbWVudFZhbHVlYkRFcWVsZW1lbnRJZGVudGlmaWVyb2lzc3VpbmdfY291bnRyedgYWFekZnJhbmRvbVDsjptGsOLGOuOwPppYmPNlaGRpZ2VzdElEC2xlbGVtZW50VmFsdWViREVxZWxlbWVudElkZW50aWZpZXJxaXNzdWluZ19hdXRob3JpdHnYGFhbpGZyYW5kb21QURhliFa8BCP_O5jhnNiESGhkaWdlc3RJRAxsZWxlbWVudFZhbHVlZkdBQkxFUnFlbGVtZW50SWRlbnRpZmllcnFmYW1pbHlfbmFtZV9iaXJ0aNgYWE-kZnJhbmRvbVAawjmoQYQSPbPL6VbjcNF-aGRpZ2VzdElEDWxlbGVtZW50VmFsdWX1cWVsZW1lbnRJZGVudGlmaWVya2FnZV9vdmVyXzE02BhYVaRmcmFuZG9tUBA_hZquh2Ijw0U_IGqCDudoZGlnZXN0SUQObGVsZW1lbnRWYWx1ZWZCRVJMSU5xZWxlbWVudElkZW50aWZpZXJrYmlydGhfcGxhY2XYGFhZpGZyYW5kb21QTiyAd2V_Ecv0u6UzXCkl7WhkaWdlc3RJRA9sZWxlbWVudFZhbHVlak1VU1RFUk1BTk5xZWxlbWVudElkZW50aWZpZXJrZmFtaWx5X25hbWXYGFhTpGZyYW5kb21QEJxB3fr8hop0-boRDfWdN2hkaWdlc3RJRBBsZWxlbWVudFZhbHVlZUVSSUtBcWVsZW1lbnRJZGVudGlmaWVyamdpdmVuX25hbWXYGFhPpGZyYW5kb21Q77KAq5Owg2xvgzLWQgWKU2hkaWdlc3RJRBFsZWxlbWVudFZhbHVl9XFlbGVtZW50SWRlbnRpZmllcmthZ2Vfb3Zlcl8xMtgYWGukZnJhbmRvbVDcWl_JEvrUn2HcXbk91CeaaGRpZ2VzdElEEmxlbGVtZW50VmFsdWXAeBgyMDI0LTA4LTEyVDE0OjQ5OjQyLjEyNFpxZWxlbWVudElkZW50aWZpZXJtaXNzdWFuY2VfZGF0ZdgYWGmkZnJhbmRvbVAYOq7D1aD0NJ7XaaSS6ARJaGRpZ2VzdElEE2xlbGVtZW50VmFsdWXAeBgyMDI0LTA4LTI2VDE0OjQ5OjQyLjEyNFpxZWxlbWVudElkZW50aWZpZXJrZXhwaXJ5X2RhdGXYGFhRpGZyYW5kb21Q89JVE6aHaufqWF9OqTxpmWhkaWdlc3RJRBRsZWxlbWVudFZhbHVlGChxZWxlbWVudElkZW50aWZpZXJsYWdlX2luX3llYXJz2BhYT6RmcmFuZG9tUCnjB_bOLqoRtc72SrV68ddoZGlnZXN0SUQVbGVsZW1lbnRWYWx1ZfRxZWxlbWVudElkZW50aWZpZXJrYWdlX292ZXJfNjU' + +export const sprindFunkeX509TrustedCertificate = + 'MIICdDCCAhugAwIBAgIBAjAKBggqhkjOPQQDAjCBiDELMAkGA1UEBhMCREUxDzANBgNVBAcMBkJlcmxpbjEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxETAPBgNVBAsMCFQgQ1MgSURFMTYwNAYDVQQDDC1TUFJJTkQgRnVua2UgRVVESSBXYWxsZXQgUHJvdG90eXBlIElzc3VpbmcgQ0EwHhcNMjQwNTMxMDgxMzE3WhcNMjUwNzA1MDgxMzE3WjBsMQswCQYDVQQGEwJERTEdMBsGA1UECgwUQnVuZGVzZHJ1Y2tlcmVpIEdtYkgxCjAIBgNVBAsMAUkxMjAwBgNVBAMMKVNQUklORCBGdW5rZSBFVURJIFdhbGxldCBQcm90b3R5cGUgSXNzdWVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOFBq4YMKg4w5fTifsytwBuJf/7E7VhRPXiNm52S3q1ETIgBdXyDK3kVxGxgeHPivLP3uuMvS6iDEc7qMxmvduKOBkDCBjTAdBgNVHQ4EFgQUiPhCkLErDXPLW2/J0WVeghyw+mIwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCB4AwLQYDVR0RBCYwJIIiZGVtby5waWQtaXNzdWVyLmJ1bmRlc2RydWNrZXJlaS5kZTAfBgNVHSMEGDAWgBTUVhjAiTjoDliEGMl2Yr+ru8WQvjAKBggqhkjOPQQDAgNHADBEAiAbf5TzkcQzhfWoIoyi1VN7d8I9BsFKm1MWluRph2byGQIgKYkdrNf2xXPjVSbjW/U/5S5vAEC5XxcOanusOBroBbU=' + +export const iso18013_5_IssuerAuthTestVector = + '8443a10126a118215901f3308201ef30820195a00302010202143c4416eed784f3b413e48f56f075abfa6d87e' + + 'b84300a06082a8648ce3d04030230233114301206035504030c0b75746f7069612069616361310b3009060355' + + '040613025553301e170d3230313030313030303030305a170d3231313030313030303030305a302131123010' + + '06035504030c0975746f706961206473310b30090603550406130255533059301306072a8648ce3d020106082' + + 'a8648ce3d03010703420004ace7ab7340e5d9648c5a72a9a6f56745c7aad436a03a43efea77b5fa7b88f0197d' + + '57d8983e1b37d3a539f4d588365e38cbbf5b94d68c547b5bc8731dcd2f146ba381a83081a5301e0603551d120' + + '417301581136578616d706c65406578616d706c652e636f6d301c0603551d1f041530133011a00fa00d820b65' + + '78616d706c652e636f6d301d0603551d0e0416041414e29017a6c35621ffc7a686b7b72db06cd12351301f0603' + + '551d2304183016801454fa2383a04c28e0d930792261c80c4881d2c00b300e0603551d0f0101ff040403020780' + + '30150603551d250101ff040b3009060728818c5d050102300a06082a8648ce3d04030203480030450221009771' + + '7ab9016740c8d7bcdaa494a62c053bbdecce1383c1aca72ad08dbc04cbb202203bad859c13a63c6d1ad67d814d' + + '43e2425caf90d422422c04a8ee0304c0d3a68d5903a2d81859039da66776657273696f6e63312e306f64696765' + + '7374416c676f726974686d675348412d3235366c76616c756544696765737473a2716f72672e69736f2e313830' + + '31332e352e31ad00582075167333b47b6c2bfb86eccc1f438cf57af055371ac55e1e359e20f254adcebf015820' + + '67e539d6139ebd131aef441b445645dd831b2b375b390ca5ef6279b205ed45710258203394372ddb78053f36d5' + + 'd869780e61eda313d44a392092ad8e0527a2fbfe55ae0358202e35ad3c4e514bb67b1a9db51ce74e4cb9b7146e' + + '41ac52dac9ce86b8613db555045820ea5c3304bb7c4a8dcb51c4c13b65264f845541341342093cca786e058fac' + + '2d59055820fae487f68b7a0e87a749774e56e9e1dc3a8ec7b77e490d21f0e1d3475661aa1d0658207d83e507ae' + + '77db815de4d803b88555d0511d894c897439f5774056416a1c7533075820f0549a145f1cf75cbeeffa881d4857d' + + 'd438d627cf32174b1731c4c38e12ca936085820b68c8afcb2aaf7c581411d2877def155be2eb121a42bc9ba5b7' + + '312377e068f660958200b3587d1dd0c2a07a35bfb120d99a0abfb5df56865bb7fa15cc8b56a66df6e0c0a5820c' + + '98a170cf36e11abb724e98a75a5343dfa2b6ed3df2ecfbb8ef2ee55dd41c8810b5820b57dd036782f7b14c6a30' + + 'faaaae6ccd5054ce88bdfa51a016ba75eda1edea9480c5820651f8736b18480fe252a03224ea087b5d10ca5485' + + '146c67c74ac4ec3112d4c3a746f72672e69736f2e31383031332e352e312e5553a4005820d80b83d25173c484c' + + '5640610ff1a31c949c1d934bf4cf7f18d5223b15dd4f21c0158204d80e1e2e4fb246d97895427ce7000bb59bb24' + + 'c8cd003ecf94bf35bbd2917e340258208b331f3b685bca372e85351a25c9484ab7afcdf0d2233105511f778d98' + + 'c2f544035820c343af1bd1690715439161aba73702c474abf992b20c9fb55c36a336ebe01a876d646576696365' + + '4b6579496e666fa1696465766963654b6579a40102200121582096313d6c63e24e3372742bfdb1a33ba2c897dc' + + 'd68ab8c753e4fbd48dca6b7f9a2258201fb3269edd418857de1b39a4e4a44b92fa484caa722c228288f01d0c03' + + 'a2c3d667646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c6964697479496e66' + + '6fa3667369676e6564c074323032302d31302d30315431333a33303a30325a6976616c696446726f6dc0743230' + + '32302d31302d30315431333a33303a30325a6a76616c6964556e74696cc074323032312d31302d30315431333a' + + '33303a30325a584059e64205df1e2f708dd6db0847aed79fc7c0201d80fa55badcaf2e1bcf5902e1e5a62e4832' + + '044b890ad85aa53f129134775d733754d7cb7a413766aeff13cb2e'.replace(' ', '') + +export const iso18013_5_SignatureStructureTestVector = + '846a5369676e61747572653143a10126405903a2d81859039da66776657273696f6e63312e3' + + '06f646967657374416c676f726974686d675348412d3235366c76616c756544696765737473a2716f72672e697' + + '36f2e31383031332e352e31ad00582075167333b47b6c2bfb86eccc1f438cf57af055371ac55e1e359e20f254a' + + 'dcebf01582067e539d6139ebd131aef441b445645dd831b2b375b390ca5ef6279b205ed45710258203394372dd' + + 'b78053f36d5d869780e61eda313d44a392092ad8e0527a2fbfe55ae0358202e35ad3c4e514bb67b1a9db51ce74' + + 'e4cb9b7146e41ac52dac9ce86b8613db555045820ea5c3304bb7c4a8dcb51c4c13b65264f845541341342093cc' + + 'a786e058fac2d59055820fae487f68b7a0e87a749774e56e9e1dc3a8ec7b77e490d21f0e1d3475661aa1d06582' + + '07d83e507ae77db815de4d803b88555d0511d894c897439f5774056416a1c7533075820f0549a145f1cf75cbee' + + 'ffa881d4857dd438d627cf32174b1731c4c38e12ca936085820b68c8afcb2aaf7c581411d2877def155be2eb121' + + 'a42bc9ba5b7312377e068f660958200b3587d1dd0c2a07a35bfb120d99a0abfb5df56865bb7fa15cc8b56a66df' + + '6e0c0a5820c98a170cf36e11abb724e98a75a5343dfa2b6ed3df2ecfbb8ef2ee55dd41c8810b5820b57dd03678' + + '2f7b14c6a30faaaae6ccd5054ce88bdfa51a016ba75eda1edea9480c5820651f8736b18480fe252a03224ea087' + + 'b5d10ca5485146c67c74ac4ec3112d4c3a746f72672e69736f2e31383031332e352e312e5553a4005820d80b83' + + 'd25173c484c5640610ff1a31c949c1d934bf4cf7f18d5223b15dd4f21c0158204d80e1e2e4fb246d97895427ce7' + + '000bb59bb24c8cd003ecf94bf35bbd2917e340258208b331f3b685bca372e85351a25c9484ab7afcdf0d223310' + + '5511f778d98c2f544035820c343af1bd1690715439161aba73702c474abf992b20c9fb55c36a336ebe01a876d6' + + '465766963654b6579496e666fa1696465766963654b6579a40102200121582096313d6c63e24e3372742bfdb1a' + + '33ba2c897dcd68ab8c753e4fbd48dca6b7f9a2258201fb3269edd418857de1b39a4e4a44b92fa484caa722c228' + + '288f01d0c03a2c3d667646f6354797065756f72672e69736f2e31383031332e352e312e6d444c6c76616c69646' + + '97479496e666fa3667369676e6564c074323032302d31302d30315431333a33303a30325a6976616c696446726' + + 'f6dc074323032302d31302d30315431333a33303a30325a6a76616c6964556e74696cc074323032312d31302d3' + + '0315431333a33303a30325a'.replace(' ', '') diff --git a/packages/core/src/modules/mdoc/__tests__/mdoc.service.test.ts b/packages/core/src/modules/mdoc/__tests__/mdoc.service.test.ts new file mode 100644 index 0000000000..c6cd67ae4d --- /dev/null +++ b/packages/core/src/modules/mdoc/__tests__/mdoc.service.test.ts @@ -0,0 +1,132 @@ +import type { AgentContext } from '../../..' + +import { KeyType, X509Service } from '../../..' +import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' +import { getAgentConfig, getAgentContext } from '../../../../tests' +import { Mdoc } from '../Mdoc' + +import { sprindFunkeTestVectorBase64Url, sprindFunkeX509TrustedCertificate } from './mdoc.fixtures' + +describe('mdoc service test', () => { + let wallet: InMemoryWallet + let agentContext: AgentContext + + beforeAll(async () => { + const agentConfig = getAgentConfig('mdoc') + wallet = new InMemoryWallet() + agentContext = getAgentContext({ wallet }) + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + await wallet.createAndOpen(agentConfig.walletConfig!) + }) + + test('can get issuer-auth protected-header alg', async () => { + const mdoc = Mdoc.fromBase64Url(sprindFunkeTestVectorBase64Url) + expect(mdoc.alg).toBe('ES256') + }) + + test('can get doctype', async () => { + const mdoc = Mdoc.fromBase64Url(sprindFunkeTestVectorBase64Url) + expect(mdoc.docType).toBe('eu.europa.ec.eudi.pid.1') + }) + + test('can create and verify mdoc', async () => { + const holderKey = await agentContext.wallet.createKey({ + keyType: KeyType.P256, + }) + const issuerKey = await agentContext.wallet.createKey({ + keyType: KeyType.P256, + }) + + const currentDate = new Date() + currentDate.setDate(currentDate.getDate() - 1) + const nextDay = new Date(currentDate) + nextDay.setDate(currentDate.getDate() + 2) + + const selfSignedCertificate = await X509Service.createSelfSignedCertificate(agentContext, { + key: issuerKey, + notBefore: currentDate, + notAfter: nextDay, + extensions: [], + countryName: 'DE', + }) + + const issuerCertificate = selfSignedCertificate.toString('pem') + + const mdoc = await Mdoc.sign(agentContext, { + docType: 'org.iso.18013.5.1.mDL', + holderPublicKey: holderKey, + namespaces: { + hello: { + world: 'world', + nicer: 'dicer', + }, + }, + issuerCertificate, + issuerKey, + }) + + expect(mdoc.alg).toBe('ES256') + expect(mdoc.docType).toBe('org.iso.18013.5.1.mDL') + expect(mdoc.issuerSignedNamespaces).toStrictEqual({ + hello: { + world: 'world', + nicer: 'dicer', + }, + }) + + expect(() => mdoc.deviceSignedNamespaces).toThrow() + + const { isValid } = await mdoc.verify(agentContext, { + trustedCertificates: [selfSignedCertificate.toString('base64')], + }) + expect(isValid).toBeTruthy() + }) + + test('can decode claims from namespaces', async () => { + const mdoc = Mdoc.fromBase64Url(sprindFunkeTestVectorBase64Url) + const namespaces = mdoc.issuerSignedNamespaces + expect(Object.entries(namespaces)).toHaveLength(1) + + expect(namespaces).toBeDefined() + const eudiPidNamespace = namespaces['eu.europa.ec.eudi.pid.1'] + expect(eudiPidNamespace).toBeDefined() + expect(eudiPidNamespace).toStrictEqual({ + resident_country: 'DE', + age_over_12: true, + family_name_birth: 'GABLER', + given_name: 'ERIKA', + age_birth_year: 1984, + age_over_18: true, + age_over_21: true, + resident_city: 'KÖLN', + family_name: 'MUSTERMANN', + birth_place: 'BERLIN', + expiry_date: new Date('2024-08-26T14:49:42.124Z'), + issuing_country: 'DE', + age_over_65: false, + issuance_date: new Date('2024-08-12T14:49:42.124Z'), + resident_street: 'HEIDESTRASSE 17', + age_over_16: true, + resident_postal_code: '51147', + birth_date: '1984-01-26', + issuing_authority: 'DE', + age_over_14: true, + age_in_years: 40, + nationality: new Map([ + ['value', 'DE'], + ['countryName', 'Germany'], + ]), + }) + }) + + test('can verify sprindFunkeTestVector Issuer Signed', async () => { + const mdoc = Mdoc.fromBase64Url(sprindFunkeTestVectorBase64Url) + const now = new Date('2024-08-12T14:50:42.124Z') + const { isValid } = await mdoc.verify(agentContext, { + trustedCertificates: [sprindFunkeX509TrustedCertificate], + now, + }) + expect(isValid).toBeTruthy() + }) +}) diff --git a/packages/core/src/modules/mdoc/index.ts b/packages/core/src/modules/mdoc/index.ts new file mode 100644 index 0000000000..0944642cd2 --- /dev/null +++ b/packages/core/src/modules/mdoc/index.ts @@ -0,0 +1,8 @@ +export * from './MdocApi' +export * from './MdocModule' +export * from './MdocService' +export * from './MdocError' +export * from './MdocOptions' +export * from './repository' +export * from './Mdoc' +export * from './MdocVerifiablePresentation' diff --git a/packages/core/src/modules/mdoc/repository/MdocRecord.ts b/packages/core/src/modules/mdoc/repository/MdocRecord.ts new file mode 100644 index 0000000000..afcceead8e --- /dev/null +++ b/packages/core/src/modules/mdoc/repository/MdocRecord.ts @@ -0,0 +1,58 @@ +import type { TagsBase } from '../../../storage/BaseRecord' +import type { Constructable } from '../../../utils/mixins' + +import { type JwaSignatureAlgorithm } from '../../../crypto' +import { BaseRecord } from '../../../storage/BaseRecord' +import { JsonTransformer } from '../../../utils' +import { uuid } from '../../../utils/uuid' +import { Mdoc } from '../Mdoc' + +export type DefaultMdocRecordTags = { + docType: string + + /** + * + * The Jwa Signature Algorithm used to sign the Mdoc. + */ + alg: JwaSignatureAlgorithm +} + +export type MdocRecordStorageProps = { + id?: string + createdAt?: Date + tags?: TagsBase + mdoc: Mdoc +} + +export class MdocRecord extends BaseRecord { + public static readonly type = 'MdocRecord' + public readonly type = MdocRecord.type + public base64Url!: string + + public constructor(props: MdocRecordStorageProps) { + super() + + if (props) { + this.id = props.id ?? uuid() + this.createdAt = props.createdAt ?? new Date() + this.base64Url = props.mdoc.base64Url + this._tags = props.tags ?? {} + } + } + + public getTags() { + const mdoc = Mdoc.fromBase64Url(this.base64Url) + const docType = mdoc.docType + const alg = mdoc.alg + + return { + ...this._tags, + docType, + alg, + } + } + + public clone(): this { + return JsonTransformer.fromJSON(JsonTransformer.toJSON(this), this.constructor as Constructable) + } +} diff --git a/packages/core/src/modules/mdoc/repository/MdocRepository.ts b/packages/core/src/modules/mdoc/repository/MdocRepository.ts new file mode 100644 index 0000000000..885ece6db0 --- /dev/null +++ b/packages/core/src/modules/mdoc/repository/MdocRepository.ts @@ -0,0 +1,17 @@ +import { EventEmitter } from '../../../agent/EventEmitter' +import { InjectionSymbols } from '../../../constants' +import { inject, injectable } from '../../../plugins' +import { Repository } from '../../../storage/Repository' +import { StorageService } from '../../../storage/StorageService' + +import { MdocRecord } from './MdocRecord' + +@injectable() +export class MdocRepository extends Repository { + public constructor( + @inject(InjectionSymbols.StorageService) storageService: StorageService, + eventEmitter: EventEmitter + ) { + super(MdocRecord, storageService, eventEmitter) + } +} diff --git a/packages/core/src/modules/mdoc/repository/index.ts b/packages/core/src/modules/mdoc/repository/index.ts new file mode 100644 index 0000000000..f211d1f7ae --- /dev/null +++ b/packages/core/src/modules/mdoc/repository/index.ts @@ -0,0 +1,2 @@ +export * from './MdocRecord' +export * from './MdocRepository' diff --git a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts index 7373d14b00..39dae088be 100644 --- a/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts +++ b/packages/core/src/modules/proofs/formats/dif-presentation-exchange/DifPresentationExchangeProofFormatService.ts @@ -39,6 +39,7 @@ import { DifPresentationExchangeService, DifPresentationExchangeSubmissionLocation, } from '../../../dif-presentation-exchange' +import { MdocVerifiablePresentation } from '../../../mdoc' import { ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE, AnonCredsDataIntegrityServiceSymbol, @@ -208,6 +209,10 @@ export class DifPresentationExchangeProofFormatService domain: options?.domain, }) + if (!presentation) { + throw new CredoError('Failed to create presentation for request.') + } + if (presentation.verifiablePresentations.length > 1) { throw new CredoError('Invalid amount of verifiable presentations. Only one is allowed.') } @@ -223,6 +228,8 @@ export class DifPresentationExchangeProofFormatService firstPresentation instanceof W3cJwtVerifiablePresentation || firstPresentation instanceof W3cJsonLdVerifiablePresentation ? firstPresentation.encoded + : firstPresentation instanceof MdocVerifiablePresentation + ? firstPresentation.deviceSignedBase64Url : firstPresentation?.compact const attachment = this.getFormatData(encodedFirstPresentation, format.attachmentId) diff --git a/packages/core/src/modules/vc/models/ClaimFormat.ts b/packages/core/src/modules/vc/models/ClaimFormat.ts index 47e1b48c52..dfe094c69d 100644 --- a/packages/core/src/modules/vc/models/ClaimFormat.ts +++ b/packages/core/src/modules/vc/models/ClaimFormat.ts @@ -13,4 +13,5 @@ export enum ClaimFormat { DiVc = 'di_vc', DiVp = 'di_vp', SdJwtVc = 'vc+sd-jwt', + MsoMdoc = 'mso_mdoc', } diff --git a/packages/core/src/modules/x509/X509Certificate.ts b/packages/core/src/modules/x509/X509Certificate.ts index 22b5fe8634..0e32dd02d0 100644 --- a/packages/core/src/modules/x509/X509Certificate.ts +++ b/packages/core/src/modules/x509/X509Certificate.ts @@ -1,3 +1,4 @@ +import type { X509CreateSelfSignedCertificateOptions } from './X509ServiceOptions' import type { CredoWebCrypto } from '../../crypto/webcrypto' import { AsnParser } from '@peculiar/asn1-schema' @@ -7,6 +8,7 @@ import * as x509 from '@peculiar/x509' import { Key } from '../../crypto/Key' import { CredoWebCryptoKey } from '../../crypto/webcrypto' import { credoKeyTypeIntoCryptoKeyAlgorithm, spkiAlgorithmIntoCredoKeyType } from '../../crypto/webcrypto/utils' +import { TypedArrayEncoder } from '../../utils' import { X509Error } from './X509Error' @@ -83,20 +85,7 @@ export class X509Certificate { } public static async createSelfSigned( - { - key, - extensions, - notAfter, - notBefore, - name, - }: { - key: Key - // For now we only support the SubjectAlternativeName as `dns` or `uri` - extensions?: ExtensionInput - notBefore?: Date - notAfter?: Date - name?: string - }, + { key, extensions, notAfter, notBefore, name, countryName }: X509CreateSelfSignedCertificateOptions, webCrypto: CredoWebCrypto ) { const cryptoKeyAlgorithm = credoKeyTypeIntoCryptoKeyAlgorithm(key.keyType) @@ -104,10 +93,20 @@ export class X509Certificate { const publicKey = new CredoWebCryptoKey(key, cryptoKeyAlgorithm, true, 'public', ['verify']) const privateKey = new CredoWebCryptoKey(key, cryptoKeyAlgorithm, false, 'private', ['sign']) + const issuerName = + name || countryName + ? [ + { + ...(name && { CN: [name] }), + ...(countryName && { C: [countryName] }), + }, + ] + : undefined + const certificate = await x509.X509CertificateGenerator.createSelfSigned( { keys: { publicKey, privateKey }, - name, + name: issuerName, extensions: extensions?.map((extension) => new x509.SubjectAlternativeNameExtension(extension)), notAfter, notBefore, @@ -154,6 +153,27 @@ export class X509Certificate { } } + public async getData(crypto?: CredoWebCrypto) { + const certificate = new x509.X509Certificate(this.rawCertificate) + + const thumbprint = await certificate.getThumbprint(crypto) + const thumbprintHex = TypedArrayEncoder.toHex(new Uint8Array(thumbprint)) + return { + issuerName: certificate.issuerName.toString(), + subjectName: certificate.subjectName.toString(), + serialNumber: certificate.serialNumber, + thumbprint: thumbprintHex, + pem: certificate.toString(), + notBefore: certificate.notBefore, + notAfter: certificate.notAfter, + } + } + + public getIssuerNameField(field: string) { + const certificate = new x509.X509Certificate(this.rawCertificate) + return certificate.issuerName.getField(field) + } + public toString(format: 'asn' | 'pem' | 'hex' | 'base64' | 'text' | 'base64url') { const certificate = new x509.X509Certificate(this.rawCertificate) return certificate.toString(format) diff --git a/packages/core/src/modules/x509/X509ServiceOptions.ts b/packages/core/src/modules/x509/X509ServiceOptions.ts index dbbfafe0c0..3a9d0dd19c 100644 --- a/packages/core/src/modules/x509/X509ServiceOptions.ts +++ b/packages/core/src/modules/x509/X509ServiceOptions.ts @@ -22,6 +22,7 @@ export interface X509CreateSelfSignedCertificateOptions { notBefore?: Date notAfter?: Date name?: string + countryName?: string } export interface X509GetLefCertificateOptions { diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index 6b8fff7b28..5c7ccb7486 100644 --- a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts +++ b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts @@ -25,6 +25,7 @@ import { getJwkFromJson, injectable, parseDid, + MdocVerifiablePresentation, } from '@credo-ts/core' import { OP, ResponseIss, ResponseMode, ResponseType, SupportedVersion, VPTokenLocation } from '@sphereon/did-auth-siop' @@ -109,6 +110,14 @@ export class OpenId4VcSiopHolderService { challenge: nonce, domain: clientId, presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL, + openid4vp: { + clientId, + verifierGeneratedNonce: nonce, + mdocGeneratedNonce: await agentContext.wallet.generateNonce(), + responseUri: + authorizationRequest.authorizationRequestPayload.response_uri ?? + authorizationRequest.authorizationRequestPayload.request_uri, + }, }) presentationExchangeOptions = { @@ -272,6 +281,8 @@ export class OpenId4VcSiopHolderService { "JWT W3C Verifiable presentation does not include did in JWT header 'kid'. Unable to extract openIdTokenIssuer from verifiable presentation" ) } + } else if (verifiablePresentation instanceof MdocVerifiablePresentation) { + throw new CredoError('Mdoc Verifiable Presentations are not yet supported') } else { const cnf = verifiablePresentation.payload.cnf // FIXME: SD-JWT VC should have better payload typing, so this doesn't become so ugly diff --git a/packages/openid4vc/src/shared/transform.ts b/packages/openid4vc/src/shared/transform.ts index bf2cebf80e..d73cfa638c 100644 --- a/packages/openid4vc/src/shared/transform.ts +++ b/packages/openid4vc/src/shared/transform.ts @@ -14,6 +14,8 @@ import { W3cJwtVerifiableCredential, W3cJsonLdVerifiableCredential, JsonEncoder, + Mdoc, + MdocVerifiablePresentation, } from '@credo-ts/core' export function getSphereonVerifiableCredential( @@ -26,6 +28,8 @@ export function getSphereonVerifiableCredential( return JsonTransformer.toJSON(verifiableCredential) as SphereonW3cVerifiableCredential } else if (verifiableCredential instanceof W3cJwtVerifiableCredential) { return verifiableCredential.serializedJwt + } else if (verifiableCredential instanceof Mdoc) { + throw new CredoError('Mdoc verifiable credential is not yet supported.') } else { return verifiableCredential.compact } @@ -41,6 +45,8 @@ export function getSphereonVerifiablePresentation( return JsonTransformer.toJSON(verifiablePresentation) as SphereonW3cVerifiablePresentation } else if (verifiablePresentation instanceof W3cJwtVerifiablePresentation) { return verifiablePresentation.serializedJwt + } else if (verifiablePresentation instanceof MdocVerifiablePresentation) { + throw new CredoError('Mdoc verifiable presentation is not yet supported.') } else { return verifiablePresentation.compact } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f083c623db..8b3c4a2fda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -414,9 +414,12 @@ importers: '@multiformats/base-x': specifier: ^4.0.1 version: 4.0.1 + '@noble/curves': + specifier: ^1.6.0 + version: 1.6.0 '@noble/hashes': - specifier: ^1.4.0 - version: 1.4.0 + specifier: ^1.5.0 + version: 1.5.0 '@peculiar/asn1-ecc': specifier: ^2.3.8 version: 2.3.13 @@ -429,6 +432,15 @@ importers: '@peculiar/x509': specifier: ^1.11.0 version: 1.12.1 + '@protokoll/core': + specifier: 0.2.27 + version: 0.2.27(typescript@5.5.4) + '@protokoll/crypto': + specifier: 0.2.27 + version: 0.2.27(typescript@5.5.4) + '@protokoll/mdoc-client': + specifier: 0.2.27 + version: 0.2.27(typescript@5.5.4) '@sd-jwt/core': specifier: ^0.7.0 version: 0.7.2 @@ -490,7 +502,7 @@ importers: specifier: ^0.4.1 version: 0.4.1 luxon: - specifier: ^3.3.0 + specifier: ^3.5.0 version: 3.5.0 make-error: specifier: ^1.3.6 @@ -1677,6 +1689,36 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@cbor-extract/cbor-extract-darwin-arm64@2.2.0': + resolution: {integrity: sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.0': + resolution: {integrity: sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.0': + resolution: {integrity: sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.0': + resolution: {integrity: sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.0': + resolution: {integrity: sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.0': + resolution: {integrity: sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==} + cpu: [x64] + os: [win32] + '@changesets/apply-release-plan@7.0.4': resolution: {integrity: sha512-HLFwhKWayKinWAul0Vj+76jVx1Pc2v55MGPVjZ924Y/ROeSsBMFutv9heHmCUj48lJyRfOTJG5+ar+29FUky/A==} @@ -2128,6 +2170,9 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + '@jfromaniello/typedmap@1.4.0': + resolution: {integrity: sha512-uHGCjkzZvdi1Kg90jmmm5H5lckH5seL5Z+dxdDjEu98ixmKfVSbFX9DvE/m5Stnw1uIGgmVD/OCSiNKZ+YT5mA==} + '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} @@ -2195,10 +2240,6 @@ packages: resolution: {integrity: sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==} engines: {node: ^14.21.3 || >=16} - '@noble/hashes@1.4.0': - resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} - engines: {node: '>= 16'} - '@noble/hashes@1.5.0': resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==} engines: {node: ^14.21.3 || >=16} @@ -2294,6 +2335,15 @@ packages: '@protobufjs/utf8@1.1.0': resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@protokoll/core@0.2.27': + resolution: {integrity: sha512-9+SOTmrehKxfb3UJBleplC8tZ99TBZDCqgB54L9JAsApqJqSRVhU/kTm4XWknn7pG5Sq4oUtQifo4Eo2nrmOhw==} + + '@protokoll/crypto@0.2.27': + resolution: {integrity: sha512-dIp5t7T5muW+G0leFNC80ra8VCXOnQNebQ9bIGuwskrzdEdY4KS2ZrljaQIu61fKx/YBFLhHkM3TO0hgrATlSg==} + + '@protokoll/mdoc-client@0.2.27': + resolution: {integrity: sha512-1z7ZLVgsInGsFW8b+VhhLGZR0rS8emZxXfRTkcZTyePSSpQilT4IoINBTfUeNMj47ANqkMBsgHmZs9UEKNSYSQ==} + '@react-native-community/cli-clean@10.1.1': resolution: {integrity: sha512-iNsrjzjIRv9yb5y309SWJ8NDHdwYtnCpmxZouQDyOljUdC9MwdZ4ChbtA4rwQyAwgOVfS9F/j56ML3Cslmvrxg==} @@ -3336,6 +3386,13 @@ packages: canonicalize@2.0.0: resolution: {integrity: sha512-ulDEYPv7asdKvqahuAY35c1selLdzDwHqugK92hfkzvlDCwXRRelDkR+Er33md/PtnpqHemgkuDPanZ4fiYZ8w==} + cbor-extract@2.2.0: + resolution: {integrity: sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==} + hasBin: true + + cbor-x@1.6.0: + resolution: {integrity: sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -3499,6 +3556,9 @@ packages: compare-versions@3.6.0: resolution: {integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + component-type@1.2.2: resolution: {integrity: sha512-99VUHREHiN5cLeHm3YLq312p6v+HUEcwtLCAtelvUDI6+SH5g5Cr85oNR2S1o6ywzL0ykMbuwLzM2ANocjEOIA==} @@ -4006,6 +4066,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true esniff@2.0.1: @@ -5730,6 +5791,10 @@ packages: resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==} engines: {node: '>= 6.13.0'} + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + node-gyp-build@4.8.1: resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==} hasBin: true @@ -7184,6 +7249,14 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + valibot@0.37.0: + resolution: {integrity: sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + valibot@0.42.1: resolution: {integrity: sha512-3keXV29Ar5b//Hqi4MbSdV7lfVp6zuYLZuA9V1PvQUsXqogr+u5lvLPLk3A4f74VUXDnf/JfWMN6sB+koJ/FFw==} peerDependencies: @@ -8489,6 +8562,24 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@cbor-extract/cbor-extract-darwin-arm64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.0': + optional: true + '@changesets/apply-release-plan@7.0.4': dependencies: '@babel/runtime': 7.25.0 @@ -8678,7 +8769,7 @@ snapshots: '@confio/ics23@0.6.8': dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 protobufjs: 6.11.4 '@cosmjs/amino@0.30.1': @@ -8693,7 +8784,7 @@ snapshots: '@cosmjs/encoding': 0.30.1 '@cosmjs/math': 0.30.1 '@cosmjs/utils': 0.30.1 - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 bn.js: 5.2.1 elliptic: 6.5.7 libsodium-wrappers: 0.7.15 @@ -9590,6 +9681,8 @@ snapshots: '@types/yargs': 17.0.33 chalk: 4.1.2 + '@jfromaniello/typedmap@1.4.0': {} + '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 @@ -9691,8 +9784,6 @@ snapshots: dependencies: '@noble/hashes': 1.5.0 - '@noble/hashes@1.4.0': {} - '@noble/hashes@1.5.0': {} '@nodelib/fs.scandir@2.1.5': @@ -9840,6 +9931,30 @@ snapshots: '@protobufjs/utf8@1.1.0': {} + '@protokoll/core@0.2.27(typescript@5.5.4)': + dependencies: + '@credo-ts/core': link:packages/core + jwt-decode: 4.0.0 + valibot: 0.37.0(typescript@5.5.4) + transitivePeerDependencies: + - typescript + + '@protokoll/crypto@0.2.27(typescript@5.5.4)': + dependencies: + '@protokoll/core': 0.2.27(typescript@5.5.4) + valibot: 0.37.0(typescript@5.5.4) + transitivePeerDependencies: + - typescript + + '@protokoll/mdoc-client@0.2.27(typescript@5.5.4)': + dependencies: + '@jfromaniello/typedmap': 1.4.0 + '@protokoll/core': 0.2.27(typescript@5.5.4) + cbor-x: 1.6.0 + compare-versions: 6.1.1 + transitivePeerDependencies: + - typescript + '@react-native-community/cli-clean@10.1.1': dependencies: '@react-native-community/cli-tools': 10.1.1 @@ -11622,6 +11737,22 @@ snapshots: canonicalize@2.0.0: {} + cbor-extract@2.2.0: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.0 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.0 + '@cbor-extract/cbor-extract-linux-x64': 2.2.0 + '@cbor-extract/cbor-extract-win32-x64': 2.2.0 + optional: true + + cbor-x@1.6.0: + optionalDependencies: + cbor-extract: 2.2.0 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -11779,6 +11910,8 @@ snapshots: compare-versions@3.6.0: optional: true + compare-versions@6.1.1: {} + component-type@1.2.2: {} compressible@2.0.18: @@ -12072,7 +12205,7 @@ snapshots: dependencies: '@noble/ciphers': 0.5.3 '@noble/curves': 1.6.0 - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.5.0 '@scure/base': 1.1.8 canonicalize: 2.0.0 did-resolver: 4.1.0 @@ -12308,7 +12441,7 @@ snapshots: debug: 4.3.6 enhanced-resolve: 5.17.1 eslint: 8.57.0 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) eslint-plugin-import: 2.29.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) fast-glob: 3.3.2 get-tsconfig: 4.7.6 @@ -12320,7 +12453,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0): + eslint-module-utils@2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: @@ -12341,7 +12474,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.8.1(@typescript-eslint/parser@7.18.0(eslint@8.57.0)(typescript@5.5.4))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.15.0 is-glob: 4.0.3 @@ -14663,6 +14796,11 @@ snapshots: node-forge@1.3.1: {} + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.0.3 + optional: true + node-gyp-build@4.8.1: {} node-int64@0.4.0: {} @@ -16159,6 +16297,10 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + valibot@0.37.0(typescript@5.5.4): + optionalDependencies: + typescript: 5.5.4 + valibot@0.42.1(typescript@5.5.4): optionalDependencies: typescript: 5.5.4