Skip to content
Closed
Show file tree
Hide file tree
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 Mar 21, 2026
191a30f
#4087: DCQL parser — test non-matching value returns empty
stevenvegt Mar 21, 2026
f419d33
#4087: DCQL parser — test nested path resolution
stevenvegt Mar 21, 2026
7451a54
#4087: DCQL parser — test multiple values OR semantics
stevenvegt Mar 21, 2026
1028bcb
#4087: DCQL parser — test multiple claims AND semantics
stevenvegt Mar 21, 2026
f21ef00
#4087: DCQL parser — test missing field and empty credentialSubject
stevenvegt Mar 21, 2026
aaf4d82
#4087: DCQL parser — test empty list and multiple credentials filtering
stevenvegt Mar 21, 2026
9d7c511
#4087: DCQL parser — test integer and boolean value matching
stevenvegt Mar 21, 2026
f14e4b4
#4087: DCQL parser — test claim without values (existence check)
stevenvegt Mar 21, 2026
6414127
#4087: DCQL parser — test JSON-deserialized query and credential
stevenvegt Mar 22, 2026
bd1ec4f
#4087: DCQL parser — add ID validation and error return
stevenvegt Mar 22, 2026
bc02b1a
#4087: DCQL parser — root-level path resolution
stevenvegt Mar 22, 2026
596e241
#4087: DCQL parser — array index path support, Path type []any
stevenvegt Mar 23, 2026
dd825fd
#4087: DCQL parser — null wildcard path support
stevenvegt Mar 23, 2026
4dc3fdd
#4087: DCQL parser — add benchmark for 2000 credentials
stevenvegt Mar 23, 2026
395f733
#4087: DCQL parser — add README documenting supported subset
stevenvegt Mar 24, 2026
b4ed184
#4087: Address PR review feedback
stevenvegt Mar 24, 2026
238bdfb
#4087: Address PR feedback — validate paths and credentialSubject arrays
stevenvegt Mar 25, 2026
08a65af
#4087: Document credentialSubject array handling in README
stevenvegt Mar 25, 2026
57b2879
#4087: Handle credentialSubject as both object and array
stevenvegt Mar 25, 2026
7e5d625
#4087: Fix non-zero index on single credentialSubject, add coverage t…
stevenvegt Mar 25, 2026
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
158 changes: 158 additions & 0 deletions vcr/dcql/README.md
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.
Loading