Skip to content

Commit

Permalink
feat: openid4vci mdoc-issuance alpha
Browse files Browse the repository at this point in the history
Signed-off-by: Martin Auer <martin.auer97@gmail.com>
  • Loading branch information
auer-martin committed Oct 25, 2024
1 parent 23a9cb6 commit 0c4a093
Show file tree
Hide file tree
Showing 15 changed files with 398 additions and 11 deletions.
6 changes: 6 additions & 0 deletions demo-openid/src/Holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
const holder = new Holder(3000, 'OpenId4VcHolder ' + Math.random().toString())
await holder.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598e')

// Set trusted issuer certificates. Required fro verifying mdoc credentials
const trustedCertificates: string[] = []
await holder.agent.x509.setTrustedCertificates(
trustedCertificates.length === 0 ? undefined : (trustedCertificates as [string, ...string[]])
)

return holder
}

Expand Down
56 changes: 55 additions & 1 deletion demo-openid/src/Issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
OpenId4VcCredentialHolderDidBinding,
OpenId4VciCredentialRequestToCredentialMapper,
OpenId4VciCredentialSupportedWithId,
OpenId4VciSignMdocCredential,
OpenId4VcIssuerRecord,
} from '@credo-ts/openid4vc'

Expand All @@ -16,6 +17,10 @@ import {
W3cCredentialSubject,
W3cIssuer,
w3cDate,
X509Service,
KeyType,
X509ModuleConfig,
MdocService,
} from '@credo-ts/core'
import { OpenId4VcIssuerModule, OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
Expand All @@ -42,18 +47,29 @@ export const universityDegreeCredentialSdJwt = {
vct: 'UniversityDegreeCredential',
} satisfies OpenId4VciCredentialSupportedWithId

export const universityDegreeCredentialMdoc = {
id: 'UniversityDegreeCredential-mdoc',
format: OpenId4VciCredentialFormatProfile.MsoMdoc,
doctype: 'UniversityDegreeCredential',
} satisfies OpenId4VciCredentialSupportedWithId

export const credentialsSupported = [
universityDegreeCredential,
openBadgeCredential,
universityDegreeCredentialSdJwt,
universityDegreeCredentialMdoc,
] satisfies OpenId4VciCredentialSupportedWithId[]

function getCredentialRequestToCredentialMapper({
issuerDidKey,
}: {
issuerDidKey: DidKey
}): OpenId4VciCredentialRequestToCredentialMapper {
return async ({ holderBinding, credentialConfigurationIds }) => {
return async ({ holderBinding, credentialConfigurationIds, agentContext }) => {
const trustedCertificates = agentContext.dependencyManager.resolve(X509ModuleConfig).trustedCertificates
if (trustedCertificates?.length !== 1) {
throw new Error(`Expected exactly one trusted certificate. Received ${trustedCertificates?.length}.`)
}
const credentialConfigurationId = credentialConfigurationIds[0]

if (credentialConfigurationId === universityDegreeCredential.id) {
Expand Down Expand Up @@ -110,6 +126,25 @@ function getCredentialRequestToCredentialMapper({
}
}

if (credentialConfigurationId === universityDegreeCredentialMdoc.id) {
const holderKey = await agentContext.dependencyManager
.resolve(MdocService)
.getKeyFromMdocCredentialHolderBinding(agentContext, holderBinding)

return {
credentialSupportedId: universityDegreeCredentialMdoc.id,
format: ClaimFormat.MsoMdoc,
docType: universityDegreeCredentialMdoc.doctype,
issuerCertificate: trustedCertificates[0],
holderKey,
namespaces: {
'Leopold-Franzens-University': {
degree: 'bachelor',
},
},
} satisfies OpenId4VciSignMdocCredential
}

throw new Error('Invalid request')
}
}
Expand Down Expand Up @@ -147,6 +182,25 @@ export class Issuer extends BaseAgent<{
public static async build(): Promise<Issuer> {
const issuer = new Issuer(2000, 'OpenId4VcIssuer ' + Math.random().toString())
await issuer.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598f')

const currentDate = new Date()
currentDate.setDate(currentDate.getDate() - 1)
const nextDay = new Date(currentDate)
nextDay.setDate(currentDate.getDate() + 2)

const selfSignedCertificate = await X509Service.createSelfSignedCertificate(issuer.agent.context, {
key: await issuer.agent.context.wallet.createKey({ keyType: KeyType.P256 }),
notBefore: currentDate,
notAfter: nextDay,
extensions: [],
name: 'C=DE',
})

const issuerCertficicate = selfSignedCertificate.toString('pem')
await issuer.agent.x509.setTrustedCertificates([issuerCertficicate])
console.log('Set the following certficate for the holder to verify mdoc credentials.')
console.log(issuerCertficicate)

issuer.issuerRecord = await issuer.agent.modules.openId4VcIssuer.createIssuer({
credentialsSupported,
})
Expand Down
13 changes: 13 additions & 0 deletions packages/core/src/modules/mdoc/MdocOptions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Mdoc } from './Mdoc'
import type { Jwk } from '../../crypto'
import type { Key } from '../../crypto/Key'
import type { DifPresentationExchangeDefinition } from '../dif-presentation-exchange'
import type { ValidityInfo, MdocNameSpaces } from '@protokoll/mdoc-client'
Expand Down Expand Up @@ -56,3 +57,15 @@ export type MdocSignOptions = {
issuerCertificate: string
holderKey: Key
}

export type MdocCredentialHolderDidBinding = {
method: 'did'
didUrl: string
}

export type MdocCredentialHolderJwkBinding = {
method: 'jwk'
jwk: Jwk
}

export type MdocCredentialHolderBinding = MdocCredentialHolderDidBinding | MdocCredentialHolderJwkBinding
26 changes: 26 additions & 0 deletions packages/core/src/modules/mdoc/MdocService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ import type {
MdocDeviceResponseOpenId4VpOptions,
MdocDeviceResponseVerifyOptions,
MdocVerifyOptions,
MdocCredentialHolderBinding,
} from './MdocOptions'
import type { Query, QueryOptions } from '../../storage/StorageService'

import { injectable } from 'tsyringe'

import { AgentContext } from '../../agent'
import { Key } from '../../crypto'
import { DidResolverService, getKeyFromVerificationMethod, parseDid } from '../dids'

import { Mdoc } from './Mdoc'
import { MdocDeviceResponse } from './MdocDeviceResponse'
Expand Down Expand Up @@ -37,6 +40,29 @@ export class MdocService {
return await mdoc.verify(agentContext, options)
}

public async getKeyFromMdocCredentialHolderBinding(
agentContext: AgentContext,
holderBinding: MdocCredentialHolderBinding
) {
let holderKey: Key
if (holderBinding.method !== 'jwk') {
const parsedDid = parseDid(holderBinding.didUrl)
if (!parsedDid.fragment) {
throw new Error(
`didUrl '${holderBinding.didUrl}' does not contain a '#'. Unable to derive key from did document`
)
}

const didResolver = agentContext.dependencyManager.resolve(DidResolverService)
const didDocument = await didResolver.resolveDidDocument(agentContext, holderBinding.didUrl)
holderKey = getKeyFromVerificationMethod(didDocument.dereferenceKey(holderBinding.didUrl, ['assertionMethod']))
} else {
holderKey = holderBinding.jwk.key
}

return holderKey
}

public async createOpenId4VpDeviceResponse(agentContext: AgentContext, options: MdocDeviceResponseOpenId4VpOptions) {
return MdocDeviceResponse.createOpenId4VpDeviceResponse(agentContext, options)
}
Expand Down
30 changes: 30 additions & 0 deletions packages/openid4vc/src/openid4vc-holder/OpenId4VciHolderService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
AuthorizationDetails,
AuthorizationDetailsJwtVcJson,
AuthorizationDetailsJwtVcJsonLdAndLdpVc,
AuthorizationDetailsMsoMdoc,
AuthorizationDetailsSdJwtVc,
CredentialResponse,
Jwt,
Expand All @@ -37,6 +38,8 @@ import {
Jwk,
JwsService,
Logger,
Mdoc,
MdocApi,
SdJwtVcApi,
SignatureSuiteRegistry,
TypedArrayEncoder,
Expand Down Expand Up @@ -178,6 +181,14 @@ export class OpenId4VciHolderService {
vct: offeredCredential.vct,
claims: offeredCredential.claims,
} satisfies AuthorizationDetailsSdJwtVc
} else if (format === OpenId4VciCredentialFormatProfile.MsoMdoc) {
return {
type,
format,
locations,
claims: offeredCredential.claims,
doctype: offeredCredential.doctype,
} satisfies AuthorizationDetailsMsoMdoc
} else {
throw new CredoError(`Cannot create authorization_details. Unsupported credential format '${format}'.`)
}
Expand Down Expand Up @@ -662,6 +673,7 @@ export class OpenId4VciHolderService {
case OpenId4VciCredentialFormatProfile.JwtVcJson:
case OpenId4VciCredentialFormatProfile.JwtVcJsonLd:
case OpenId4VciCredentialFormatProfile.SdJwtVc:
case OpenId4VciCredentialFormatProfile.MsoMdoc:
signatureAlgorithm = options.possibleProofOfPossessionSignatureAlgorithms.find((signatureAlgorithm) =>
proofSigningAlgsSupported.includes(signatureAlgorithm)
)
Expand Down Expand Up @@ -782,6 +794,24 @@ export class OpenId4VciHolderService {
}

return { credential, notificationMetadata }
} else if (format === OpenId4VciCredentialFormatProfile.MsoMdoc) {
if (typeof credentialResponse.successBody.credential !== 'string')
throw new CredoError(
`Received a credential of format ${
OpenId4VciCredentialFormatProfile.MsoMdoc
}, but the credential is not a string. ${JSON.stringify(credentialResponse.successBody.credential)}`
)

const mdocApi = agentContext.dependencyManager.resolve(MdocApi)
const mdoc = Mdoc.fromBase64Url(credentialResponse.successBody.credential)
const verificationResult = await mdocApi.verify(mdoc, {})

if (!verificationResult.isValid) {
agentContext.config.logger.error('Failed to validate credential', { verificationResult })
throw new CredoError(`Failed to validate mdoc credential. Results = ${verificationResult.error}`)
}

return { credential: mdoc, notificationMetadata }
}

throw new CredoError(`Unsupported credential format ${credentialResponse.successBody.format}`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@ export type OpenId4VciSupportedCredentialFormats =
| OpenId4VciCredentialFormatProfile.JwtVcJsonLd
| OpenId4VciCredentialFormatProfile.SdJwtVc
| OpenId4VciCredentialFormatProfile.LdpVc
| OpenId4VciCredentialFormatProfile.MsoMdoc

export const openId4VciSupportedCredentialFormats: OpenId4VciSupportedCredentialFormats[] = [
OpenId4VciCredentialFormatProfile.JwtVcJson,
OpenId4VciCredentialFormatProfile.JwtVcJsonLd,
OpenId4VciCredentialFormatProfile.SdJwtVc,
OpenId4VciCredentialFormatProfile.LdpVc,
OpenId4VciCredentialFormatProfile.MsoMdoc,
]

export interface OpenId4VciNotificationMetadata {
Expand Down
38 changes: 38 additions & 0 deletions packages/openid4vc/src/openid4vc-issuer/OpenId4VcIssuerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
OpenId4VcIssuerMetadata,
OpenId4VciSignSdJwtCredential,
OpenId4VciSignW3cCredential,
OpenId4VciSignMdocCredential,
} from './OpenId4VcIssuerServiceOptions'
import type { OpenId4VcIssuanceSessionRecord } from './repository'
import type {
Expand Down Expand Up @@ -47,6 +48,7 @@ import {
KeyType,
utils,
W3cCredentialService,
MdocApi,
} from '@credo-ts/core'
import { VcIssuerBuilder } from '@sphereon/oid4vci-issuer'

Expand Down Expand Up @@ -499,6 +501,11 @@ export class OpenId4VcIssuerService {
offeredCredential.format === credentialRequest.format
) {
return offeredCredential.vct === credentialRequest.vct
} else if (
credentialRequest.format === OpenId4VciCredentialFormatProfile.MsoMdoc &&
offeredCredential.format === credentialRequest.format
) {
return offeredCredential.doctype === credentialRequest.doctype
}

return false
Expand All @@ -518,6 +525,18 @@ export class OpenId4VcIssuerService {
}
}

private getMsoMdocCredentialSigningCallback = (
agentContext: AgentContext,
options: OpenId4VciSignMdocCredential
): CredentialSignerCallback<DidDocument> => {
return async () => {
const mdocApi = agentContext.dependencyManager.resolve(MdocApi)

const mdoc = await mdocApi.sign(options)
return getSphereonVerifiableCredential(mdoc)
}
}

private getW3cCredentialSigningCallback = (
agentContext: AgentContext,
options: OpenId4VciSignW3cCredential
Expand Down Expand Up @@ -712,6 +731,25 @@ export class OpenId4VcIssuerService {
credential: { ...signOptions.payload } as unknown as CredentialIssuanceInput,
signCallback: this.getSdJwtVcCredentialSigningCallback(agentContext, signOptions),
}
} else if (signOptions.format === ClaimFormat.MsoMdoc) {
if (credentialRequest.format !== OpenId4VciCredentialFormatProfile.MsoMdoc) {
throw new CredoError(
`Invalid credential format. Expected '${OpenId4VciCredentialFormatProfile.MsoMdoc}', received '${credentialRequest.format}'.`
)
}

if (credentialRequest.doctype !== signOptions.docType) {
throw new CredoError(
`The types of the offered credentials do not match the types of the requested credential. Offered '${signOptions.docType}' Requested '${credentialRequest.doctype}'.`
)
}

return {
format: credentialRequest.format,
// NOTE: we don't use the credential value here as we pass the credential directly to the singer
credential: { ...signOptions.namespaces, docType: signOptions.docType } as unknown as CredentialIssuanceInput,
signCallback: this.getMsoMdocCredentialSigningCallback(agentContext, signOptions),
}
} else {
throw new CredoError(`Unsupported credential format ${signOptions.format}`)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import type {
W3cCredential,
SdJwtVcSignOptions,
JwaSignatureAlgorithm,
MdocSignOptions,
} from '@credo-ts/core'

export interface OpenId4VciPreAuthorizedCodeFlowConfig {
Expand Down Expand Up @@ -139,13 +140,21 @@ export type OpenId4VciCredentialRequestToCredentialMapper = (options: {
credentialConfigurationIds: [string, ...string[]]
}) => Promise<OpenId4VciSignCredential> | OpenId4VciSignCredential

export type OpenId4VciSignCredential = OpenId4VciSignSdJwtCredential | OpenId4VciSignW3cCredential
export type OpenId4VciSignCredential =
| OpenId4VciSignSdJwtCredential
| OpenId4VciSignW3cCredential
| OpenId4VciSignMdocCredential

export interface OpenId4VciSignSdJwtCredential extends SdJwtVcSignOptions {
credentialSupportedId: string
format: ClaimFormat.SdJwtVc | `${ClaimFormat.SdJwtVc}`
}

export interface OpenId4VciSignMdocCredential extends MdocSignOptions {
credentialSupportedId: string
format: ClaimFormat.MsoMdoc | `${ClaimFormat.MsoMdoc}`
}

export interface OpenId4VciSignW3cCredential {
credentialSupportedId: string
format: ClaimFormat.JwtVc | `${ClaimFormat.JwtVc}` | ClaimFormat.LdpVc | `${ClaimFormat.LdpVc}`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,9 @@ export class OpenId4VcSiopVerifierService {
responseTypesSupported: [ResponseType.VP_TOKEN],
subject_syntax_types_supported: supportedDidMethods.map((m) => `did:${m}`),
vpFormatsSupported: {
mso_mdoc: {
alg: supportedAlgs,
},
jwt_vc: {
alg: supportedAlgs,
},
Expand Down
Loading

0 comments on commit 0c4a093

Please sign in to comment.