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: preparing for mdoc #1991

Closed
wants to merge 7 commits into from
Closed
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
3 changes: 3 additions & 0 deletions demo-openid/src/Holder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
W3cJwtVerifiableCredential,
W3cJsonLdVerifiableCredential,
DifPresentationExchangeService,
Mdoc,
} from '@credo-ts/core'
import { OpenId4VcHolderModule } from '@credo-ts/openid4vc'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'
Expand Down Expand Up @@ -56,6 +57,8 @@ export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>>
const credential = response.credential
if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) {
return this.agent.w3cCredentials.storeCredential({ credential })
} else if (credential instanceof Mdoc) {
return this.agent.mdoc.store(credential)
} else {
return this.agent.sdJwtVc.store(credential.compact)
}
Expand Down
11 changes: 8 additions & 3 deletions demo-openid/src/HolderInquirer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { SdJwtVcRecord, W3cCredentialRecord } from '@credo-ts/core'
import type { MdocRecord, SdJwtVcRecord, W3cCredentialRecord } from '@credo-ts/core'
import type { OpenId4VcSiopResolvedAuthorizationRequest, OpenId4VciResolvedCredentialOffer } from '@credo-ts/openid4vc'

import { DifPresentationExchangeService } from '@credo-ts/core'
import { DifPresentationExchangeService, Mdoc } from '@credo-ts/core'
import console, { clear } from 'console'
import { textSync } from 'figlet'
import { prompt } from 'inquirer'
Expand Down Expand Up @@ -181,11 +181,16 @@ export class HolderInquirer extends BaseInquirer {
}
}

