Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: mdoc-support #2054

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/metal-carrots-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@credo-ts/core': patch
---

feat: mdoc-support
3 changes: 3 additions & 0 deletions demo-openid/src/Holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -56,6 +57,8 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
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)
}
Expand Down
11 changes: 8 additions & 3 deletions demo-openid/src/HolderInquirer.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -181,11 +181,16 @@ export class HolderInquirer extends BaseInquirer {
}
}

private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord) => {
private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord | MdocRecord) => {
if (credential.type === 'W3cCredentialRecord') {
console.log(greenText(`W3cCredentialRecord with claim format ${credential.credential.claimFormat}`, true))
console.log(JSON.stringify(credential.credential.jsonCredential, null, 2))
console.log('')
} else if (credential.type === 'MdocRecord') {
console.log(greenText(`MdocRecord`, true))
const namespaces = Mdoc.fromBase64Url(credential.base64Url).namespaces
console.log(JSON.stringify(namespaces, null, 2))
console.log('')
} else {
console.log(greenText(`SdJwtVcRecord`, true))
const prettyClaims = this.holder.agent.sdJwtVc.fromCompact(credential.compactSdJwtVc).prettyClaims
Expand Down
8 changes: 6 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,15 @@
"@digitalcredentials/jsonld-signatures": "^9.4.0",
"@digitalcredentials/vc": "^6.0.1",
"@multiformats/base-x": "^4.0.1",
"@noble/hashes": "^1.4.0",
"@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0",
"@peculiar/asn1-ecc": "^2.3.8",
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8",
"@peculiar/x509": "^1.11.0",
"@protokoll/core": "0.2.26",
"@protokoll/crypto": "0.2.26",
"@protokoll/mdoc-client": "0.2.26",
"@sd-jwt/core": "^0.7.0",
"@sd-jwt/decode": "^0.7.0",
"@sd-jwt/jwt-status-list": "^0.7.0",
Expand All @@ -54,7 +58,7 @@
"did-resolver": "^4.1.0",
"jsonpath": "^1.1.1",
"lru_map": "^0.4.1",
"luxon": "^3.3.0",
"luxon": "^3.5.0",
"make-error": "^1.3.6",
"object-inspect": "^1.10.3",
"query-string": "^7.0.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/agent/AgentModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -137,6 +138,7 @@ function getDefaultAgentModules() {
pex: () => new DifPresentationExchangeModule(),
sdJwtVc: () => new SdJwtVcModule(),
x509: () => new X509Module(),
mdoc: () => new MdocModule(),
} as const
}

Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/agent/BaseAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -53,6 +54,7 @@ export abstract class BaseAgent<AgentModules extends ModulesMap = EmptyModuleMap
public readonly mediationRecipient: MediationRecipientApi
public readonly messagePickup: CustomOrDefaultApi<AgentModules['messagePickup'], MessagePickupModule>
public readonly basicMessages: BasicMessagesApi
public readonly mdoc: MdocApi
public readonly genericRecords: GenericRecordsApi
public readonly discovery: DiscoverFeaturesApi
public readonly dids: DidsApi
Expand Down Expand Up @@ -111,6 +113,7 @@ export abstract class BaseAgent<AgentModules extends ModulesMap = EmptyModuleMap
this.w3cCredentials = this.dependencyManager.resolve(W3cCredentialsApi)
this.sdJwtVc = this.dependencyManager.resolve(SdJwtVcApi)
this.x509 = this.dependencyManager.resolve(X509Api)
this.mdoc = this.dependencyManager.resolve(MdocApi)

