feat: add W3C Digital Credentials API support via @sirosfoundation/dc-api - #16
Closed
leifj wants to merge 67 commits into
Closed
feat: add W3C Digital Credentials API support via @sirosfoundation/dc-api#16leifj wants to merge 67 commits into
leifj wants to merge 67 commits into
Conversation
… file Adds a -priv-in flag that allows specifying an existing private JWK file instead of generating a new key pair on every run. This is useful when you need a stable signing key across multiple invocations. When -priv-in is not set, behavior is unchanged (new key generated).
Use the Docker buildx automatic platform ARG TARGETARCH instead of hardcoding GOARCH=amd64. When building with plain 'docker build', the default (amd64) preserves current behavior. When building with 'docker buildx build --platform linux/amd64,linux/arm64', each platform variant compiles with the correct GOARCH automatically. This enables CI pipelines to produce multi-architecture container images without modifying the Makefile or build scripts.
Add a CI workflow that builds and pushes multi-arch container images for all services (verifier, registry, apigw, issuer) to GHCR. Features: - Multi-arch builds (linux/amd64, linux/arm64) via QEMU + buildx - Matrix strategy: 4 parallel jobs, one per service - Trivy CVE scanning with SARIF upload to Security tab - Standard semver tag policy via docker/metadata-action - GHA build cache per service - Manual dispatch (workflow_dispatch) for ad-hoc builds Triggers on semver tags (v*.*.*) and manual dispatch. Depends on: - SUNET#473 (TARGETARCH support for multi-arch) - SUNET#474 (inline gobuild for self-contained builds)
Pass inputs.image_tag through an environment variable instead of interpolating directly in the run block. This prevents potential script injection via crafted dispatch inputs. Addresses SonarQube security hotspot.
Pin action dependencies to immutable commit SHAs for supply-chain security. Resolves 6 SonarCloud security hotspots.
- Fix tag glob: use [0-9]* (glob) not [0-9]+ (regex) - Fix uses: indentation (align with name:/with: at step level) - Guard inputs context: use github.event_name checks so inputs.* is never evaluated on push events - Fix Trivy image-ref: use sanitized tag from custom-tag step output instead of raw inputs.image_tag - Export SAFE_TAG as 'version' output for consistent downstream use - Use step outputs for BUILDTAG instead of direct inputs reference
- Set context: git on docker/metadata-action so workflow_dispatch builds with inputs.git_ref derive metadata from the checked-out ref, not the triggering workflow ref. - Use steps.meta.outputs.version (instead of github.ref_name) for BUILDTAG so the version embedded in binaries matches the image tag.
When no presentation request templates are configured, the verifier falls back to buildDCQLQueryFromConfig() which iterated all claims from the VCTM and added them to the DCQL query. This caused the verifier to request every possible claim from the wallet, ignoring any claim filtering configured in presentation request templates. Remove the VCTM claim enumeration from the fallback path. When no templates are configured, the DCQL query now omits the Claims field entirely, letting the wallet decide what to disclose. To request specific claims, configure presentation request templates with explicit DCQL claim paths. Fixes SUNET#480
Previously, when presentation_requests_dir was configured but template loading failed (e.g. duplicate scopes across templates), the verifier silently fell back to the buildDCQLQueryFromConfig() path, which requests all VCTM claims instead of the configured subset. This made configuration errors invisible — the verifier appeared to work but requested far more claims than intended. Now template loading errors propagate from loadPresentationTemplates() through New(), preventing the verifier from starting with a broken configuration. Fixes SUNET#480
- Validate that -priv-in contains an EC P-256 private key immediately after parsing, failing fast with a clear error for wrong key type, curve, or public-only keys. - Guard against output files (-jwt-out, -jwk-out, -priv-out) matching -priv-in, preventing accidental overwrite of the private key.
The redirect_uri validator resolved hostnames via net.LookupIP() and rejected URIs whose hostnames didn't resolve (for http/https). This caused ccTLD domains like example.se to fail registration while example.org (which resolves to an IANA-reserved IP) succeeded. Redirect URIs are never fetched server-side — they're URLs the browser is redirected to. The SSRF concern that motivated the DNS check doesn't apply here. Remove the DNS resolution and private-IP blocking from the redirect_uri validator, keeping only the syntactic checks required by RFC 6749 (scheme present, no fragment). The safe_uri validator (used for server-side fetches like logo_uri) retains its DNS/SSRF checks. Add test cases for ccTLD and non-resolving hostnames.
Add a new endpoint that generates a pre-authorized credential offer for a specific document in the datastore. This restores the capability that was lost when PR SUNET#375 removed the old /api/v1/notification endpoint and the QR field from the CompleteDocument model. The endpoint: 1. Looks up the document by (authentic_source, scope, document_id) 2. Generates a credential offer with a pre-authorized code 3. Creates and persists an authorization context in the cache 4. Caches the document data for the credential endpoint 5. Returns the credential offer, credential_offer_url, and a QR code This follows the same pre-authorized code flow pattern used by the OIDC and SAML standalone authentication flows, but sources the document data from the datastore instead of from authentication claims. Closes SUNET#492
Wire OTel Metrics SDK alongside existing tracing infrastructure: - pkg/metric: MeterProvider with OTLP push + Prometheus scrape exporter - pkg/metric/vci.go: VCI counters (offers, tokens, credentials, notifications) and histograms (token latency, issuance latency) - pkg/metric/vp.go: VP counters (requests, presentations, failures) and histogram (verification latency) Instrument key handlers: - apigw: OAuthToken, VCICredential, VCIDeferredCredential, VCINotification, OIDC RP credential offer creation - verifier: CreateRequestObject, HandleDirectPost All metric labels use protocol-level metadata only (format, grant_type, credential_config_id, source, error_class) — no PII is ever recorded. Adds /metrics Prometheus scrape endpoint to apigw HTTP server. Adds common.metrics config section (mirrors common.tracing).
…ate swagger docs - Add missing DatastorePreAuthOffer stub to unimplementedApiv1 struct to fix build error in admin status tests - Add pkg/openid4vp to swagger-apigw parse directories so openid4vp.QRReply type is resolved - Regenerate apigw swagger docs
Per reviewer feedback (issue SUNET#492 comment), QR code generation is unnecessary server-side complexity that clients can handle more flexibly (custom size, branding, etc). The endpoint now returns only the credential_offer and credential_offer_url. Clients can generate QR codes from the URL as needed.
When AuthorizationDetails is set on the AuthorizationContext, the token endpoint returns authorization_details with credential_identifiers in the token response. Per OID4VCI spec, this forces the wallet to use credential_identifier (not credential_configuration_id) in the credential request. Most wallets use credential_configuration_id for pre-auth flows, causing a 400 error. Remove AuthorizationDetails from the pre-auth context. The scope is already conveyed via the Scopes field, and the credential offer itself contains the credential_configuration_id.
The credential endpoint dispatches on AuthProvider to retrieve cached documents. The datastore pre-auth handler was not setting AuthProvider, causing 'unsupported or missing auth provider' errors when the wallet redeemed the offer. Add AuthProviderDatastore constant and set it on the authorization context. The credential handler now recognizes it alongside the existing OIDC/SAML/OpenID4VP providers.
The credential endpoint's requireIdentifier check rejected datastore pre-auth sessions because they don't have an authenticated identifier. Like assertion-based issuance, datastore issuance sources its data from pre-uploaded documents, not identity-mapped lookups, so an empty identifier is valid. Downstream code already guards identifier usage with != "" checks.
…-api Integrates the @sirosfoundation/dc-api library for DC API protocol constants, feature detection, and native invocation. The verifier polyfill adds transport fallbacks (redirect, QR, SSE/poll). New files: - dc-api.js: vendored bundle of @sirosfoundation/dc-api (3.2 kB) - dc-api-polyfill.js: verifier integration importing the library, adding same-device redirect + cross-device SSE/poll fallbacks Updated: - presentation-definition.html/js: tries DC API first, falls back to QR - authorize_enhanced.html: uses polyfill for credential requests Detection uses W3C-specified feature checks: - typeof DigitalCredential !== 'undefined' - DigitalCredential.userAgentAllowsProtocol(protocol) Ref: W3C Digital Credentials API spec, CS-07 Dep: https://github.com/sirosfoundation/dc-api
- dc-api.js: rebuild bundle with es2022 target, var→const - dc-api-polyfill.js: use export...from, globalThis, RegExp.exec(), optional chaining, move SSE error handler to outer scope - presentation-definition.html: add id/label for select (a11y), add aria-label for group toggle checkbox - presentation-definition.js: remove unused imports, extract _tryNativeDCAPI() and _setupFallbackFlow() to reduce cognitive complexity, use optional chaining + globalThis
Adds newly exported: OID4VP_ALL_PROTOCOLS, OID4VP_SPEC_PROTOCOLS, isOID4VPProtocol
… §6.4.1 The DCQL spec (Section 6.4.1) defines claim_sets as an array of arrays of claim identifiers, but the implementation used []string (flat array). Additionally, ClaimQuery was missing the ID field which is required when claim_sets is present on a CredentialQuery. - Add ID field to ClaimQuery struct - Update MarshalJSON/UnmarshalJSON/UnmarshalYAML to handle id field - Change ClaimSet type from []string to [][]string - Update copyDCQL to deep-copy both the new ID field and nested slices - Add TestClaimSetsRoundTrip regression test
Native-app redirect URIs (e.g. com.example.app:/oauth2redirect) use custom schemes without an authority component. Only require a hostname for http/https schemes; for custom schemes, require at least a path or opaque component beyond the scheme.
…n test - Distinguish ErrNoDocumentFound from internal store failures instead of wrapping all errors as 'document not found' - Use assert.ErrorIs in test for robustness
…vendor - Call meter.Shutdown() in cmd/apigw and cmd/verifier for clean metric flush on graceful shutdown - Record IssuanceLatency on the failure path too so latency stats include failed requests - Run go mod tidy + go mod vendor to fix indirect annotations
The comment incorrectly referenced 'openid4vp-v1-unsigned' as the example protocol check. The library actually prefers signed requests via getBestProtocol() (signed > multisigned > unsigned).
Previously only org.iso.18013.5.1.mDL was routed to the mdl-issuer policy. Other mDoc doctypes (e.g. eu.europa.ec.eudi.pid.1) fell through to the generic credential-issuer action, which typically has no mdociaca registry configured — causing 'no registry returned positive match' denials in go-trust. Since DocType is exclusively an ISO 18013-5 (mDoc) concept, any non-empty DocType with an issuer/verifier role now routes to mdoc-issuer/mdoc-verifier policies that include the IACA registry.
extractMDocIssuerID now checks (in order): 1. URI SANs (e.g. https://issuer.example.com) 2. DNS SANs → converted to https://<hostname> 3. Configured IssuerURL on the Verifier (new field) 4. Certificate Organization (fallback for allowlist registries) This enables the mdociaca registry to discover IACA certificates via .well-known/openid-credential-issuer metadata when the DS certificate contains a SAN identifying the issuer. Also adds WithMDocIssuerURL option to MDocHandler for cases where the certificate doesn't contain a discoverable URL.
Adds the mdoc_iacas_uri field to .well-known/openid-credential-issuer
metadata, enabling go-trust's mdociaca registry to dynamically discover
IACA certificates for mDOC verification.
Configuration:
issuer_metadata:
mdoc_iacas_uri: "https://issuer.example.com/iacas"
The operator must serve an endpoint at that URL returning:
{"iacas": [{"certificate": "<base64-DER>"}]}
Addresses s-jairl's two comments on the PR: - The DCQL playground UI (presentation-definition.js) always attempted the native W3C Digital Credentials API regardless of server config. Thread it through the existing verifier.digital_credentials.enable flag (already respected by authorize_enhanced.html) via a new dc_api_enabled field on /ui/metadata, instead of adding a duplicate toggle. - When the user cancelled/declined the native DC API picker, the native call rejects with NotAllowedError. dc-api-polyfill.js's requestCredential() silently swallowed that and fell through to its own cross-device wait (SSE/poll, up to timeoutMs = 5min by default), even though both callers (presentation-definition.js, authorize_enhanced.html) already have their own immediate QR/wallet- link fallback UI. This left the page stuck on a loading spinner after cancelling, on both desktop Chromium and Android Chromium/ Vanadium. Let native failures propagate directly to callers instead; the polyfill fallback path is now only reached when native DC API isn't available/usable at all, not as a retry after a completed native attempt.
Phase 2 of hardening.
Update vendor.
Update pipeline version and dockerfile for dev-container default gola…
fix: add ClaimQuery.ID and fix ClaimSet type to [][]string per OID4VP §6.4.1
…uth-offer feat: add POST /api/v1/datastore/preauth_offer endpoint
…-mdoc-format fix(verifier): use credential format from metadata for custom presentation requests
…on-routing fix: route all mDoc doctypes to mdoc-issuer policy for IACA validation
…s-validation fix: don't DNS-resolve redirect_uris during client registration
…mit-claims fix: do not enumerate all VCTM claims in fallback DCQL query
Consistency fix per Copilot review comment on PR SUNET#524 - the sibling helper createValidJWTProof already formats this map one key per line.
# Conflicts: # go.mod # go.sum # vendor/modules.txt
- Move metric.NewVCI/NewVP construction out of cmd/apigw/main.go and cmd/verifier/main.go and into apiv1.New() itself, in both services. main.go now just constructs the shared *metric.Meter and passes it through; each apiv1.New() builds its own domain-specific metrics from it. Keeps metrics wiring next to the code that uses it instead of scattered across main(). - Rename the ambiguous `vci`/`vp` Client fields to `vciMetrics`/ `vpMetrics` in both apigw and verifier for clarity, and update all call sites.
feat: add OpenTelemetry metrics for credential issuance and verification
# Conflicts: # internal/verifier/staticembed/presentation-definition.js
…-docker-ci ci: add GitHub Actions workflow for Docker image builds
… precision Per Copilot's low-confidence suggestions on PR SUNET#524: - Express the tolerated/rejected iat offsets in createJWTProofWithIat tests relative to proofIatClockSkew instead of hard-coded durations, so the tests keep exercising the intended boundary if the constant changes. - Set iat via jwtv5.NewNumericDate(iat) instead of iat.Unix(), which truncated sub-second precision and made these tests less faithful to how the verifier actually parses claims (claims.GetIssuedAt()).
…lock-skew fix(openid4vci): allow clock skew when validating proof JWT iat
feat: W3C Digital Credentials API support via @sirosfoundation/dc-api
Bumps the vendored dc-api.js bundle to v0.5.0 and wires the verifier's native DC API path through the new requestCredentialFromAuthorizationRequestURI helper instead of passing the redirect-flow authorization_request URI directly as the DC API request value - that URI is neither a valid JWT (openid4vp-v1-signed) nor a valid raw-params object (openid4vp-v1-unsigned), so no protocol handler could ever parse it and every native DC API attempt failed. The new helper resolves the authorization request into whatever shape the detected protocol actually needs. Also fixes two bugs found in the same testing pass, both required for the above to actually reach a working state: - supported_wallets can arrive as JSON null (an unconfigured Go map marshals that way) - the schema had no fallback for it, so the whole /ui/metadata response failed to parse and the page hung on its loading spinner before a user could ever select a credential. - The claim-selection form always built DCQL meta.vct_values, even for mso_mdoc credentials, which have no vct - the correct constraint there is meta.doctype_value (OpenID4VP 1.0 6.4.1). Sending vct_values for an mdoc credential matches nothing on the wallet side. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NhWrEC7D4b3vxZ4wm3gDML
leifj
force-pushed
the
feat/dc-api-polyfill
branch
from
July 30, 2026 08:52
77f46df to
30c3e69
Compare
Collaborator
Author
|
Superseding this PR - the DC API polyfill feature it represents was already merged upstream via SUNET#504, and this fork branch has since diverged (119 commits behind upstream/main, base The one commit here that was genuinely never upstreamed - the authorization-request-bridge fix ( Closing this without merging; no content is lost, it's now tracked cleanly in SUNET#534. |
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
Integrates the W3C Digital Credentials API (DC API) into the vc verifier via the
@sirosfoundation/dc-apipolyfill package.What changed
internal/verifier/staticembed/dc-api-polyfill.js/dc-api.js: Vendor the DC API browser polyfill for environments without native supportinternal/verifier/staticembed/presentation-definition.html/js: Wire up DC API credential request path in the verifier presentation flowinternal/verifier/apiv1/: Add handler support for DC API credential presentationMakefile: Add target to update the vendored DC API bundleWhy
The W3C Digital Credentials API is now supported in Chrome and is being rolled out across browsers. This change allows the vc verifier to request credentials via the browser-native API instead of QR-code-only flows, enabling a direct tap-to-present UX.
Update (vendored bundle now v0.5.0)
Live end-to-end testing against a real Android device surfaced a real bug: the native DC API path was passing the redirect-flow
authorization_requestURI (openid4vp://...?client_id=...&request_uri=...) directly as the DC APIrequestvalue. That URI is neither a valid JWT (openid4vp-v1-signedneeds one) nor a valid raw-params object (openid4vp-v1-unsignedneeds that instead) - no protocol handler could parse it, so every native DC API attempt failed silently ("Your info wasn't found").Fixed in
@sirosfoundation/dc-apiitself (v0.5.0): a newrequestCredentialFromAuthorizationRequestURI()helper resolves a standard authorization request URI into whateverdatashape the detected protocol actually needs (fetchingrequest_urifor the JWT when required), so any verifier producing a standard authorization request gets a working native DC API path for free.Two more bugs found and fixed in the same pass (both required for the above to actually reach a working state):
supported_walletscan arrive as JSONnull(an unconfigured Go map marshals that way) - the schema had no fallback for it, so the whole/ui/metadataresponse failed to parse and the page hung on its loading spinner before a user could select a credential.meta.vct_values, even formso_mdoccredentials, which have no vct - the correct constraint there ismeta.doctype_value(OpenID4VP 1.0 6.4.1).Testing
Sonarcloud quality gate issues resolved (commit
a323ed65). Vendored bundle updated to v0.5.0 (was v0.2.0), verified end-to-end against a real Android device (Chrome DC API + native mdoc credential presentation) after the above fixes.