Skip to content

Commit

Permalink
feat: openid4vp-mdoc (openwallet-foundation#2080)
Browse files Browse the repository at this point in the history
  • Loading branch information
auer-martin authored Nov 1, 2024
1 parent 98ce8b6 commit 595c3d6
Show file tree
Hide file tree
Showing 22 changed files with 1,017 additions and 206 deletions.
6 changes: 6 additions & 0 deletions .changeset/beige-adults-compete.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@credo-ts/core": patch
"@credo-ts/openid4vc": patch
---

feat: mdoc device response and presentation over oid4vp
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/asn1-x509": "^2.3.8",
"@peculiar/x509": "^1.11.0",
"@protokoll/mdoc-client": "0.2.35",
"@protokoll/mdoc-client": "0.2.36",
"@sd-jwt/core": "^0.7.0",
"@sd-jwt/decode": "^0.7.0",
"@sd-jwt/jwt-status-list": "^0.7.0",
"@sd-jwt/sd-jwt-vc": "^0.7.0",
"@sd-jwt/types": "^0.7.0",
"@sd-jwt/utils": "^0.7.0",
"@sphereon/pex": "5.0.0-unstable.18",
"@sphereon/pex": "5.0.0-unstable.25",
"@sphereon/pex-models": "^2.3.1",
"@sphereon/ssi-types": "^0.30.1",
"@sphereon/ssi-types": "0.30.2-next.135",
"@stablelib/ed25519": "^1.0.2",
"@types/ws": "^8.5.4",
"abort-controller": "^3.0.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/crypto/jose/jwt/Jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ interface JwtOptions {
}

