Archived
Implement the standalone Carl application, native administration, family API, financial workflows, protected reports, public fictional fixtures and consistent monetary presentation. Fresh full verification passes 419 tests; running package payloads match. Record the owner-confirmed CI/CD and GitOps direction for the next implementation.
335 lines
14 KiB
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);
|
|
}
|
|
}
|