const defaultApis = [
this.connections,
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/crypto/webcrypto/CredoWebCrypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -18,4 +20,8 @@ export class CredoWebCrypto extends core.Crypto {
public getRandomValues<T extends ArrayBufferView | null>(array: T): T {
return this.walletWebCrypto.generateRandomValues(array)
}

public digest(algorithm: string, data: ArrayBuffer): ArrayBuffer {
return Hasher.hash(new Uint8Array(data), algorithm)
}
}
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ import type {
W3CVerifiablePresentation,
} from '@sphereon/ssi-types'

import { PEVersion, PEX, Status } from '@sphereon/pex'
import { PEVersion, PEX, PresentationSubmissionLocation, Status } from '@sphereon/pex'
import { injectable } from 'tsyringe'

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,
Expand Down Expand Up @@ -151,9 +153,10 @@ export class DifPresentationExchangeService {
presentationSubmissionLocation?: DifPresentationExchangeSubmissionLocation
challenge: string
domain?: string
openid4vp?: MdocOpenId4VpSessionTranscriptOptions
}
) {
const { presentationDefinition, domain, challenge } = options
const { presentationDefinition, domain, challenge, openid4vp } = options
const presentationSubmissionLocation =
options.presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION

Expand All @@ -172,11 +175,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,
Expand All @@ -185,6 +183,42 @@ export class DifPresentationExchangeService {
submission_requirements: undefined,
}

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.')
}

const { deviceResponseBase64Url, presentationSubmission } = await MdocDeviceResponse.openId4Vp(agentContext, {
mdocs: [Mdoc.fromBase64Url(mdocRecord.base64Url)],
presentationDefinition: presentationDefinition as DifPresentationExchangeDefinitionV2,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if v1 is used?

sessionTranscriptOptions: {
...openid4vp,
},
})

verifiablePresentationResultsWithFormat.push({
verifiablePresentationResult: {
presentationSubmission: presentationSubmission,
verifiablePresentation: deviceResponseBase64Url,
presentationSubmissionLocation: PresentationSubmissionLocation.EXTERNAL,
},
claimFormat: presentationToCreate.claimFormat,
})

continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think instead of continue we could use an else clause maybe? I had a hard time understanding this code, until i spotted this continue statement

}

// Get all the credentials for the presentation
const credentialsForPresentation = presentationToCreate.verifiableCredentials.map((c) =>
getSphereonOriginalVerifiableCredential(c.credential)
)

const verifiablePresentationResult = await this.pex.verifiablePresentationFrom(
presentationDefinitionForSubject,
credentialsForPresentation,
Expand Down Expand Up @@ -564,10 +598,12 @@ export class DifPresentationExchangeService {
private async queryCredentialForPresentationDefinition(
agentContext: AgentContext,
presentationDefinition: DifPresentationExchangeDefinition
): Promise<Array<SdJwtVcRecord | W3cCredentialRecord>> {
): Promise<Array<SdJwtVcRecord | W3cCredentialRecord | MdocRecord>> {
const w3cCredentialRepository = agentContext.dependencyManager.resolve(W3cCredentialRepository)
const w3cQuery: Array<Query<W3cCredentialRecord>> = []
const sdJwtVcQuery: Array<Query<SdJwtVcRecord>> = []
const mdocQuery: Array<Query<MdocRecord>> = []

const presentationDefinitionVersion = PEX.definitionVersionDiscovery(presentationDefinition)

if (!presentationDefinitionVersion.version) {
Expand All @@ -591,6 +627,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) {
Expand All @@ -603,33 +642,33 @@ export class DifPresentationExchangeService {
)
}

const allRecords: Array<SdJwtVcRecord | W3cCredentialRecord> = []
const allRecords: Array<SdJwtVcRecord | W3cCredentialRecord | MdocRecord> = []

// 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)
}
}
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand Down Expand Up @@ -129,8 +131,13 @@ export type SubmissionEntryCredential =
type: ClaimFormat.JwtVc | ClaimFormat.LdpVc
credentialRecord: W3cCredentialRecord
}
| {
type: ClaimFormat.MsoMdoc
credentialRecord: MdocRecord
disclosedPayload: Record<string, IssuerSignedItem[]>
}

/**
* Mapping of selected credentials for an input descriptor
*/
export type DifPexInputDescriptorToCredentials = Record<string, Array<W3cCredentialRecord | SdJwtVcRecord>>
export type DifPexInputDescriptorToCredentials = Record<string, Array<W3cCredentialRecord | SdJwtVcRecord | MdocRecord>>
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Loading
Loading