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: openid4vp-mdoc #2080

Merged
merged 6 commits into from
Nov 1, 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
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,
Comment on lines +46 to +49
Copy link
Contributor

Choose a reason for hiding this comment

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

i don't understand this? mdoc also handle mdoc selecting now right? And what if the mdoc is not available, it should'nt be info the nright?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we set it to error below when we also check if all the mdoc credentials are available.

// 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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

no extra error provided in parsed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, not really atm

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

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
Loading