-
Notifications
You must be signed in to change notification settings - Fork 23
#4087: DCQL credential query parser #4091
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
Closed
stevenvegt
wants to merge
21
commits into
feature/4067-credential-selection-dcql
from
feature/4087-dcql-parser
Closed
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
55f59ea
#4087: DCQL parser — tracer bullet: single claim value matching
stevenvegt 191a30f
#4087: DCQL parser — test non-matching value returns empty
stevenvegt f419d33
#4087: DCQL parser — test nested path resolution
stevenvegt 7451a54
#4087: DCQL parser — test multiple values OR semantics
stevenvegt 1028bcb
#4087: DCQL parser — test multiple claims AND semantics
stevenvegt f21ef00
#4087: DCQL parser — test missing field and empty credentialSubject
stevenvegt aaf4d82
#4087: DCQL parser — test empty list and multiple credentials filtering
stevenvegt 9d7c511
#4087: DCQL parser — test integer and boolean value matching
stevenvegt f14e4b4
#4087: DCQL parser — test claim without values (existence check)
stevenvegt 6414127
#4087: DCQL parser — test JSON-deserialized query and credential
stevenvegt bd1ec4f
#4087: DCQL parser — add ID validation and error return
stevenvegt bc02b1a
#4087: DCQL parser — root-level path resolution
stevenvegt 596e241
#4087: DCQL parser — array index path support, Path type []any
stevenvegt dd825fd
#4087: DCQL parser — null wildcard path support
stevenvegt 4dc3fdd
#4087: DCQL parser — add benchmark for 2000 credentials
stevenvegt 395f733
#4087: DCQL parser — add README documenting supported subset
stevenvegt b4ed184
#4087: Address PR review feedback
stevenvegt 238bdfb
#4087: Address PR feedback — validate paths and credentialSubject arrays
stevenvegt 08a65af
#4087: Document credentialSubject array handling in README
stevenvegt 57b2879
#4087: Handle credentialSubject as both object and array
stevenvegt 7e5d625
#4087: Fix non-zero index on single credentialSubject, add coverage t…
stevenvegt File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| # DCQL — Digital Credentials Query Language | ||
|
|
||
| This package implements a subset of the Digital Credentials Query Language (DCQL) | ||
| as specified in [OpenID for Verifiable Presentations 1.0](https://openid.net/specs/openid-4-verifiable-presentations-1_0.html), | ||
| sections 6.1, 6.3, and 7. | ||
|
|
||
| ## Purpose | ||
|
|
||
| DCQL is used in this codebase for **deterministic credential selection** from the wallet. | ||
| When multiple credentials of the same type exist (e.g., multiple PatientEnrollmentCredentials | ||
| for different patients), the EHR provides a DCQL credential query to specify which one to present. | ||
|
|
||
| This differs from the spec's primary use case, where DCQL is used by a Verifier to request | ||
| selective disclosure from a Wallet. In that context, the `values` parameter is a best-effort | ||
| privacy hint — the spec states: *"Verifiers MUST treat restrictions expressed using values as | ||
| a best-effort way to improve user privacy, but MUST NOT rely on it for security checks."* | ||
|
|
||
| In our context, the node is selecting from its own wallet on behalf of the EHR. The matching | ||
| is deterministic: if a credential matches the query, it is selected. If no credential matches, | ||
| an empty result is returned. The caller (e.g., the `CredentialSelector` in the PD matcher) is | ||
| responsible for deciding whether an empty result is an error. There is no privacy negotiation | ||
| involved. | ||
|
|
||
| ## Supported subset | ||
|
|
||
| ### Credential Query (section 6.1) | ||
|
|
||
| | Field | Status | Notes | | ||
| |-------|--------|-------| | ||
| | `id` | Supported | Validated: non-empty, alphanumeric/underscore/hyphen | | ||
| | `claims` | Supported | Array of claims queries | | ||
| | `format` | Not supported | Format selection handled by Presentation Definition matching | | ||
| | `meta` | Not supported | Metadata constraints handled by Presentation Definition matching | | ||
| | `multiple` | Not supported | Handled by `match_policy` in the filter chain | | ||
| | `claim_sets` | Not supported | Not needed for value-based selection | | ||
| | `trusted_authorities` | Not supported | Trust handled by PD matching and DID resolution | | ||
| | `require_cryptographic_holder_binding` | Not supported | Handled by VP verification | | ||
|
|
||
| ### Claims Query (section 6.3) | ||
|
|
||
| | Field | Status | Notes | | ||
| |-------|--------|-------| | ||
| | `path` | Supported | Claims Path Pointer per section 7 | | ||
| | `values` | Supported | Exact value matching with OR semantics | | ||
| | `id` | Not supported | Only needed with `claim_sets` | | ||
|
|
||
| ### Claims Path Pointer (section 7) | ||
|
|
||
| | Element type | Status | Notes | | ||
| |-------------|--------|-------| | ||
| | String | Supported | Key lookup in JSON objects | | ||
| | Non-negative integer | Supported | Array index lookup | | ||
| | Null | Supported | Wildcard — selects all array elements | | ||
|
|
||
| The path starts at the credential root, supporting top-level fields (`issuer`, `type`, etc.) | ||
| as well as nested `credentialSubject` fields. | ||
|
|
||
| `credentialSubject` handling: the W3C VC data model allows `credentialSubject` to be either a | ||
| single object or an array. The Go VC struct always models it as `[]map[string]any`. The DCQL | ||
| spec examples use paths without an array index (e.g., `["credentialSubject", "family_name"]`). | ||
|
|
||
| - **Single credentialSubject** (common case): the array is automatically unwrapped to a single | ||
| object, so paths like `["credentialSubject", "patientId"]` work without an index. | ||
| - **Multiple credentialSubjects** (rare): the path must include an explicit integer index to | ||
| select which subject, e.g., `["credentialSubject", 0, "patientId"]`. Using a string key on | ||
| an array with multiple elements returns an error. | ||
|
|
||
| ## Examples | ||
|
|
||
| ### Select a PatientEnrollmentCredential by BSN | ||
|
|
||
| ```json | ||
| { | ||
| "id": "id_patient_enrollment", | ||
| "claims": [ | ||
| { | ||
| "path": ["credentialSubject", "hasEnrollment", "patient", "identifier", "value"], | ||
| "values": ["123456789"] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ### Select a credential by multiple possible values (OR) | ||
|
|
||
| ```json | ||
| { | ||
| "id": "id_patient_enrollment", | ||
| "claims": [ | ||
| { | ||
| "path": ["credentialSubject", "hasEnrollment", "patient", "identifier", "value"], | ||
| "values": ["123456789", "987654321"] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ### Select by issuer DID | ||
|
|
||
| ```json | ||
| { | ||
| "id": "id_provider", | ||
| "claims": [ | ||
| { | ||
| "path": ["issuer"], | ||
| "values": ["did:x509:0:sha256:abc123::san:otherName:12345678"] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| ### Match a value anywhere in an array (null wildcard) | ||
|
|
||
| ```json | ||
| { | ||
| "id": "id_delegation", | ||
| "claims": [ | ||
| { | ||
| "path": ["credentialSubject", "qualifications", null, "roleCode"], | ||
| "values": ["30.000"] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| This matches if any element in the `qualifications` array has `roleCode` equal to `"30.000"`. | ||
|
|
||
| ### Multiple claims (AND semantics) | ||
|
|
||
| ```json | ||
| { | ||
| "id": "id_enrollment", | ||
| "claims": [ | ||
| { | ||
| "path": ["credentialSubject", "hasEnrollment", "patient", "identifier", "value"], | ||
| "values": ["123456789"] | ||
| }, | ||
| { | ||
| "path": ["credentialSubject", "hasEnrollment", "enrolledBy", "identifier", "value"], | ||
| "values": ["87654321"] | ||
| } | ||
| ] | ||
| } | ||
| ``` | ||
|
|
||
| Both claims must match for a credential to be selected. | ||
|
|
||
| ## Performance | ||
|
|
||
| Benchmark on Apple M5, worst case (match last of 2000 credentials, 2 claims with wildcards, | ||
| each credential has multiple identifiers and qualifications): | ||
|
|
||
| ``` | ||
| BenchmarkMatch_2000Credentials ~24ms/op ~24MB/op ~504k allocs/op | ||
| ``` | ||
|
|
||
| The cost is dominated by `json.Marshal`/`json.Unmarshal` per credential for generic root-level | ||
| path resolution. For typical use cases (tens of credentials, not thousands) this is negligible. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.