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..bd745a7bbf 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).issuerSignedNamespaces + 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..e25ead39b2 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -29,11 +29,13 @@ "@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/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 +56,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..b404e76798 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 { oob: expect.any(OutOfBandModule), w3cCredentials: expect.any(W3cCredentialsModule), sdJwtVc: expect.any(SdJwtVcModule), + mdoc: expect.any(MdocModule), x509: expect.any(X509Module), cache: expect.any(CacheModule), }) @@ -101,6 +103,7 @@ describe('AgentModules', () => { w3cCredentials: expect.any(W3cCredentialsModule), cache: expect.any(CacheModule), sdJwtVc: expect.any(SdJwtVcModule), + mdoc: expect.any(MdocModule), x509: expect.any(X509Module), myModule, }) @@ -131,6 +134,7 @@ describe('AgentModules', () => { w3cCredentials: expect.any(W3cCredentialsModule), cache: expect.any(CacheModule), sdJwtVc: expect.any(SdJwtVcModule), + mdoc: expect.any(MdocModule), x509: expect.any(X509Module), myModule, }) diff --git a/packages/core/src/crypto/webcrypto/CredoWebCrypto.ts b/packages/core/src/crypto/webcrypto/CredoWebCrypto.ts index 89612ae4d3..f0b424ee26 100644 --- a/packages/core/src/crypto/webcrypto/CredoWebCrypto.ts +++ b/packages/core/src/crypto/webcrypto/CredoWebCrypto.ts @@ -2,6 +2,8 @@ import type { AgentContext } from '../../agent' import * as core from 'webcrypto-core' +import { Hasher } from '../hashes' + import { CredoSubtle } from './CredoSubtle' import { CredoWalletWebCrypto } from './CredoWalletWebCrypto' @@ -18,4 +20,8 @@ export class CredoWebCrypto extends core.Crypto { public getRandomValues(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..b933a62bb8 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?: Omit } ) { - 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,25 +184,66 @@ 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, - }) + if (!domain) { + throw new DifPresentationExchangeError('Missing domain property for creating MDOC presentation.') + } + + const { deviceResponseBase64Url, presentationSubmission } = + await MdocDeviceResponse.createOpenId4VpDeviceResponse(agentContext, { + mdocs: [Mdoc.fromBase64Url(mdocRecord.base64Url)], + presentationDefinition: presentationDefinition, + sessionTranscriptOptions: { + ...openid4vp, + clientId: domain, + verifierGeneratedNonce: challenge, + }, + }) + + 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) + ) + + const verifiablePresentationResult = await this.pex.verifiablePresentationFrom( + presentationDefinitionForSubject, + credentialsForPresentation, + this.getPresentationSignCallback(agentContext, presentationToCreate), + { + proofOptions: { + challenge, + domain, + }, + signatureOptions: {}, + presentationSubmissionLocation: + presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION, + } + ) + + verifiablePresentationResultsWithFormat.push({ + verifiablePresentationResult, + claimFormat: presentationToCreate.claimFormat, + }) + } } if (verifiablePresentationResultsWithFormat.length === 0) { @@ -568,10 +607,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 +636,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 +651,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..7c3a8daf98 --- /dev/null +++ b/packages/core/src/modules/mdoc/Mdoc.ts @@ -0,0 +1,175 @@ +import type { MdocSignOptions, MdocNameSpaces, MdocVerifyOptions } from './MdocOptions' +import type { AgentContext } from '../../agent' +import type { IssuerSignedDocument } from '@protokoll/mdoc-client' + +import { + DeviceSignedDocument, + Document, + Verifier, + cborEncode, + parseDeviceSigned, + 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 fromBase64Url(mdocBase64Url: string, expectedDocType?: string): Mdoc { + const issuerSignedDocument = parseIssuerSigned(TypedArrayEncoder.fromBase64(mdocBase64Url), expectedDocType) + return new Mdoc(issuerSignedDocument) + } + + public static fromIssuerSignedDocument( + issuerSignedBase64Url: string, + deviceSignedBase64Url?: string, + expectedDocType?: string + ): Mdoc { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + + if (deviceSignedBase64Url) { + return new Mdoc( + parseDeviceSigned( + TypedArrayEncoder.fromBase64(deviceSignedBase64Url), + TypedArrayEncoder.fromBase64(issuerSignedBase64Url), + expectedDocType + ) + ) + } else { + return new Mdoc(parseIssuerSigned(TypedArrayEncoder.fromBase64(issuerSignedBase64Url), expectedDocType)) + } + } + + 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, holderKey, issuerCertificate } = options + const mdocContext = getMdocContext(agentContext) + + const holderPublicJwk = await getJwkFromKey(holderKey) + 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(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 }> { + let trustedCerts: [string, ...string[]] | undefined + + if (options?.trustedCertificates) { + trustedCerts = options.trustedCertificates + } else if (options?.verificationContext) { + trustedCerts = await agentContext.dependencyManager + .resolve(X509ModuleConfig) + .getTrustedCertificatesForVerification?.(agentContext, options.verificationContext) + } else { + trustedCerts = 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, + }, + mdocContext + ) + + 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..ea6038982b --- /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 sign(options: MdocSignOptions) { + return await this.mdocService.signMdoc(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..795eeade11 --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocContext.ts @@ -0,0 +1,112 @@ +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 { CredoWebCrypto, getJwkFromJson, getJwkFromKey, 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) + return getJwkFromKey(comp.publicKey).toJson() + }, + 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..9c842d3b3f --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocDeviceResponse.ts @@ -0,0 +1,215 @@ +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, + cborEncode, +} from '@protokoll/mdoc-client' + +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 createOpenId4VpDeviceResponse( + 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) + .usingPresentationDefinition(presentationDefinition) + .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) + + let trustedCerts: [string, ...string[]] | undefined + if (options?.trustedCertificates) { + trustedCerts = options.trustedCertificates + } else if (options?.verificationContext) { + agentContext.dependencyManager.resolve(X509ModuleConfig).getTrustedCertificatesForVerification + trustedCerts = await agentContext.dependencyManager + .resolve(X509ModuleConfig) + .getTrustedCertificatesForVerification?.(agentContext, options.verificationContext) + } else { + trustedCerts = 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) => { + const prepared = doc.prepare() + const docType = prepared.get('docType') as string + const issuerSigned = cborEncode(prepared.get('issuerSigned')) + const deviceSigned = cborEncode(prepared.get('deviceSigned')) + + return Mdoc.fromIssuerSignedDocument( + TypedArrayEncoder.toBase64URL(issuerSigned), + TypedArrayEncoder.toBase64URL(deviceSigned), + docType + ) + }) + } +} 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..e6290c6fd4 --- /dev/null +++ b/packages/core/src/modules/mdoc/MdocOptions.ts @@ -0,0 +1,58 @@ +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 interface MdocVerificationContext { + /** + * The `id` of the `OpenId4VcVerificationSessionRecord` that this verification is bound to. + */ + openId4VcVerificationSessionId?: string +} + +export type MdocVerifyOptions = { + trustedCertificates?: [string, ...string[]] + now?: Date + verificationContext?: MdocVerificationContext +} + +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 = { + trustedCertificates?: [string, ...string[]] + sessionTranscriptOptions: MdocOpenId4VpSessionTranscriptOptions + /** + * The base64Url-encoded device response string. + */ + deviceResponse: string + now?: Date + verificationContext?: MdocVerificationContext +} + +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 + holderKey: 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..b60b7eeed3 --- /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 signMdoc(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 createOpenId4VpDeviceResponse(agentContext: AgentContext, options: MdocDeviceResponseOpenId4VpOptions) { + return MdocDeviceResponse.createOpenId4VpDeviceResponse(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.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__/mdocDeviceResponse.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.test.ts new file mode 100644 index 0000000000..36025a044d --- /dev/null +++ b/packages/core/src/modules/mdoc/__tests__/mdocDeviceResponse.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', + holderKey: 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__/mdocOpenId4VcDeviceResponse.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts new file mode 100644 index 0000000000..6a0572b57d --- /dev/null +++ b/packages/core/src/modules/mdoc/__tests__/mdocOpenId4VcDeviceResponse.test.ts @@ -0,0 +1,283 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { Key as AskarKey, Jwk } from '@hyperledger/aries-askar-nodejs' +import { cborEncode, 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) }) + 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'), + }, + holderKey: getJwkFromJson(DEVICE_JWK_PUBLIC).key, + issuerCertificate: ISSUER_CERTIFICATE, + 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.createOpenId4VpDeviceResponse(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) + + const prepared = parsed.documents[0].prepare() + const docType = prepared.get('docType') as string + const issuerSigned = cborEncode(prepared.get('issuerSigned')) + const deviceSigned = cborEncode(prepared.get('deviceSigned')) + parsedDocument = Mdoc.fromIssuerSignedDocument( + TypedArrayEncoder.toBase64URL(issuerSigned), + TypedArrayEncoder.toBase64URL(deviceSigned), + docType + ) + } + }) + + it('should be verifiable', async () => { + const res = await MdocDeviceResponse.verify(agent.context, { + deviceResponse, + trustedCertificates: [ISSUER_CERTIFICATE], + sessionTranscriptOptions: { + clientId, + responseUri, + verifierGeneratedNonce, + mdocGeneratedNonce, + }, + }) + expect(res).toHaveLength(1) + }) + + describe('should not be verifiable', () => { + const testCases = ['clientId', 'responseUri', 'verifierGeneratedNonce', 'mdocGeneratedNonce'] + + testCases.forEach((name) => { + const values = { + clientId, + responseUri, + verifierGeneratedNonce, + mdocGeneratedNonce, + [name]: 'wrong', + } + it(`with a different ${name}`, async () => { + try { + await MdocDeviceResponse.verify(agent.context, { + 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__/mdocServer.test.ts b/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts new file mode 100644 index 0000000000..9f285b0f10 --- /dev/null +++ b/packages/core/src/modules/mdoc/__tests__/mdocServer.test.ts @@ -0,0 +1,131 @@ +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: [], + name: 'C=DE', + }) + + const issuerCertificate = selfSignedCertificate.toString('pem') + + const mdoc = await Mdoc.sign(agentContext, { + docType: 'org.iso.18013.5.1.mDL', + holderKey: holderKey, + namespaces: { + hello: { + world: 'world', + nicer: 'dicer', + }, + }, + issuerCertificate, + }) + + 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..4caf4d63a1 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' @@ -5,8 +6,11 @@ import { id_ce_subjectAltName, SubjectPublicKeyInfo } from '@peculiar/asn1-x509' import * as x509 from '@peculiar/x509' import { Key } from '../../crypto/Key' +import { KeyType } from '../../crypto/KeyType' +import { compress } from '../../crypto/jose/jwk/ecCompression' import { CredoWebCryptoKey } from '../../crypto/webcrypto' import { credoKeyTypeIntoCryptoKeyAlgorithm, spkiAlgorithmIntoCredoKeyType } from '../../crypto/webcrypto/utils' +import { TypedArrayEncoder } from '../../utils' import { X509Error } from './X509Error' @@ -50,7 +54,18 @@ export class X509Certificate { const keyType = spkiAlgorithmIntoCredoKeyType(publicKey.algorithm) - const key = new Key(new Uint8Array(publicKey.subjectPublicKey), keyType) + // TODO(crypto): Currently this only does point-compression for P256. + // We should either store all keys as uncompressed, or we should compress all supported keys here correctly + let keyBytes = new Uint8Array(publicKey.subjectPublicKey) + if (publicKey.subjectPublicKey.byteLength === 65 && keyType === KeyType.P256) { + if (keyBytes[0] !== 0x04) { + throw new X509Error('Received P256 key with 65 bytes, but key did not start with 0x04. Invalid key') + } + // TODO(crypto): the compress method is bugged because it does not expect the required `0x04` prefix. Here we strip that and receive the expected result + keyBytes = compress(keyBytes.slice(1)) + } + + const key = new Key(keyBytes, keyType) return new X509Certificate({ publicKey: key, @@ -83,20 +98,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 }: X509CreateSelfSignedCertificateOptions, webCrypto: CredoWebCrypto ) { const cryptoKeyAlgorithm = credoKeyTypeIntoCryptoKeyAlgorithm(key.keyType) @@ -104,10 +106,24 @@ export class X509Certificate { const publicKey = new CredoWebCryptoKey(key, cryptoKeyAlgorithm, true, 'public', ['verify']) const privateKey = new CredoWebCryptoKey(key, cryptoKeyAlgorithm, false, 'private', ['sign']) + const issuerName = name?.includes(',') + ? [ + Object.fromEntries( + name.split(', ').map((s) => { + const keyValPairs = s.trim().split('=') + if (keyValPairs.some((pair) => pair.length !== 2)) { + throw new X509Error(`Cannot create self-signed certificate. Name parsing failed. '${name}'`) + } + return keyValPairs.map(([key, val]) => [key, [val]] as [string, string[]]) + }) + ), + ] + : name + const certificate = await x509.X509CertificateGenerator.createSelfSigned( { keys: { publicKey, privateKey }, - name, + name: issuerName, extensions: extensions?.map((extension) => new x509.SubjectAlternativeNameExtension(extension)), notAfter, notBefore, @@ -154,6 +170,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/__tests__/X509Service.test.ts b/packages/core/src/modules/x509/__tests__/X509Service.test.ts index ccb04c886c..141185fb6f 100644 --- a/packages/core/src/modules/x509/__tests__/X509Service.test.ts +++ b/packages/core/src/modules/x509/__tests__/X509Service.test.ts @@ -6,6 +6,7 @@ import * as x509 from '@peculiar/x509' import { InMemoryWallet } from '../../../../../../tests/InMemoryWallet' import { getAgentConfig, getAgentContext } from '../../../../tests' import { KeyType } from '../../../crypto/KeyType' +import { getJwkFromKey, P256Jwk } from '../../../crypto/jose/jwk' import { CredoWebCrypto, CredoWebCryptoKey } from '../../../crypto/webcrypto' import { X509Error } from '../X509Error' import { X509Service } from '../X509Service' @@ -119,6 +120,51 @@ describe('X509Service', () => { await wallet.close() }) + it('should correctly parse an X.509 certificate with an uncompressed key to a JWK', async () => { + const encodedCertificate = + 'MIICKjCCAdCgAwIBAgIUV8bM0wi95D7KN0TyqHE42ru4hOgwCgYIKoZIzj0EAwIwUzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMQ8wDQYDVQQHDAZBbGJhbnkxDzANBgNVBAoMBk5ZIERNVjEPMA0GA1UECwwGTlkgRE1WMB4XDTIzMDkxNDE0NTUxOFoXDTMzMDkxMTE0NTUxOFowUzELMAkGA1UEBhMCVVMxETAPBgNVBAgMCE5ldyBZb3JrMQ8wDQYDVQQHDAZBbGJhbnkxDzANBgNVBAoMBk5ZIERNVjEPMA0GA1UECwwGTlkgRE1WMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiTwtg0eQbcbNabf2Nq9L/VM/lhhPCq2s0Qgw2kRx29tgrBcNHPxTT64tnc1Ij3dH/fl42SXqMenpCDw4K6ntU6OBgTB/MB0GA1UdDgQWBBSrbS4DuR1JIkAzj7zK3v2TM+r2xzAfBgNVHSMEGDAWgBSrbS4DuR1JIkAzj7zK3v2TM+r2xzAPBgNVHRMBAf8EBTADAQH/MCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdlbmVyYXRlZCBDZXJ0aWZpY2F0ZTAKBggqhkjOPQQDAgNIADBFAiAJ/Qyrl7A+ePZOdNfc7ohmjEdqCvxaos6//gfTvncuqQIhANo4q8mKCA9J8k/+zh//yKbN1bLAtdqPx7dnrDqV3Lg+' + + const x509Certificate = X509Service.parseCertificate(agentContext, { encodedCertificate }) + + expect(x509Certificate.publicKey.publicKey.length).toStrictEqual(33) + expect(x509Certificate.publicKey.publicKeyBase58).toStrictEqual('23vfBuUJkWXTC3zZWMh1TR57CTubFjFfhm4CGfgszRMHU') + + const jwk = getJwkFromKey(x509Certificate.publicKey) + + expect(jwk).toBeInstanceOf(P256Jwk) + expect(jwk).toMatchObject({ + x: 'iTwtg0eQbcbNabf2Nq9L_VM_lhhPCq2s0Qgw2kRx29s', + y: 'YKwXDRz8U0-uLZ3NSI93R_35eNkl6jHp6Qg8OCup7VM', + }) + }) + + it('should parse a valid X.509 certificate', async () => { + const key = await agentContext.wallet.createKey({ keyType: KeyType.P256 }) + const certificate = await X509Service.createSelfSignedCertificate(agentContext, { + key, + extensions: [ + [ + { type: 'url', value: 'animo.id' }, + { type: 'dns', value: 'paradym.id' }, + ], + [ + { type: 'dns', value: 'wallet.paradym.id' }, + { type: 'dns', value: 'animo.id' }, + ], + ], + }) + const encodedCertificate = certificate.toString('base64') + + const x509Certificate = X509Service.parseCertificate(agentContext, { encodedCertificate }) + + expect(x509Certificate).toMatchObject({ + sanDnsNames: expect.arrayContaining(['paradym.id', 'wallet.paradym.id', 'animo.id']), + sanUriNames: expect.arrayContaining(['animo.id']), + }) + + expect(x509Certificate.publicKey.publicKey.length).toStrictEqual(33) + }) + it('should correctly parse x5c chain provided as a test-vector', async () => { const x5c = [ 'MIICaTCCAg+gAwIBAgIUShyxcIZGiPV3wBRp4YOlNp1I13YwCgYIKoZIzj0EAwIwgYkxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIDAZiZHIuZGUxDzANBgNVBAcMBkJlcmxpbjEMMAoGA1UECgwDQkRSMQ8wDQYDVQQLDAZNYXVyZXIxHTAbBgNVBAMMFGlzc3VhbmNlLXRlc3QuYmRyLmRlMRowGAYJKoZIhvcNAQkBFgt0ZXN0QGJkci5kZTAeFw0yNDA1MjgwODIyMjdaFw0zNDA0MDYwODIyMjdaMIGJMQswCQYDVQQGEwJERTEPMA0GA1UECAwGYmRyLmRlMQ8wDQYDVQQHDAZCZXJsaW4xDDAKBgNVBAoMA0JEUjEPMA0GA1UECwwGTWF1cmVyMR0wGwYDVQQDDBRpc3N1YW5jZS10ZXN0LmJkci5kZTEaMBgGCSqGSIb3DQEJARYLdGVzdEBiZHIuZGUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASygZ1Ma0m9uif4n8g3CiCP+E1r2KWFxVmS6LRWqUBMgn5fODKIBftdzVSbv/38gujy5qxh/q5bLcT+yLilazCao1MwUTAdBgNVHQ4EFgQUMGdPNMIdo3iHfqt2jlTnBNCfRNAwHwYDVR0jBBgwFoAUMGdPNMIdo3iHfqt2jlTnBNCfRNAwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAgNIADBFAiAu2h5xulXReb5IhgpkYiYR1BONTtsjT7nfzQAhL4ISOQIhAK6jKwwf6fTTSZwvJUOAu7dz1Dy/DmH19Lef0zqaNNht', diff --git a/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts b/packages/openid4vc/src/openid4vc-holder/OpenId4vcSiopHolderService.ts index 6b8fff7b28..ce98e14ddc 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' @@ -83,6 +84,7 @@ export class OpenId4VcSiopHolderService { let presentationExchangeOptions: PresentationExchangeResponseOpts | undefined = undefined const wantsIdToken = await authorizationRequest.authorizationRequest.containsResponseType(ResponseType.ID_TOKEN) + const authorizationResponseNonce = await agentContext.wallet.generateNonce() // Handle presentation exchange part if (authorizationRequest.presentationDefinitions && authorizationRequest.presentationDefinitions.length > 0) { @@ -109,6 +111,12 @@ export class OpenId4VcSiopHolderService { challenge: nonce, domain: clientId, presentationSubmissionLocation: DifPresentationExchangeSubmissionLocation.EXTERNAL, + openid4vp: { + mdocGeneratedNonce: authorizationResponseNonce, + responseUri: + authorizationRequest.authorizationRequestPayload.response_uri ?? + authorizationRequest.authorizationRequestPayload.request_uri, + }, }) presentationExchangeOptions = { @@ -152,49 +160,52 @@ export class OpenId4VcSiopHolderService { } ) - const createJarmResponse = async (opts: { - authorizationResponsePayload: AuthorizationResponsePayload - requestObjectPayload: RequestObjectPayload - }) => { - const { authorizationResponsePayload, requestObjectPayload } = opts + const getCreateJarmResponseCallback = (authorizationResponseNonce: string) => { + return async (opts: { + authorizationResponsePayload: AuthorizationResponsePayload + requestObjectPayload: RequestObjectPayload + }) => { + const { authorizationResponsePayload, requestObjectPayload } = opts - const jwk = await OP.extractEncJwksFromClientMetadata(requestObjectPayload.client_metadata) - if (!jwk.kty) { - throw new CredoError('Missing kty in jwk.') - } + const jwk = await OP.extractEncJwksFromClientMetadata(requestObjectPayload.client_metadata) + if (!jwk.kty) { + throw new CredoError('Missing kty in jwk.') + } - const validatedMetadata = OP.validateJarmMetadata({ - client_metadata: requestObjectPayload.client_metadata, - server_metadata: { - authorization_encryption_alg_values_supported: ['ECDH-ES'], - authorization_encryption_enc_values_supported: ['A256GCM'], - }, - }) + const validatedMetadata = OP.validateJarmMetadata({ + client_metadata: requestObjectPayload.client_metadata, + server_metadata: { + authorization_encryption_alg_values_supported: ['ECDH-ES'], + authorization_encryption_enc_values_supported: ['A256GCM'], + }, + }) - if (validatedMetadata.type !== 'encrypted') { - throw new CredoError('Only encrypted JARM responses are supported.') - } + if (validatedMetadata.type !== 'encrypted') { + throw new CredoError('Only encrypted JARM responses are supported.') + } - // Extract nonce from the request, we use this as the `apv` - const nonce = authorizationRequest.payload?.nonce - if (!nonce || typeof nonce !== 'string') { - throw new CredoError('Missing nonce in authorization request payload') - } + // Extract nonce from the request, we use this as the `apv` + const nonce = authorizationRequest.payload?.nonce + if (!nonce || typeof nonce !== 'string') { + throw new CredoError('Missing nonce in authorization request payload') + } - const jwe = await this.encryptJarmResponse(agentContext, { - jwkJson: jwk as JwkJson, - payload: authorizationResponsePayload, - authorizationRequestNonce: nonce, - alg: validatedMetadata.client_metadata.authorization_encrypted_response_alg, - enc: validatedMetadata.client_metadata.authorization_encrypted_response_enc, - }) + const jwe = await this.encryptJarmResponse(agentContext, { + jwkJson: jwk as JwkJson, + payload: authorizationResponsePayload, + authorizationRequestNonce: nonce, + alg: validatedMetadata.client_metadata.authorization_encrypted_response_alg, + enc: validatedMetadata.client_metadata.authorization_encrypted_response_enc, + authorizationResponseNonce, + }) - return { response: jwe } + return { response: jwe } + } } const response = await openidProvider.submitAuthorizationResponse( authorizationResponseWithCorrelationId, - createJarmResponse + getCreateJarmResponseCallback(authorizationResponseNonce) ) let responseDetails: string | Record | undefined = undefined try { @@ -272,6 +283,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 @@ -338,6 +351,7 @@ export class OpenId4VcSiopHolderService { alg: string enc: string authorizationRequestNonce: string + authorizationResponseNonce: string } ) { const { payload, jwkJson } = options 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 a6d920b425..9eead0b120 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,9 @@ importers: '@peculiar/x509': specifier: ^1.11.0 version: 1.12.1 + '@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 +496,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 @@ -1680,6 +1686,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==} @@ -2131,6 +2167,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'} @@ -2198,10 +2237,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} @@ -2297,6 +2332,12 @@ 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/mdoc-client@0.2.27': + resolution: {integrity: sha512-1z7ZLVgsInGsFW8b+VhhLGZR0rS8emZxXfRTkcZTyePSSpQilT4IoINBTfUeNMj47ANqkMBsgHmZs9UEKNSYSQ==} + '@react-native-community/cli-clean@10.1.1': resolution: {integrity: sha512-iNsrjzjIRv9yb5y309SWJ8NDHdwYtnCpmxZouQDyOljUdC9MwdZ4ChbtA4rwQyAwgOVfS9F/j56ML3Cslmvrxg==} @@ -3339,6 +3380,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'} @@ -3502,6 +3550,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==} @@ -4009,6 +4060,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: @@ -4121,6 +4173,7 @@ packages: expo-random@14.0.1: resolution: {integrity: sha512-gX2mtR9o+WelX21YizXUCD/y+a4ZL+RDthDmFkHxaYbdzjSYTn8u/igoje/l3WEO+/RYspmqUFa8w/ckNbt6Vg==} + deprecated: This package is now deprecated in favor of expo-crypto, which provides the same functionality. To migrate, replace all imports from expo-random with imports from expo-crypto. peerDependencies: expo: '*' @@ -5733,6 +5786,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 @@ -7187,6 +7244,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: @@ -8492,6 +8557,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 @@ -8681,7 +8764,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': @@ -8696,7 +8779,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 @@ -9593,6 +9676,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 @@ -9694,8 +9779,6 @@ snapshots: dependencies: '@noble/hashes': 1.5.0 - '@noble/hashes@1.4.0': {} - '@noble/hashes@1.5.0': {} '@nodelib/fs.scandir@2.1.5': @@ -9843,6 +9926,23 @@ 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/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 @@ -11625,6 +11725,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 @@ -11782,6 +11898,8 @@ snapshots: compare-versions@3.6.0: optional: true + compare-versions@6.1.1: {} + component-type@1.2.2: {} compressible@2.0.18: @@ -12075,7 +12193,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 @@ -12311,7 +12429,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 @@ -12323,7 +12441,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: @@ -12344,7 +12462,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 @@ -14666,6 +14784,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: {} @@ -16162,6 +16285,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