private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord) => {
private printCredential = (credential: W3cCredentialRecord | SdJwtVcRecord | MdocRecord) => {
if (credential.type === 'W3cCredentialRecord') {
console.log(greenText(`W3cCredentialRecord with claim format ${credential.credential.claimFormat}`, true))
console.log(JSON.stringify(credential.credential.jsonCredential, null, 2))
console.log('')
} else if (credential.type === 'MdocRecord') {
console.log(greenText(`MdocRecord`, true))
const namespaces = Mdoc.fromIssuerSignedHex(credential.issuerSignedHex).namespaces
console.log(JSON.stringify(namespaces, null, 2))
console.log('')
} else {
console.log(greenText(`SdJwtVcRecord`, true))
const prettyClaims = this.holder.agent.sdJwtVc.fromCompact(credential.compactSdJwtVc).prettyClaims
Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@sd-jwt/sd-jwt-vc": "^0.7.0",
"@sd-jwt/types": "^0.7.0",
"@sd-jwt/utils": "^0.7.0",
"@sphereon/kmp-mdl-mdoc": "0.1.0-SNAPSHOT.21",
"@sphereon/pex": "^3.3.2",
"@sphereon/pex-models": "^2.2.4",
"@sphereon/ssi-types": "^0.28.0",
Expand All @@ -54,7 +55,7 @@
"did-resolver": "^4.1.0",
"jsonpath": "^1.1.1",
"lru_map": "^0.4.1",
"luxon": "^3.3.0",
"luxon": "^3.5.0",
"make-error": "^1.3.6",
"object-inspect": "^1.10.3",
"query-string": "^7.0.1",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/agent/AgentModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { DidsModule } from '../modules/dids'
import { DifPresentationExchangeModule } from '../modules/dif-presentation-exchange'
import { DiscoverFeaturesModule } from '../modules/discover-features'
import { GenericRecordsModule } from '../modules/generic-records'
import { MdocModule } from '../modules/mdoc'
import { MessagePickupModule } from '../modules/message-pickup'
import { OutOfBandModule } from '../modules/oob'
import { ProofsModule } from '../modules/proofs'
Expand Down Expand Up @@ -136,6 +137,7 @@ function getDefaultAgentModules() {
cache: () => new CacheModule(),
pex: () => new DifPresentationExchangeModule(),
sdJwtVc: () => new SdJwtVcModule(),
mdoc: () => new MdocModule(),
x509: () => new X509Module(),
} as const
}
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/agent/BaseAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CredentialsApi } from '../modules/credentials'
import { DidsApi } from '../modules/dids'
import { DiscoverFeaturesApi } from '../modules/discover-features'
import { GenericRecordsApi } from '../modules/generic-records'
import { MdocApi } from '../modules/mdoc'
import { MessagePickupApi } from '../modules/message-pickup/MessagePickupApi'
import { OutOfBandApi } from '../modules/oob'
import { ProofsApi } from '../modules/proofs'
Expand Down Expand Up @@ -60,6 +61,7 @@ export abstract class BaseAgent<AgentModules extends ModulesMap = EmptyModuleMap
public readonly oob: OutOfBandApi
public readonly w3cCredentials: W3cCredentialsApi
public readonly sdJwtVc: SdJwtVcApi
public readonly mdoc: MdocApi
public readonly x509: X509Api

public readonly modules: AgentApi<WithoutDefaultModules<AgentModules>>
Expand Down Expand Up @@ -110,6 +112,7 @@ export abstract class BaseAgent<AgentModules extends ModulesMap = EmptyModuleMap
this.oob = this.dependencyManager.resolve(OutOfBandApi)
this.w3cCredentials = this.dependencyManager.resolve(W3cCredentialsApi)
this.sdJwtVc = this.dependencyManager.resolve(SdJwtVcApi)
this.mdoc = this.dependencyManager.resolve(MdocApi)
this.x509 = this.dependencyManager.resolve(X509Api)

const defaultApis = [
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export { ReturnRouteTypes } from './decorators/transport/TransportDecorator'
export * from './plugins'
export * from './transport'
export * from './modules/basic-messages'
export * from './modules/mdoc'
export * from './modules/x509'
export * from './modules/common'
export * from './modules/credentials'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './DifPexCredentialsForRequest'
import type { Mdoc } 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 @@ -14,4 +15,4 @@ export { PresentationSubmissionLocation as DifPresentationExchangeSubmissionLoca

// TODO: we might want to move this to another place at some point
export type VerifiablePresentation = W3cVerifiablePresentation | SdJwtVc
export type VerifiableCredential = W3cVerifiableCredential | SdJwtVc
export type VerifiableCredential = W3cVerifiableCredential | SdJwtVc | Mdoc
138 changes: 138 additions & 0 deletions packages/core/src/modules/mdoc/Mdoc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import type { AgentContext } from '../../agent'

import { com, kotlin } from '@sphereon/kmp-mdl-mdoc'

import { JwaSignatureAlgorithm } from '../../crypto'
import { X509Service } from '../../modules/x509'
import { Buffer, TypedArrayEncoder } from '../../utils'

import { MdocCoseCallbackService } from './MdocCoseCallbackService'
import { MdocError } from './MdocError'
import { MdocX509CallbackService } from './MdocX509CallbackService'

type IssuerSignedJson = com.sphereon.mdoc.data.device.IssuerSignedJson
type IssuerSignedCbor = com.sphereon.mdoc.data.device.IssuerSignedCbor

type MdocIssuerSignedItem<T = unknown> = com.sphereon.mdoc.data.device.IssuerSignedItemJson<T>
type MdocNamespaceData = Record<string, MdocIssuerSignedItem>
type MdocNamespace = Record<string, MdocNamespaceData>

export class Mdoc {
private issuerSignedCbor: IssuerSignedCbor
private issuerSignedJson: IssuerSignedJson

private constructor(buffer: Buffer) {
// TODO: CONVERSION FROM CBOR TO JSON AND BACK CURRENTLY NOT COMPLETELY WORKING THEREFORE WE STORE BOTH FOR NOW
this.issuerSignedCbor = com.sphereon.mdoc.data.device.IssuerSignedCbor.Companion.cborDecode(Int8Array.from(buffer))
this.issuerSignedJson = this.issuerSignedCbor.toJson()
}

public get docType() {
// TODO: This will be a part of the { ... issuerSigned } structure
auer-martin marked this conversation as resolved.
Show resolved Hide resolved
return 'org.iso.18013.5.1.mDL'
}

// TODO: Use a different return type. Wait for sphereon
public get namespaces(): Record<string, unknown> {
const namespaces: MdocNamespace = {}
const entries = this.issuerSignedJson.nameSpaces?.asJsMapView().entries()

// eslint-disable-next-line no-constant-condition
while (true) {
const next = entries?.next()
if (next?.done) break
if (!next?.value) {
throw new MdocError('Missing value in Mdoc')
}

// TODO: This is not completely working yet. The values are still not json if nested.
// TODO: wait for sphereon to complete
// Waiting for fix from sphereon
const mdocDataItem = next.value[1]
const mdocDataItemRecord: Record<
string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
com.sphereon.mdoc.data.device.IssuerSignedItemJson<any>['elementValue']
> = Object.fromEntries(mdocDataItem.map((dataItem) => [dataItem.elementIdentifier, dataItem.elementValue]))

namespaces[next.value[0]] = mdocDataItemRecord
}

return namespaces
}

public get jwaSignatureAlgorithm() {
const alg = this.issuerSignedJson.issuerAuth.protectedHeader.alg
if (!alg) {
throw new MdocError(`Missing Signature Algorithm in Mdoc.`)
}

const jwaAlgorithm = com.sphereon.crypto.SignatureAlgorithmMapping.Companion.toJose(alg)
.value as JwaSignatureAlgorithm

if (!Object.values(JwaSignatureAlgorithm).includes(jwaAlgorithm)) {
throw new MdocError(`Invalid Signature Algorithm on MDoc Document. Alg '${alg}'`)
}

return jwaAlgorithm
}

public static fromIssuerSignedHex(hexEncodedMdoc: string) {
return new Mdoc(TypedArrayEncoder.fromHex(hexEncodedMdoc))
}

public static fromIssuerSignedBase64(issuerSignedBase64: string) {
return new Mdoc(TypedArrayEncoder.fromBase64(issuerSignedBase64))
}

public get issuerSignedHex() {
return TypedArrayEncoder.toHex(Buffer.from(this.issuerSignedCbor.toCbor().cborEncode()))
}

public get issuerSignedBase64UrlEncoded() {
return TypedArrayEncoder.toBase64URL(Buffer.from(this.issuerSignedCbor.toCbor().cborEncode()))
}

public async verify(agentContext: AgentContext, options?: { trustedCertificates?: [string, ...string[]] }) {
const { trustedCertificates } = options ?? {}

const cryptoServiceJS = com.sphereon.crypto.CryptoServiceJS

if (agentContext.contextCorrelationId !== 'default') {
// TODO: This way of of registering and working with the x509/cose services is subject to race-conditions
// TODO: This is a known issue and beeing worked on by sphereon
throw new MdocError('Multitenancy is currently not supported for Mdoc.')
}

cryptoServiceJS.X509.register(new MdocX509CallbackService(agentContext, trustedCertificates))
cryptoServiceJS.COSE.register(new MdocCoseCallbackService(agentContext))

const certificateChain = this.issuerSignedCbor.issuerAuth.toJson().unprotectedHeader?.x5chain
if (!certificateChain) {
return {
isValid: false,
error: `The issuer signed structure is missing the 'x5chain' property in the unprotected header.`,
}
}

// TODO: CHECK why this is required
const _leafCertificate = TypedArrayEncoder.toBase64(TypedArrayEncoder.fromBase64(certificateChain[0]))
const leafCertificate = X509Service.getLeafCertificate(agentContext, { certificateChain: [_leafCertificate] })

auer-martin marked this conversation as resolved.
Show resolved Hide resolved
const verificationResult = await com.sphereon.mdoc.ValidationsJS.fromIssuerAuthAsync(
this.issuerSignedCbor.issuerAuth,
// This key is used later on in the cose verification callback for signature verification
{ opts: kotlin.collections.KtMap.fromJsMap(new Map([['publicKey', leafCertificate.publicKey]])) },
trustedCertificates
)

if (verificationResult.error) {
return {
isValid: false,
error: verificationResult.verifications.join(' '),
}
}

return { isValid: true }
}
}
72 changes: 72 additions & 0 deletions packages/core/src/modules/mdoc/MdocApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type { MdocVerifyOptions } from './MdocOptions'
import type { MdocRecord } from './repository'
import type { Query, QueryOptions } from '../../storage/StorageService'

import { AgentContext } from '../../agent'
import { injectable } from '../../plugins'

import { Mdoc } from './Mdoc'
import { MdocService } from './MdocService'

/**
* @public
*/
@injectable()
export class MdocApi {
private agentContext: AgentContext
private mdocService: MdocService

public constructor(agentContext: AgentContext, mdocService: MdocService) {
this.agentContext = agentContext
this.mdocService = mdocService
}

/**
*
* Verify an incoming mdoc. It will check whether everything is valid, but also returns parts of the validation.
*
* For example, you might still want to continue with a flow if not all the claims are included, but the signature is valid.
*
*/
public async verify(options: MdocVerifyOptions) {
return await this.mdocService.verify(this.agentContext, options)
}

/**
* Create an Mdoc class from a hex encoded Mdoc Issuer-Signed structure
*/
public fromIssuerSignedHex(hexEncodedMdoc: string) {
return Mdoc.fromIssuerSignedHex(hexEncodedMdoc)
}

/**
* Create an Mdoc class from a base64/base64url encoded Mdoc Issuer-Signed structure
*/
public fromIssuerSignedBase64(issuerSignedBase64: string) {
return Mdoc.fromIssuerSignedBase64(issuerSignedBase64)
}

public async store(mdoc: Mdoc) {
return await this.mdocService.store(this.agentContext, mdoc)
}

public async getById(id: string): Promise<MdocRecord> {
return await this.mdocService.getById(this.agentContext, id)
}

public async getAll(): Promise<Array<MdocRecord>> {
return await this.mdocService.getAll(this.agentContext)
}

public async findAllByQuery(query: Query<MdocRecord>, queryOptions?: QueryOptions): Promise<Array<MdocRecord>> {
return await this.mdocService.findByQuery(this.agentContext, query, queryOptions)
}

public async deleteById(id: string) {
return await this.mdocService.deleteById(this.agentContext, id)
}

public async update(mdocRecord: MdocRecord) {
return await this.mdocService.update(this.agentContext, mdocRecord)
}
}
Loading
Loading