Skip to content

[OID4VCI]: Harden JSON-LD context loading for LD-VC signing - #51448

Open
Awambeng wants to merge 3 commits into
keycloak:mainfrom
adorsys:issue-50523
Open

[OID4VCI]: Harden JSON-LD context loading for LD-VC signing#51448
Awambeng wants to merge 3 commits into
keycloak:mainfrom
adorsys:issue-50523

Conversation

@Awambeng

@Awambeng Awambeng commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

This PR hardens JSON-LD context resolution during LD-VC signing by replacing the default uncached HttpLoader used by Ed255192018Suite with a controlled context document loader.

Previously, credential issuance fetched remote @context documents on every issuance without restrictions or timeouts, making signing dependent on external context availability and allowing remote context changes to affect the canonicalized signing payload.

Key changes:

  • Add JsonLdContextDocumentLoader with:
    • HTTPS-only context resolution.
    • Configurable host allowlist.
    • Bounded in-memory context cache.
    • Connection and request timeouts.
  • Enforce https on every redirect target to prevent scheme downgrades during context resolution.
  • Replace direct HttpLoader usage in Ed255192018Suite.
  • Add tests covering:
    • Allowlist validation.
    • Context caching.
    • Redirect handling.
    • Timeout behaviour.
    • Signing flow with deterministic context resolution.

Closes #50523

- Ed255192018Suite previously dereferenced credential @context URLs during every issuance using an uncached HttpLoader(DefaultHttpClient.defaultInstance()).
This made credential issuance depend on remote context availability and allowed changes in remote context documents to affect the canonicalized
signing payload.

- Introduce a hardened JSON-LD context loader with HTTPS-only resolution, configurable host allowlisting, bounded in-memory caching, and HTTP timeouts.

- Use the hardened loader in Ed255192018Suite to prevent repeated remote context fetching during credential issuance.

Closes keycloak#50523

Signed-off-by: Awambeng Rodrick <awambengrodrick@gmail.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Hardens JSON-LD context resolution used during LD-VC signing.

Changes:

  • Adds HTTPS enforcement, host allowlisting, caching, and timeouts.
  • Integrates the controlled loader into Ed255192018Suite.
  • Adds loader and signing-flow tests.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
JsonLdContextDocumentLoader.java Implements context validation, loading, and caching.
JdkHttpClient.java Provides timeout-aware HTTP transport.
Ed255192018Suite.java Uses the hardened document loader.
JsonLdContextDocumentLoaderTest.java Tests loading policies and caching.
Ed255192018SuiteTest.java Tests signing integration.
Suppressed comments (1)

services/src/main/java/org/keycloak/protocol/oid4vc/issuance/signing/vcdm/JdkHttpClient.java:76

  • This no-op violates the response close contract and leaves unread bodies open on redirects, HTTP errors, or parse failures, preventing connection reuse and leaking resources. Close the JDK response body stream here.
        void close() {
            // The body stream is consumed and closed by the document reader.
        }

Signed-off-by: Awambeng Rodrick <awambengrodrick@gmail.com>
Copilot AI review requested due to automatic review settings August 5, 2026 08:36

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

services/src/main/java/org/keycloak/protocol/oid4vc/issuance/signing/vcdm/JsonLdContextDocumentLoader.java:117

  • Concurrent failures are not coalesced: if the first fetch times out or throws, each waiter enters this monitor in turn and performs another full-timeout request, so the last issuance can wait numberOfWaiters × requestTimeout. Represent the in-flight load with a shared future/result (including its exception) so all callers in the same burst complete after the first attempt.
        Object lock = locks.computeIfAbsent(url, u -> new Object());
        synchronized (lock) {
            cached = getCached(url);
            if (cached == null) {
                cached = delegate.loadDocument(url, options);

services/src/main/java/org/keycloak/protocol/oid4vc/issuance/signing/vcdm/JdkHttpClient.java:89

  • When the response has no Link header, Map.get returns null, but Titanium's response API expects an empty collection and may iterate it while processing non-application/ld+json responses. allValues is also case-insensitive, so use it rather than looking up a raw map key.
        Collection<String> links() {
            return delegate.headers().map().get("link");
        }

Signed-off-by: Awambeng Rodrick <awambengrodrick@gmail.com>
Copilot AI review requested due to automatic review settings August 5, 2026 08:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (4)

services/src/main/java/org/keycloak/protocol/oid4vc/issuance/signing/vcdm/JsonLdContextDocumentLoader.java:120

  • If delegate.loadDocument throws (including the new timeout and size-limit failures), locks.remove is skipped, so every distinct failed context URI remains in this unbounded map for the server lifetime. Use exception-safe single-flight state (for example, a future per URI) and remove completed or failed entries without allowing concurrent retries to bypass coalescing.
                cached = delegate.loadDocument(url, options);
                putCached(url, cached);
            }
            locks.remove(url, lock);

services/src/main/java/org/keycloak/protocol/oid4vc/issuance/signing/vcdm/JdkHttpClient.java:140

  • Map.get("link") returns null when the response has no Link header, but the Titanium HttpResponse contract exposes a collection that its loader can consume. Return allValues("link") so absent headers produce an empty collection and header-name matching is handled by HttpHeaders.
        Collection<String> links() {
            return delegate.headers().map().get("link");
        }

services/src/test/java/org/keycloak/protocol/oid4vc/issuance/signing/vcdm/Ed255192018SuiteTest.java:54

  • This caller-supplied fixed thread pool is never shut down by HttpServer.stop, leaving non-daemon worker threads after the test class completes. This server does not need concurrent handlers, so use the server-managed default executor as the surrounding tests do.
        server.setExecutor(Executors.newFixedThreadPool(2));

services/src/test/java/org/keycloak/protocol/oid4vc/issuance/signing/vcdm/JsonLdContextDocumentLoaderTest.java:98

  • This fixed thread pool is never shut down: HttpServer.stop does not own or terminate a caller-supplied executor, and newFixedThreadPool creates non-daemon threads. Retain the ExecutorService and call shutdownNow() in stopServer, especially to interrupt the 30-second handlers.
        // A dedicated pool so the blocking /slow handler cannot starve other tests.
        server.setExecutor(Executors.newFixedThreadPool(4));

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OID4VCI: LD-VC signer fetches remote @context URLs over HTTP with no allowlist or cache

2 participants