This repository has been archived on 2026-11-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
carl/scripts/e2e/NativeVisualIdentity.java

335 lines
14 KiB
Java

/*
* Copyright (C) 2026 KofTwentyTwo
*/
package com.kof22.agentadmin.bootstrap;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.concurrent.TimeUnit;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsServer;
/*******************************************************************************
* Real HTTPS issuer for native-host tests; trusts only its temporary synthetic certificate.
******************************************************************************/
final class NativeVisualIdentity implements AutoCloseable
{
private final HttpsServer server;
private final KeyPair key;
private final SSLSocketFactory previous;
final String issuer;
NativeVisualIdentity(Path temporaryDirectory) throws Exception
{
Path storePath = temporaryDirectory.resolve("synthetic-issuer.p12");
Path log = temporaryDirectory.resolve("keytool.log");
var keytool = new ProcessBuilder(
Path.of(System.getProperty("java.home"), "bin", "keytool").toString(),
"-genkeypair",
"-alias",
"issuer",
"-keyalg",
"RSA",
"-keysize",
"2048",
"-storetype",
"PKCS12",
"-keystore",
storePath.toString(),
"-storepass",
"synthetic-only",
"-dname",
"CN=localhost",
"-ext",
"SAN=dns:localhost,ip:127.0.0.1",
"-validity",
"2",
"-noprompt")
.redirectErrorStream(true)
.redirectOutput(log.toFile())
.start();
if(!keytool.waitFor(15, TimeUnit.SECONDS))
{
keytool.destroyForcibly();
throw new IllegalStateException("Synthetic issuer certificate generation timed out");
}
if(keytool.exitValue() != 0)
{
throw new IllegalStateException("Synthetic issuer certificate generation failed");
}
var store = KeyStore.getInstance("PKCS12");
try(var input = Files.newInputStream(storePath))
{
store.load(input, "synthetic-only".toCharArray());
}
var managers = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
managers.init(store, "synthetic-only".toCharArray());
var certificates = KeyStore.getInstance(KeyStore.getDefaultType());
try(var input = Files.newInputStream(Path.of(System.getProperty("java.home"), "lib", "security", "cacerts")))
{
certificates.load(input, "changeit".toCharArray());
}
certificates.setCertificateEntry("carl-synthetic-issuer", store.getCertificate("issuer"));
Path trustPath = temporaryDirectory.resolve("synthetic-trust.p12");
Files.createFile(trustPath, java.nio.file.attribute.PosixFilePermissions.asFileAttribute(
java.nio.file.attribute.PosixFilePermissions.fromString("rw-------")));
try(var output = Files.newOutputStream(trustPath))
{
certificates.store(output, "synthetic-only".toCharArray());
}
var trust = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trust.init(certificates);
var tls = SSLContext.getInstance("TLS");
tls.init(managers.getKeyManagers(), trust.getTrustManagers(), null);
var generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
key = generator.generateKeyPair();
server = HttpsServer.create(new InetSocketAddress("127.0.0.1", 0), 0);
server.setHttpsConfigurator(new HttpsConfigurator(tls));
issuer = "https://localhost:" + server.getAddress().getPort() + "/";
var publicKey = (RSAPublicKey) key.getPublic();
byte[] jwks = ("{\"keys\":[{\"kty\":\"RSA\",\"kid\":\"native-synthetic\",\"alg\":\"RS256\","
+ "\"use\":\"sig\",\"n\":\""
+ unsigned(publicKey.getModulus().toByteArray())
+ "\",\"e\":\""
+ unsigned(publicKey.getPublicExponent().toByteArray())
+ "\"}]}")
.getBytes(StandardCharsets.UTF_8);
server.createContext(
"/.well-known/jwks.json",
exchange ->
{
exchange.getResponseHeaders().set("Content-Type", "application/json");
exchange.sendResponseHeaders(200, jwks.length);
try(var output = exchange.getResponseBody())
{
output.write(jwks);
}
});
previous = HttpsURLConnection.getDefaultSSLSocketFactory();
HttpsURLConnection.setDefaultSSLSocketFactory(tls.getSocketFactory());
addBrowserEndpoints();
server.start();
}
String token(String subject)
{
return token(subject, Instant.now().plusSeconds(300), "native-admin");
}
String token(String subject, Instant expiry, String audience)
{
return JWT.create()
.withKeyId("native-synthetic")
.withIssuer(issuer)
.withAudience(audience)
.withSubject(subject)
.withExpiresAt(expiry)
.sign(Algorithm.RSA256((RSAPublicKey) key.getPublic(), (RSAPrivateKey) key.getPrivate()));
}
private final java.util.Map<String, java.util.Map<String, String>> codes = new java.util.concurrent.ConcurrentHashMap<>();
private static java.util.Map<String, String> parameters(String text)
{
var values = new java.util.HashMap<String, String>();
if(text != null)
{
for(String part : text.split("&"))
{
var pair = part.split("=", 2);
values.put(java.net.URLDecoder.decode(pair[0], StandardCharsets.UTF_8),
pair.length == 2 ? java.net.URLDecoder.decode(pair[1], StandardCharsets.UTF_8) : "");
}
}
return values;
}
private static void reply(com.sun.net.httpserver.HttpExchange exchange, int code, String type, String body) throws java.io.IOException
{
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", type);
exchange.getResponseHeaders().set("Cache-Control", "no-store");
exchange.sendResponseHeaders(code, bytes.length);
try(var out = exchange.getResponseBody())
{
out.write(bytes);
}
}
private void addBrowserEndpoints()
{
var json = new com.fasterxml.jackson.databind.ObjectMapper();
server.createContext("/authorize", exchange ->
{
var p = parameters(exchange.getRequestURI().getRawQuery());
if(!"POST".equals(exchange.getRequestMethod()) && !"none".equals(p.get("prompt")))
{
reply(exchange, 200, "text/html; charset=UTF-8", "<html><body><h1>Sign in to Carl AI</h1><p>This preview uses fictional household data. Choose a synthetic family member.</p><form method='POST'><button type='submit' name='subject' value='alice'>Sign in as Alice</button><button type='submit' name='subject' value='bob'>Sign in as Bob</button></form></body></html>");
return;
}
String subject = "alice";
String cookie = exchange.getRequestHeaders().getFirst("Cookie");
if(cookie != null && java.util.Arrays.stream(cookie.split(";")).anyMatch(value -> value.trim().equals("syntheticOperator=bob")))
{
subject = "bob";
}
if("POST".equals(exchange.getRequestMethod()))
{
subject = parameters(new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8)).getOrDefault("subject", "alice");
if(!java.util.Set.of("alice", "bob").contains(subject))
{
reply(exchange, 400, "text/plain", "Declared synthetic actor required");
return;
}
exchange.getResponseHeaders().add("Set-Cookie", "syntheticOperator=" + subject + "; Path=/; HttpOnly; Secure; SameSite=Lax");
}
p.put("syntheticSubject", subject);
var code = java.util.UUID.randomUUID().toString();
codes.put(code, p);
var result = java.util.Map.of("code", code, "state", p.getOrDefault("state", ""));
if("web_message".equals(p.get("response_mode")))
{
var redirect = java.net.URI.create(p.get("redirect_uri"));
var origin = redirect.getScheme() + "://" + redirect.getRawAuthority();
reply(exchange, 200, "text/html; charset=UTF-8", "<script>window.parent.postMessage(" + json.writeValueAsString(java.util.Map.of("type", "authorization_response", "response", result)) + "," + json.writeValueAsString(origin) + ");</script>");
}
else
{
exchange.getResponseHeaders().set("Location", p.get("redirect_uri") + "?code=" + code + "&state=" + java.net.URLEncoder.encode(p.getOrDefault("state", ""), StandardCharsets.UTF_8));
exchange.sendResponseHeaders(302, -1);
exchange.close();
}
});
server.createContext("/oauth/token", exchange ->
{
String origin = exchange.getRequestHeaders().getFirst("Origin");
if(origin != null && (origin.startsWith("https://127.0.0.1:") || origin.startsWith("https://localhost:")))
{
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "content-type,auth0-client");
}
if("OPTIONS".equals(exchange.getRequestMethod()))
{
exchange.sendResponseHeaders(204, -1);
exchange.close();
return;
}
String body = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
java.util.Map<String, String> p;
if(body.startsWith("{"))
{
p = json.readValue(body, new com.fasterxml.jackson.core.type.TypeReference<java.util.Map<String, String>>()
{
});
}
else
{
p = parameters(body);
}
var auth = codes.remove(p.get("code"));
if(auth == null)
{
reply(exchange, 400, "application/json", "{\"error\":\"invalid_grant\"}");
return;
}
try
{
String challenge = java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(java.security.MessageDigest.getInstance("SHA-256").digest(p.getOrDefault("code_verifier", "").getBytes(StandardCharsets.UTF_8)));
if(!challenge.equals(auth.get("code_challenge")))
{
reply(exchange, 400, "application/json", "{\"error\":\"invalid_grant\"}");
return;
}
}
catch(java.security.NoSuchAlgorithmException e)
{
throw new IllegalStateException(e);
}
String subject = auth.get("syntheticSubject");
String label = subject.equals("bob") ? "Bob (synthetic)" : "Alice (synthetic)";
var id = JWT.create().withKeyId("native-synthetic").withIssuer(issuer).withAudience("synthetic-client")
.withSubject(subject).withIssuedAt(Instant.now()).withExpiresAt(Instant.now().plusSeconds(1800))
.withClaim("nonce", auth.get("nonce")).withClaim("name", label).withClaim("email", subject + "@example.invalid")
.sign(Algorithm.RSA256((RSAPublicKey) key.getPublic(), (RSAPrivateKey) key.getPrivate()));
reply(exchange, 200, "application/json", json.writeValueAsString(java.util.Map.of("access_token", token(subject, Instant.now().plusSeconds(1800), "native-admin"), "id_token", id, "token_type", "Bearer", "expires_in", 1800, "scope", auth.getOrDefault("scope", "openid profile email"))));
});
server.createContext("/v2/logout", exchange ->
{
var p = parameters(exchange.getRequestURI().getRawQuery());
exchange.getResponseHeaders().add("Set-Cookie", "syntheticOperator=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Lax");
exchange.getResponseHeaders().set("Location", p.getOrDefault("returnTo", "/"));
exchange.sendResponseHeaders(302, -1);
exchange.close();
});
server.createContext("/userinfo", exchange ->
{
String authorization = exchange.getRequestHeaders().getFirst("Authorization");
try
{
if(authorization == null || !authorization.startsWith("Bearer "))
{
throw new IllegalArgumentException("Synthetic bearer required");
}
String subject = JWT.require(Algorithm.RSA256((RSAPublicKey) key.getPublic(), (RSAPrivateKey) key.getPrivate())).withIssuer(issuer).withAudience("native-admin").build().verify(authorization.substring(7)).getSubject();
if(!java.util.Set.of("alice", "bob").contains(subject))
{
throw new IllegalArgumentException("Declared synthetic actor required");
}
reply(exchange, 200, "application/json", json.writeValueAsString(java.util.Map.of("sub", subject, "name", subject.equals("bob") ? "Bob (synthetic)" : "Alice (synthetic)")));
}
catch(RuntimeException invalid)
{
reply(exchange, 401, "application/json", "{\"error\":\"invalid_token\"}");
}
});
}
private static String unsigned(byte[] bytes)
{
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(Arrays.copyOfRange(bytes, bytes[0] == 0 ? 1 : 0, bytes.length));
}
@Override
public void close()
{
server.stop(0);
HttpsURLConnection.setDefaultSSLSocketFactory(previous);
}
}