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 1 commit
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: 3 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,17 @@
"@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",
Copy link
Contributor

Choose a reason for hiding this comment

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

can you update this to unstable 5.0.0-unstable.25

"@sphereon/kmp-mdl-mdoc": "0.2.0-SNAPSHOT.22",
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need this right?

"@sphereon/pex-models": "^2.3.1",
"@sphereon/ssi-types": "^0.30.1",
"@sphereon/ssi-types": "0.30.2-next.129",
Copy link
Contributor

Choose a reason for hiding this comment

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

And this one to 0.30.2-next.135 (for latest mdoc features)

"@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,11 @@ import type {
W3CVerifiablePresentation as SphereonW3CVerifiablePresentation,
} from '@sphereon/ssi-types'

import { CredoError } from '../../../error'
import { JsonTransformer } from '../../../utils'
import { MdocVerifiablePresentation } from '../../mdoc/MdocVerifiablePresentation'
import { com } from '@sphereon/kmp-mdl-mdoc'

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

Expand All @@ -32,8 +34,10 @@ 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 com.sphereon.mdoc.data.device.DeviceResponseCbor.Static.cborDecode(
new Int8Array(TypedArrayEncoder.fromBase64(verifiablePresentation.base64Url))
)
Copy link
Contributor

Choose a reason for hiding this comment

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

original can just be a string for PEX / ssi-types now. That way we do'nt have to depend on / import the sphereon mdl lib

} else {
return verifiablePresentation.compact
}
Expand All @@ -47,12 +51,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)
}
}
93 changes: 64 additions & 29 deletions packages/core/src/modules/mdoc/MdocDeviceResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
Verifier,
MDocStatus,
cborEncode,
parseDeviceResponse,
} from '@protokoll/mdoc-client'

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

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

public static isBase64DeviceResponse(base64Url: string) {
try {
const parsed = parseDeviceResponse(TypedArrayEncoder.fromBase64(base64Url))
if (parsed.status === MDocStatus.OK) return true
return false
} catch (error) {
// no-op
}

return false
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
try {
const parsed = parseDeviceResponse(TypedArrayEncoder.fromBase64(base64Url))
if (parsed.status === MDocStatus.OK) return true
return false
} catch (error) {
// no-op
}
return false
try {
const parsed = parseDeviceResponse(TypedArrayEncoder.fromBase64(base64Url))
return parsed.status === MDocStatus.OK
} catch (error) {
return false
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Also seems quite expensive. That we try to parse. But maybe the best way. Are we doing in places:

if (isBase64DeviceReponse(data)) {
   parseBase64DeviceResponse()
}

Because in that case a tryParseBase64DeviceResponse that returns null on error might be better?

}

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.fromIssuerSignedDocument(
Copy link
Contributor

Choose a reason for hiding this comment

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

fromIssuerSigned and then pasing deviceSigned is a bit weird maybe?

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 +148,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 = mdocLimitDisclosureToId(_mdoc, inputDescriptor)
Copy link
Contributor

Choose a reason for hiding this comment

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

Id is maybe a bit offputting as inputDescriptor ( i thought which ID?). So mabye just mdocLimitDisclosureToInputDescriptor?

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 +206,32 @@ 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.isRegistered(X509ModuleConfig)
? agentContext.dependencyManager.resolve(X509ModuleConfig)
: undefined
Copy link
Contributor

Choose a reason for hiding this comment

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

this should always be configured, are you getting issues when you just reslve it directly?

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 +245,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 @@ -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
13 changes: 7 additions & 6 deletions packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,13 @@
},
"dependencies": {
"@credo-ts/core": "workspace:*",
"@sphereon/did-auth-siop": "0.16.1-next.168",
"@sphereon/oid4vc-common": "0.16.1-next.168",
"@sphereon/oid4vci-client": "0.16.1-next.168",
"@sphereon/oid4vci-common": "0.16.1-next.168",
"@sphereon/oid4vci-issuer": "0.16.1-next.168",
"@sphereon/ssi-types": "^0.30.1",
"@sphereon/did-auth-siop": "0.16.1-fix.173",
"@sphereon/kmp-mdl-mdoc": "0.2.0-SNAPSHOT.22",
Copy link
Contributor

Choose a reason for hiding this comment

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

again, would be good to not depend on this.

"@sphereon/oid4vc-common": "0.16.1-fix.173",
"@sphereon/oid4vci-client": "0.16.1-fix.173",
"@sphereon/oid4vci-common": "0.16.1-fix.173",
"@sphereon/oid4vci-issuer": "0.16.1-fix.173",
"@sphereon/ssi-types": "0.30.2-next.129",
Copy link
Contributor

Choose a reason for hiding this comment

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

also update this one

"class-transformer": "^0.5.1",
"rxjs": "^7.8.0"
},
Expand Down
Loading
Loading