Skip to content
Merged
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.nimbusds.jose.jwk.ECKey;
import com.nimbusds.jose.jwk.JWK;
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
import com.nimbusds.jose.util.JSONObjectUtils;
import com.nimbusds.jwt.SignedJWT;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer;
import foundation.aep.agent.AepAgent;
Expand All @@ -16,6 +18,7 @@
import foundation.aep.core.ClaimValues;
import foundation.aep.core.ClientAssertionClaims;
import foundation.aep.core.ClientAssertions;
import foundation.aep.core.DidWeb;
import foundation.aep.core.GrantResponses;
import foundation.aep.core.InspectDocument;
import foundation.aep.core.ManagedAgentStatus;
Expand All @@ -41,6 +44,7 @@
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URLDecoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
Expand All @@ -52,16 +56,20 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.Executors;

public final class NodeInteroperability {
private static final String API_KEY_HEADER = "X-API-Key";
private static final String COMMAND_AGENT = "agent";
private static final String COMMAND_SERVER = "server";
private static final String HTTP_GET = "GET";
private static final String HTTP_POST = "POST";
private static final int HTTP_OK = 200;
private static final String PLATFORM_AUTHORIZATION = "Bearer demo-agent";
private static final String PLATFORM_IDENTITIES = "/platform/agent-identities";
private static final int MAXIMUM_BODY_BYTES = 65_536;
private static final int SERVER_THREADS = 4;
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();

private NodeInteroperability() {}

Expand Down Expand Up @@ -136,8 +144,9 @@ private static void runServer(String... arguments) throws IOException, Interrupt
String serviceDid = "did:web:" + encodedHost + ":services:store";
InteroperabilityKeyStore keys = new InteroperabilityKeyStore();
AepPlatform platform = platform(listen, serviceDid, keys);
AepServiceHttpHandler service = service(serviceDid, keys);
AepServiceHttpHandler service = service(serviceDid);
HttpServer server = HttpServer.create(new InetSocketAddress(host, port), 0);
server.setExecutor(Executors.newFixedThreadPool(SERVER_THREADS));
AepHttpServer.register(server, service);
URI origin = URI.create("http://" + listen);
server.createContext(
Expand Down Expand Up @@ -165,7 +174,7 @@ private static void runServer(String... arguments) throws IOException, Interrupt
new java.util.concurrent.CountDownLatch(1).await();
}

private static AepServiceHttpHandler service(String serviceDid, InteroperabilityKeyStore keys) {
private static AepServiceHttpHandler service(String serviceDid) {
InspectDocument.GrantTypeConfig apiKeyConfig = new InspectDocument.GrantTypeConfig(
null,
"3600",
Expand Down Expand Up @@ -202,13 +211,64 @@ private static AepServiceHttpHandler service(String serviceDid, Interoperability
AepService protocol = AepService.builder(
document,
ClientAssertionVerifier.withKeyResolver(
(assertion, claims, context) -> keys.publicKey(claims.issuer())))
(assertion, claims, context) -> resolveAgentKey(assertion, claims.issuer())))
.allowInsecureLoopback(true)
.storedCredentialGrantType(apiKey)
.build();
return new AepServiceHttpHandler(protocol);
}

private static CompletionStage<JWK> resolveAgentKey(String assertion, String agentDid) {
URI documentUri;
try {
documentUri = DidWeb.documentUri(agentDid, true);
} catch (IllegalArgumentException exception) {
return CompletableFuture.failedFuture(exception);
}
HttpRequest request = HttpRequest.newBuilder(documentUri)
.header("Accept", Aep.DID_MEDIA_TYPE)
.GET()
.build();
return HTTP_CLIENT
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenApply(response -> {
if (response.statusCode() != HTTP_OK) {
throw new IllegalArgumentException(
"Agent DID document returned HTTP " + response.statusCode() + ".");
}
try {
Map<String, Object> document = JSONObjectUtils.parse(response.body());
if (!agentDid.equals(document.get("id"))) {
throw new IllegalArgumentException("Agent DID document identifier does not match.");
}
String keyId = SignedJWT.parse(assertion).getHeader().getKeyID();
if (keyId == null || keyId.isBlank()) {
throw new IllegalArgumentException(
"Agent assertion does not identify a verification method.");
}
int fragment = keyId.indexOf('#');
String keyDid = fragment < 0 ? keyId : keyId.substring(0, fragment);
if (!agentDid.equals(keyDid)) {
throw new IllegalArgumentException(
"Agent verification method does not identify the assertion issuer.");
}
Map<String, Object>[] methods =
JSONObjectUtils.getJSONObjectArray(document, "verificationMethod");
if (methods == null) {
throw new IllegalArgumentException("Agent DID document has no verification methods.");
}
for (Map<String, Object> method : methods) {
if (keyId.equals(method.get("id"))) {
return JWK.parse(JSONObjectUtils.getJSONObject(method, "publicKeyJwk"));
}
}
throw new IllegalArgumentException("Agent verification method was not found.");
} catch (java.text.ParseException exception) {
throw new IllegalArgumentException("Agent DID document is invalid.", exception);
}
});
}

private static AepPlatform platform(String host, String serviceDid, InteroperabilityKeyStore keys) {
PlatformDiscoveryDocument discovery = new PlatformDiscoveryDocument(
Aep.VERSION,
Expand Down