export class Jwt {
private static format = /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/
public static format = /^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$/

public readonly payload: JwtPayload
public readonly header: JwtHeader
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +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'
import type { MdocNameSpaces } from '@protokoll/mdoc-client'

export interface DifPexCredentialsForRequest {
/**
Expand Down Expand Up @@ -134,7 +134,7 @@ export type SubmissionEntryCredential =
| {
type: ClaimFormat.MsoMdoc
credentialRecord: MdocRecord
disclosedPayload: Record<string, IssuerSignedItem[]>
disclosedPayload: MdocNameSpaces
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from './DifPexCredentialsForRequest'
import type { Mdoc } from '../../mdoc'
import type { MdocVerifiablePresentation } from '../../mdoc/MdocVerifiablePresentation'
import type { Mdoc, MdocDeviceResponse } from '../../mdoc'
import type { SdJwtVc } from '../../sd-jwt-vc'
import type { W3cVerifiableCredential, W3cVerifiablePresentation } from '../../vc'
import type { PresentationDefinitionV1, PresentationDefinitionV2, PresentationSubmission } from '@sphereon/pex-models'
Expand All @@ -15,5 +14,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 | MdocVerifiablePresentation
export type VerifiablePresentation = W3cVerifiablePresentation | SdJwtVc | MdocDeviceResponse
export type VerifiableCredential = W3cVerifiableCredential | SdJwtVc | Mdoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { IPresentationDefinition, SelectResults, SubmissionRequirementMatch
import type { InputDescriptorV1, InputDescriptorV2, SubmissionRequirement } from '@sphereon/pex-models'

import { decodeSdJwtSync, getClaimsSync } from '@sd-jwt/decode'
import { Status } from '@sphereon/pex'
import { SubmissionRequirementMatchType } from '@sphereon/pex/dist/main/lib/evaluation/core'
import { Rules } from '@sphereon/pex-models'
import { default as jp } from 'jsonpath'
Expand Down Expand Up @@ -41,6 +42,11 @@ export async function getCredentialsForRequest(

const selectResults: CredentialRecordSelectResults = {
...selectResultsRaw,
areRequiredCredentialsPresent:
nonMdocPresentationDefinition.input_descriptors.length === 0 &&
mdocPresentationDefinition.input_descriptors.length > 0
? Status.INFO
: selectResultsRaw.areRequiredCredentialsPresent,
// Map the encoded credential to their respective w3c credential record
verifiableCredential: selectResultsRaw.verifiableCredential?.map((selectedEncoded): SubmissionEntryCredential => {
const credentialRecordIndex = encodedCredentials.findIndex((encoded) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import type {
W3CVerifiablePresentation as SphereonW3CVerifiablePresentation,
} from '@sphereon/ssi-types'

import { CredoError } from '../../../error'
import { Jwt } from '../../../crypto'
import { JsonTransformer } from '../../../utils'
import { MdocVerifiablePresentation } from '../../mdoc/MdocVerifiablePresentation'
import { MdocDeviceResponse } from '../../mdoc'
import { SdJwtVcApi } from '../../sd-jwt-vc'
import { W3cCredentialRecord, W3cJsonLdVerifiablePresentation, W3cJwtVerifiablePresentation } from '../../vc'

Expand All @@ -32,8 +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 if (verifiablePresentation instanceof MdocDeviceResponse) {
return verifiablePresentation.base64Url
} else {
return verifiablePresentation.compact
}
Expand All @@ -47,12 +47,11 @@ export function getVerifiablePresentationFromEncoded(
if (typeof encodedVerifiablePresentation === 'string' && encodedVerifiablePresentation.includes('~')) {
const sdJwtVcApi = agentContext.dependencyManager.resolve(SdJwtVcApi)
return sdJwtVcApi.fromCompact(encodedVerifiablePresentation)
} else if (typeof encodedVerifiablePresentation === 'string') {
} else if (typeof encodedVerifiablePresentation === 'string' && Jwt.format.test(encodedVerifiablePresentation)) {
return W3cJwtVerifiablePresentation.fromSerializedJwt(encodedVerifiablePresentation)
} 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')
return MdocDeviceResponse.fromBase64Url(encodedVerifiablePresentation)
}
}
26 changes: 14 additions & 12 deletions packages/core/src/modules/mdoc/Mdoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,26 @@ export class Mdoc {
return new Mdoc(issuerSignedDocument)
}

public static fromIssuerSignedDocument(
public static fromIssuerSignedDocument(issuerSignedBase64Url: string, expectedDocType?: string): Mdoc {
// eslint-disable-next-line @typescript-eslint/no-explicit-any

return new Mdoc(parseIssuerSigned(TypedArrayEncoder.fromBase64(issuerSignedBase64Url), expectedDocType))
}

public static fromDeviceSignedDocument(
issuerSignedBase64Url: string,
deviceSignedBase64Url?: 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
)
return new Mdoc(
parseDeviceSigned(
TypedArrayEncoder.fromBase64(deviceSignedBase64Url),
TypedArrayEncoder.fromBase64(issuerSignedBase64Url),
expectedDocType
)
} else {
return new Mdoc(parseIssuerSigned(TypedArrayEncoder.fromBase64(issuerSignedBase64Url), expectedDocType))
}
)
}

public get docType(): string {
Expand Down
81 changes: 51 additions & 30 deletions packages/core/src/modules/mdoc/MdocDeviceResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import type { PresentationDefinition } from '@protokoll/mdoc-client'
import type { InputDescriptorV2 } from '@sphereon/pex-models'

import {
limitDisclosureToInputDescriptor as mdocLimitDisclosureToId,
limitDisclosureToInputDescriptor as mdocLimitDisclosureToInputDescriptor,
COSEKey,
DeviceResponse,
MDoc,
parseIssuerSigned,
Verifier,
MDocStatus,
cborEncode,
parseDeviceResponse,
} from '@protokoll/mdoc-client'

import { CredoError } from '../../error'
Expand All @@ -26,7 +27,29 @@ import { getMdocContext } from './MdocContext'
import { MdocError } from './MdocError'

export class MdocDeviceResponse {
public constructor() {}
private constructor(public base64Url: string, public documents: Mdoc[]) {}

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

const documents = parsed.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.fromDeviceSignedDocument(
TypedArrayEncoder.toBase64URL(issuerSigned),
TypedArrayEncoder.toBase64URL(deviceSigned),
docType
)
})

return new MdocDeviceResponse(base64Url, documents)
}

private static assertMdocInputDescriptor(inputDescriptor: InputDescriptorV2) {
if (!inputDescriptor.format || !inputDescriptor.format.mso_mdoc) {
Expand Down Expand Up @@ -113,7 +136,18 @@ export class MdocDeviceResponse {

const inputDescriptor = this.assertMdocInputDescriptor(options.inputDescriptor)
const _mdoc = parseIssuerSigned(TypedArrayEncoder.fromBase64(mdoc.base64Url), mdoc.docType)
return mdocLimitDisclosureToId({ mdoc: _mdoc, inputDescriptor })

const disclosure = mdocLimitDisclosureToInputDescriptor(_mdoc, inputDescriptor)
const disclosedPayloadAsRecord = Object.fromEntries(
Object.entries(disclosure).map(([namespace, issuerSignedItem]) => {
return [
namespace,
Object.fromEntries(issuerSignedItem.map((item) => [item.elementIdentifier, item.elementValue])),
]
})
)

return disclosedPayloadAsRecord
}

public static async createOpenId4VpDeviceResponse(
Expand Down Expand Up @@ -160,32 +194,30 @@ export class MdocDeviceResponse {
}
}

public static async verify(agentContext: AgentContext, options: MdocDeviceResponseVerifyOptions) {
public async verify(agentContext: AgentContext, options: Omit<MdocDeviceResponseVerifyOptions, 'deviceResponse'>) {
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
}
const x509ModuleConfig = agentContext.dependencyManager.resolve(X509ModuleConfig)
const getTrustedCertificatesForVerification = x509ModuleConfig.getTrustedCertificatesForVerification

if (!trustedCerts) {
const trustedCertificates =
options.trustedCertificates ??
(await getTrustedCertificatesForVerification?.(agentContext, options.verificationContext)) ??
x509ModuleConfig?.trustedCertificates

if (!trustedCertificates) {
throw new MdocError('No trusted certificates found. Cannot verify mdoc.')
}

const result = await verifier.verifyDeviceResponse(
{
encodedDeviceResponse: TypedArrayEncoder.fromBase64(options.deviceResponse),
encodedDeviceResponse: TypedArrayEncoder.fromBase64(this.base64Url),
//ephemeralReaderKey: options.verifierKey ? getJwkFromKey(options.verifierKey).toJson() : undefined,
encodedSessionTranscript: DeviceResponse.calculateSessionTranscriptForOID4VP(options.sessionTranscriptOptions),
trustedCertificates: trustedCerts.map((cert) => X509Certificate.fromEncodedCertificate(cert).rawCertificate),
trustedCertificates: trustedCertificates.map(
(cert) => X509Certificate.fromEncodedCertificate(cert).rawCertificate
),
now: options.now,
},
mdocContext
Expand All @@ -199,17 +231,6 @@ export class MdocDeviceResponse {
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
)
})
return this.documents
}
}
3 changes: 2 additions & 1 deletion packages/core/src/modules/mdoc/MdocService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export class MdocService {
}

public async verifyDeviceResponse(agentContext: AgentContext, options: MdocDeviceResponseVerifyOptions) {
return MdocDeviceResponse.verify(agentContext, options)
const deviceResponse = MdocDeviceResponse.fromBase64Url(options.deviceResponse)
return deviceResponse.verify(agentContext, options)
}

public async store(agentContext: AgentContext, mdoc: Mdoc) {
Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/modules/mdoc/MdocVerifiablePresentation.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,11 @@ describe('mdoc device-response test', () => {
},
})

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')
expect(limitedDisclosedPayload).toStrictEqual({
hello: {
world: 'from-mdoc',
nicer: 'dicer',
},
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ describe('mdoc device-response openid4vp test', () => {
const docType = prepared.get('docType') as string
const issuerSigned = cborEncode(prepared.get('issuerSigned'))
const deviceSigned = cborEncode(prepared.get('deviceSigned'))
parsedDocument = Mdoc.fromIssuerSignedDocument(
parsedDocument = Mdoc.fromDeviceSignedDocument(
TypedArrayEncoder.toBase64URL(issuerSigned),
TypedArrayEncoder.toBase64URL(deviceSigned),
docType
Expand All @@ -220,8 +220,8 @@ describe('mdoc device-response openid4vp test', () => {
})

it('should be verifiable', async () => {
const res = await MdocDeviceResponse.verify(agent.context, {
deviceResponse,
const mdocDeviceResponse = MdocDeviceResponse.fromBase64Url(deviceResponse)
const res = await mdocDeviceResponse.verify(agent.context, {
trustedCertificates: [ISSUER_CERTIFICATE],
sessionTranscriptOptions: {
clientId,
Expand All @@ -246,9 +246,9 @@ describe('mdoc device-response openid4vp test', () => {
}
it(`with a different ${name}`, async () => {
try {
await MdocDeviceResponse.verify(agent.context, {
const mdocDeviceResponse = MdocDeviceResponse.fromBase64Url(deviceResponse)
await mdocDeviceResponse.verify(agent.context, {
trustedCertificates: [ISSUER_CERTIFICATE],
deviceResponse,
sessionTranscriptOptions: {
clientId: values.clientId,
responseUri: values.responseUri,
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/modules/mdoc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ export * from './MdocError'
export * from './MdocOptions'
export * from './repository'
export * from './Mdoc'
export * from './MdocVerifiablePresentation'
export * from './MdocDeviceResponse'
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import {
DifPresentationExchangeService,
DifPresentationExchangeSubmissionLocation,
} from '../../../dif-presentation-exchange'
import { MdocVerifiablePresentation } from '../../../mdoc'
import { MdocDeviceResponse } from '../../../mdoc'
import {
ANONCREDS_DATA_INTEGRITY_CRYPTOSUITE,
AnonCredsDataIntegrityServiceSymbol,
Expand Down Expand Up @@ -228,8 +228,8 @@ export class DifPresentationExchangeProofFormatService
firstPresentation instanceof W3cJwtVerifiablePresentation ||
firstPresentation instanceof W3cJsonLdVerifiablePresentation
? firstPresentation.encoded
: firstPresentation instanceof MdocVerifiablePresentation
? firstPresentation.deviceSignedBase64Url
: firstPresentation instanceof MdocDeviceResponse
? firstPresentation.base64Url
: firstPresentation?.compact
const attachment = this.getFormatData(encodedFirstPresentation, format.attachmentId)

Expand Down
Loading

0 comments on commit 595c3d6

Please sign in to comment.