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: dcql alpha #2098

Draft
wants to merge 16 commits into
base: main
Choose a base branch
from
Draft
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
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"dependencies": {
"@digitalcredentials/jsonld": "^6.0.0",
"dcql": "^0.2.13",
"dcql": "^0.2.16",
"@digitalcredentials/jsonld-signatures": "^9.4.0",
"@digitalcredentials/vc": "^6.0.1",
"@multiformats/base-x": "^4.0.1",
Expand Down
38 changes: 17 additions & 21 deletions packages/core/src/modules/dcql/DcqlService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { AgentContext } from '../../agent'
import { DcqlCredential, DcqlMdocCredential, DcqlQuery, DcqlSdJwtVcCredential } from 'dcql'
import { injectable } from 'tsyringe'

import { JsonValue } from '../../types'
import { Mdoc, MdocApi, MdocDeviceResponse, MdocOpenId4VpSessionTranscriptOptions, MdocRecord } from '../mdoc'
import { IPresentationFrame, SdJwtVcApi, SdJwtVcRecord } from '../sd-jwt-vc'
import { SdJwtVcApi, SdJwtVcRecord, SdJwtVcService } from '../sd-jwt-vc'
import { buildDisclosureFrameForPayload } from '../sd-jwt-vc/disclosureFrame'
import { ClaimFormat, W3cCredentialRecord, W3cCredentialRepository } from '../vc'

import { DcqlError } from './DcqlError'
Expand Down Expand Up @@ -100,13 +100,13 @@ export class DcqlService {
const dcqlCredentials: DcqlCredential[] = credentialRecords.map((record) => {
if (record.type === 'MdocRecord') {
return {
credentialFormat: 'mso_mdoc',
credential_format: 'mso_mdoc',
doctype: record.getTags().docType,
namespaces: Mdoc.fromBase64Url(record.base64Url).issuerSignedNamespaces,
} satisfies DcqlMdocCredential
} else if (record.type === 'SdJwtVcRecord') {
return {
credentialFormat: 'vc+sd-jwt',
credential_format: 'vc+sd-jwt',
vct: record.getTags().vct,
claims: this.getSdJwtVcApi(agentContext).fromCompact(record.compactSdJwtVc)
.prettyClaims as DcqlSdJwtVcCredential.Claims,
Expand All @@ -120,7 +120,18 @@ export class DcqlService {
const queryResult = DcqlQuery.query(DcqlQuery.parse(dcqlQuery), dcqlCredentials)
const matchesWithRecord = Object.fromEntries(
Object.entries(queryResult.credential_matches).map(([credential_query_id, result]) => {
return [credential_query_id, { ...result, record: credentialRecords[result.credential_index] }]
if (result.success) {
if (result.output.credential_format === 'vc+sd-jwt') {
const sdJwtVcRecord = credentialRecords[result.input_credential_index] as SdJwtVcRecord
agentContext.dependencyManager
Copy link
Contributor

Choose a reason for hiding this comment

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

The return value of this call is not used

.resolve(SdJwtVcService)
.applyDisclosuresForPayload(sdJwtVcRecord.compactSdJwtVc, result.output.claims)
}

return [credential_query_id, { ...result, record: credentialRecords[result.input_credential_index] }]
} else {
return [credential_query_id, result]
}
})
)

Expand Down Expand Up @@ -207,21 +218,6 @@ export class DcqlService {
return DcqlQuery.parse(dcqlQuery)
}

// TODO: this IS WRONG
private createPresentationFrame(obj: Record<string, JsonValue>): IPresentationFrame {
const frame: IPresentationFrame = {}

for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object' && value !== null) {
frame[key] = true
} else {
frame[key] = !!value
}
}

return frame
}

public async createPresentation(
agentContext: AgentContext,
options: {
Expand Down Expand Up @@ -268,7 +264,7 @@ export class DcqlService {

dcqlPresentation[credentialQueryId] = MdocDeviceResponse.fromBase64Url(deviceResponseBase64Url)
} else if (presentationToCreate.claimFormat === ClaimFormat.SdJwtVc) {
const presentationFrame = this.createPresentationFrame(presentationToCreate.disclosedPayload)
const presentationFrame = buildDisclosureFrameForPayload(presentationToCreate.disclosedPayload)

if (!domain) {
throw new DcqlError('Missing domain property for creating SdJwtVc presentation.')
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/modules/dcql/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ export type DcqlMdocCredential = _DcqlMdocCredential.Model['Input']
export type DcqlSdJwtVcCredential = _DcqlSdJwtVcCredential.Model['Input']
export type DcqlW3cVcCredential = _DcqlW3cVcCredential.Model['Input']

export type DcqlMatchWithRecord = {
record: W3cCredentialRecord | SdJwtVcRecord | MdocRecord
} & _DcqlQueryResult['credential_matches'][number]
export type DcqlMatchWithRecord =
| (_DcqlQueryResult['credential_matches'][number] & {
success: true
record: MdocRecord | SdJwtVcRecord | W3cCredentialRecord
})
| (_DcqlQueryResult['credential_matches'][number] & {
success: false
})

export type DcqlQueryResult = Omit<_DcqlQueryResult.Input, 'credential_matches'> & {
credential_matches: Record<string, DcqlMatchWithRecord>
Expand Down
7 changes: 4 additions & 3 deletions packages/core/src/modules/sd-jwt-vc/SdJwtVcService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { AgentContext } from '../../agent'
import { JwtPayload, Jwk, getJwkFromJson, getJwkFromKey, Hasher } from '../../crypto'
import { CredoError } from '../../error'
import { X509Service } from '../../modules/x509/X509Service'
import { JsonObject } from '../../types'
import { TypedArrayEncoder, nowInSeconds } from '../../utils'
import { getDomainFromUrl } from '../../utils/domain'
import { fetchWithTimeout } from '../../utils/fetch'
Expand All @@ -31,7 +32,7 @@ import { X509Certificate, X509ModuleConfig } from '../x509'

import { SdJwtVcError } from './SdJwtVcError'
import { decodeSdJwtVc, sdJwtVcHasher } from './decodeSdJwtVc'
import { buildDisclosureFrameFromPayload } from './disclosureFrame'
import { buildDisclosureFrameForPayload } from './disclosureFrame'
import { SdJwtVcRecord, SdJwtVcRepository } from './repository'
import { SdJwtVcTypeMetadata } from './typeMetadata'

Expand Down Expand Up @@ -158,9 +159,9 @@ export class SdJwtVcService {
return decodeSdJwtVc(compactSdJwtVc, typeMetadata)
}

public applyDisclosuresForPayload(compactSdJwtVc: string, requestedPayload: Record<string, unknown>): SdJwtVc {
public applyDisclosuresForPayload(compactSdJwtVc: string, requestedPayload: JsonObject): SdJwtVc {
const decoded = decodeSdJwtSync(compactSdJwtVc, Hasher.hash)
const presentationFrame = buildDisclosureFrameFromPayload(requestedPayload) ?? {}
const presentationFrame = buildDisclosureFrameForPayload(requestedPayload) ?? {}

if (decoded.kbJwt) {
throw new SdJwtVcError('Cannot apply limit disclosure on an sd-jwt with key binding jwt')
Expand Down
39 changes: 15 additions & 24 deletions packages/core/src/modules/sd-jwt-vc/disclosureFrame.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
import type { JsonObject } from '../../types'

import { isObject } from 'class-validator'

type DisclosureFrame = {
[key: string]: boolean | DisclosureFrame
}

export function buildDisclosureFrameFromPayload(input: Record<string, unknown>): DisclosureFrame | null {
// Handle objects recursively
const result: DisclosureFrame = {}

// Base case: input is null or undefined
if (input === null || input === undefined) {
return result
}

for (const [key, value] of Object.entries(input)) {
// Ignore non-value values
if (value === null || value === undefined) continue

if (typeof value === 'object') {
export function buildDisclosureFrameForPayload(input: JsonObject): DisclosureFrame {
return Object.fromEntries(
Object.entries(input).map(([key, value]) => {
// TODO: Array disclosure frames are not yet supported - treating entire array as disclosed
if (Array.isArray(value)) {
// TODO: Array disclosure frames are not yet supported - treating entire array as disclosed
result[key] = true
return [key, true]
} else if (isObject(value)) {
if (Object.keys.length === 0) return [key, false]
return [key, buildDisclosureFrameForPayload(value)]
} else {
result[key] = buildDisclosureFrameFromPayload(value as Record<string, unknown>) ?? false
return [key, true]
}
} else {
// Handle primitive values
result[key] = true
}
}

return Object.keys(result).length > 0 ? result : null
})
)
}
2 changes: 1 addition & 1 deletion packages/openid4vc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"dependencies": {
"@credo-ts/core": "workspace:*",
"@sphereon/did-auth-siop": "https://gitpkg.vercel.app/animo/OID4VC/packages/siop-oid4vp?funke",
"@sphereon/did-auth-siop": "link:/../../../CODE/OID4VC/packages/siop-oid4vp",
"@sphereon/oid4vc-common": "0.16.1-fix.173",
"@sphereon/ssi-types": "0.30.2-next.135",
"class-transformer": "^0.5.1",
Expand Down
8 changes: 4 additions & 4 deletions packages/openid4vc/tests/openid4vc.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2339,15 +2339,15 @@ describe('OpenId4Vc', () => {
success: true,
output: {
doctype: 'org.eu.university',
credentialFormat: 'mso_mdoc',
credential_format: 'mso_mdoc',
namespaces: {
'eu.europa.ec.eudi.pid.1': {
name: 'John Doe',
degree: 'bachelor',
},
},
},
credential_index: 0,
input_credential_index: 0,
claim_set_index: undefined,
all: expect.any(Array),
record: expect.any(MdocRecord),
Expand All @@ -2356,13 +2356,13 @@ describe('OpenId4Vc', () => {
typed: true,
success: true,
output: {
credentialFormat: 'vc+sd-jwt',
credential_format: 'vc+sd-jwt',
vct: 'OpenBadgeCredential',
claims: {
university: 'innsbruck',
},
},
credential_index: 1,
input_credential_index: 1,
claim_set_index: undefined,
all: expect.any(Array),
record: expect.any(SdJwtVcRecord),
Expand Down
Loading