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

Merged
merged 18 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from all 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).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
Expand Down
6 changes: 4 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
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
4 changes: 4 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 All @@ -128,6 +131,7 @@ export abstract class BaseAgent<AgentModules extends ModulesMap = EmptyModuleMap
this.w3cCredentials,
this.sdJwtVc,
this.x509,
this.mdoc,
]

// Set the api of the registered modules on the agent, excluding the default apis
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/agent/__tests__/AgentModules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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'
import { MessagePickupModule } from '../../modules/message-pickup'
import { OutOfBandModule } from '../../modules/oob'
import { ProofsModule } from '../../modules/proofs'
Expand Down Expand Up @@ -73,6 +74,7 @@ describe('AgentModules', () => {
oob: expect.any(OutOfBandModule),
w3cCredentials: expect.any(W3cCredentialsModule),
sdJwtVc: expect.any(SdJwtVcModule),
mdoc: expect.any(MdocModule),
x509: expect.any(X509Module),
cache: expect.any(CacheModule),
})
Expand Down Expand Up @@ -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,
})
Expand Down Expand Up @@ -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,
})
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,14 +22,16 @@ 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'

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 @@ -152,9 +154,10 @@ export class DifPresentationExchangeService {
presentationSubmissionLocation?: DifPresentationExchangeSubmissionLocation
challenge: string
domain?: string
openid4vp?: Omit<MdocOpenId4VpSessionTranscriptOptions, 'verifierGeneratedNonce' | 'clientId'>
}
) {
const { presentationDefinition, domain, challenge } = options
const { presentationDefinition, domain, challenge, openid4vp } = options
const presentationSubmissionLocation =
options.presentationSubmissionLocation ?? DifPresentationExchangeSubmissionLocation.PRESENTATION

Expand All @@ -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,
Expand All @@ -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) {
Expand Down Expand Up @@ -568,10 +607,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 @@ -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) {
Expand All @@ -607,33 +651,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>>
Loading
Loading