Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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/funny-carrots-draw.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@credo-ts/core": patch
---

fix: allow purpose to be an empty string in presentation exchange definition
6 changes: 6 additions & 0 deletions .changeset/new-drinks-cheat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@credo-ts/openid4vc": patch
"@credo-ts/core": patch
---

feat: support mdoc device response containing multiple documents for OpenID4VP presentation
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
},
"dependencies": {
"@animo-id/mdoc": "^0.5.2",
"@animo-id/pex": "5.2.0",
"@animo-id/pex": "^6.0.0",
"@astronautlabs/jsonpath": "^1.1.2",
"@digitalcredentials/jsonld": "^6.0.0",
"@digitalcredentials/jsonld-signatures": "^9.4.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@animo-id/pex'
import type { Checked, PresentationSignCallBackParams, Validated, VerifiablePresentationResult } from '@animo-id/pex'
import type { InputDescriptorV2 } from '@sphereon/pex-models'
import type {
SdJwtDecodedVerifiableCredential,
Expand Down Expand Up @@ -109,6 +109,7 @@ export class DifPresentationExchangeService {
public validatePresentationDefinition(presentationDefinition: DifPresentationExchangeDefinition) {
const validation = PEX.validateDefinition(presentationDefinition)
const errorMessages = this.formatValidated(validation)

if (errorMessages.length > 0) {
throw new DifPresentationExchangeError('Invalid presentation definition', { additionalMessages: errorMessages })
}
Expand All @@ -127,7 +128,7 @@ export class DifPresentationExchangeService {
presentations: VerifiablePresentation | VerifiablePresentation[],
presentationSubmission?: DifPresentationExchangeSubmission
) {
const { errors } = this.pex.evaluatePresentation(
const result = this.pex.evaluatePresentation(
presentationDefinition,
Array.isArray(presentations)
? presentations.map(getSphereonOriginalVerifiablePresentation)
Expand All @@ -138,18 +139,16 @@ export class DifPresentationExchangeService {
}
)

if (errors) {
const errorMessages = this.formatValidated(errors as Validated)
if (errorMessages.length > 0) {
throw new DifPresentationExchangeError('Invalid presentation', { additionalMessages: errorMessages })
}
if (result.areRequiredCredentialsPresent === Status.ERROR) {
const errorMessages = this.formatValidated(result.errors)
throw new DifPresentationExchangeError('Invalid presentation', { additionalMessages: errorMessages })
}
}

private formatValidated(v: Validated) {
const validated = Array.isArray(v) ? v : [v]
private formatValidated(v?: Checked[] | Validated) {
const validated = Array.isArray(v) ? v : v ? [v] : []
return validated
.filter((r) => r.tag === Status.ERROR)
.filter((r) => r.status === Status.ERROR)
.map((r) => r.message)
.filter((r): r is string => Boolean(r))
}
Expand Down

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion packages/core/src/modules/mdoc/Mdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { isMdocSupportedSignatureAlgorithm, mdocSupporteSignatureAlgorithms } fr
export class Mdoc {
public base64Url: string

private constructor(private issuerSignedDocument: IssuerSignedDocument | DeviceSignedDocument) {
private constructor(public issuerSignedDocument: IssuerSignedDocument | DeviceSignedDocument) {
const issuerSigned = issuerSignedDocument.prepare().get('issuerSigned')
this.base64Url = TypedArrayEncoder.toBase64URL(cborEncode(issuerSigned))
}
Expand Down
38 changes: 31 additions & 7 deletions packages/core/src/modules/mdoc/MdocDeviceResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,29 @@ export class MdocDeviceResponse {
return this.base64Url
}

public static fromBase64Url(base64Url: string) {
const parsed = parseDeviceResponse(TypedArrayEncoder.fromBase64(base64Url))
if (parsed.status !== MDocStatus.OK) {
throw new MdocError('Parsing Mdoc Device Response failed.')
/**
* To support a single DeviceResponse with multiple documents in OpenID4VP
*/
public splitIntoSingleDocumentResponses(): MdocDeviceResponse[] {
const deviceResponses: MdocDeviceResponse[] = []

if (this.documents.length === 0) {
throw new MdocError('mdoc device response does not contain any mdocs')
}

for (const document of this.documents) {
const deviceResponse = new MDoc()

deviceResponse.addDocument(document.issuerSignedDocument)

deviceResponses.push(MdocDeviceResponse.fromDeviceResponse(deviceResponse))
}

const documents = parsed.documents.map((doc) => {
return deviceResponses
}

private static fromDeviceResponse(mdoc: MDoc) {
const documents = mdoc.documents.map((doc) => {
const prepared = doc.prepare()
const docType = prepared.get('docType') as string
const issuerSigned = cborEncode(prepared.get('issuerSigned'))
Expand All @@ -73,7 +89,16 @@ export class MdocDeviceResponse {
)
})

return new MdocDeviceResponse(base64Url, documents)
return new MdocDeviceResponse(TypedArrayEncoder.toBase64URL(mdoc.encode()), documents)
}

public static fromBase64Url(base64Url: string) {
const parsed = parseDeviceResponse(TypedArrayEncoder.fromBase64(base64Url))
if (parsed.status !== MDocStatus.OK) {
throw new MdocError('Parsing Mdoc Device Response failed.')
}

return MdocDeviceResponse.fromDeviceResponse(parsed)
}

private static assertMdocInputDescriptor(inputDescriptor: InputDescriptorV2) {
Expand Down Expand Up @@ -237,7 +262,6 @@ export class MdocDeviceResponse {

for (const document of options.mdocs) {
const deviceKeyJwk = document.deviceKeyJwk
document.deviceKey
if (!deviceKeyJwk) throw new MdocError(`Device key is missing in mdoc with doctype ${document.docType}`)
const alg = MdocDeviceResponse.getAlgForDeviceKeyJwk(deviceKeyJwk)

Expand Down
2 changes: 1 addition & 1 deletion packages/didcomm/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"rxjs": "^7.8.0"
},
"devDependencies": {
"@animo-id/pex": "5.2.0",
"@animo-id/pex": "^6.0.0",
"@sphereon/pex-models": "^2.3.2",
"@types/luxon": "^3.2.0",
"reflect-metadata": "^0.1.13",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -913,52 +913,57 @@ export class OpenId4VpVerifierService {
throw new CredoError('Expected vp_token entry for format mso_mdoc to be of type string')
}
const mdocDeviceResponse = MdocDeviceResponse.fromBase64Url(presentation)
if (mdocDeviceResponse.documents.length !== 1) {
throw new CredoError('Only a single mdoc is supported per device response for OpenID4VP verification')
if (mdocDeviceResponse.documents.length === 0) {
throw new CredoError('mdoc device response does not contain any mdocs')
}

const document = mdocDeviceResponse.documents[0]
const certificateChain = document.issuerSignedCertificateChain.map((cert) =>
X509Certificate.fromRawCertificate(cert)
)
const deviceResponses = mdocDeviceResponse.splitIntoSingleDocumentResponses()

const trustedCertificates = await x509Config.getTrustedCertificatesForVerification?.(agentContext, {
certificateChain,
verification: {
type: 'credential',
credential: document,
openId4VcVerificationSessionId: options.verificationSessionId,
},
})
for (const deviceResponseIndex in deviceResponses) {
const mdocDeviceResponse = deviceResponses[deviceResponseIndex]

let sessionTranscriptOptions: MdocSessionTranscriptOptions
if (options.origin) {
sessionTranscriptOptions = {
type: 'openId4VpDcApi',
clientId: options.audience,
verifierGeneratedNonce: options.nonce,
origin: options.origin,
}
} else {
if (!options.mdocGeneratedNonce || !options.responseUri) {
throw new CredoError(
'mdocGeneratedNonce and responseUri are required for mdoc openid4vp session transcript calculation'
)
}
sessionTranscriptOptions = {
type: 'openId4Vp',
clientId: options.audience,
mdocGeneratedNonce: options.mdocGeneratedNonce,
responseUri: options.responseUri,
verifierGeneratedNonce: options.nonce,
}
}
const document = mdocDeviceResponse.documents[0]
const certificateChain = document.issuerSignedCertificateChain.map((cert) =>
X509Certificate.fromRawCertificate(cert)
)

await mdocDeviceResponse.verify(agentContext, {
sessionTranscriptOptions,
trustedCertificates,
})
const trustedCertificates = await x509Config.getTrustedCertificatesForVerification?.(agentContext, {
certificateChain,
verification: {
type: 'credential',
credential: document,
openId4VcVerificationSessionId: options.verificationSessionId,
},
})

let sessionTranscriptOptions: MdocSessionTranscriptOptions
if (options.origin) {
sessionTranscriptOptions = {
type: 'openId4VpDcApi',
clientId: options.audience,
verifierGeneratedNonce: options.nonce,
origin: options.origin,
}
} else {
if (!options.mdocGeneratedNonce || !options.responseUri) {
throw new CredoError(
'mdocGeneratedNonce and responseUri are required for mdoc openid4vp session transcript calculation'
)
}
sessionTranscriptOptions = {
type: 'openId4Vp',
clientId: options.audience,
mdocGeneratedNonce: options.mdocGeneratedNonce,
responseUri: options.responseUri,
verifierGeneratedNonce: options.nonce,
}
}

await mdocDeviceResponse.verify(agentContext, {
sessionTranscriptOptions,
trustedCertificates,
})
}
// TODO: extract transaction data hashes once https://github.com/openid/OpenID4VP/pull/330 is resolved

isValid = true
Expand Down
Loading
Loading