[OID4VCI]: Harden JSON-LD context loading for LD-VC signing - #51448
[OID4VCI]: Harden JSON-LD context loading for LD-VC signing#51448Awambeng wants to merge 3 commits into
Conversation
- 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>
There was a problem hiding this comment.
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>
There was a problem hiding this comment.
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
Linkheader,Map.getreturnsnull, but Titanium's response API expects an empty collection and may iterate it while processing non-application/ld+jsonresponses.allValuesis 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>
There was a problem hiding this comment.
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.loadDocumentthrows (including the new timeout and size-limit failures),locks.removeis 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")returnsnullwhen the response has no Link header, but the TitaniumHttpResponsecontract exposes a collection that its loader can consume. ReturnallValues("link")so absent headers produce an empty collection and header-name matching is handled byHttpHeaders.
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.stopdoes not own or terminate a caller-supplied executor, andnewFixedThreadPoolcreates non-daemon threads. Retain theExecutorServiceand callshutdownNow()instopServer, especially to interrupt the 30-second handlers.
// A dedicated pool so the blocking /slow handler cannot starve other tests.
server.setExecutor(Executors.newFixedThreadPool(4));
This PR hardens JSON-LD context resolution during LD-VC signing by replacing the default uncached
HttpLoaderused byEd255192018Suitewith a controlled context document loader.Previously, credential issuance fetched remote
@contextdocuments 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:
JsonLdContextDocumentLoaderwith:httpson every redirect target to prevent scheme downgrades during context resolution.HttpLoaderusage inEd255192018Suite.Closes #50523