Files
document-haness/docs/clean-architecture-backend-template/final/evidence/meta/a13-f009-sigv4-string-heap.json
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

15 lines
8.0 KiB
JSON

{
"assetKey": "a13-f009-sigv4-string-heap",
"kind": "terminal",
"command": "set -e\nset -o pipefail\nD=$(mktemp -d); trap 'rm -rf \"$D\"' EXIT\nC=/shared/codebase/clean-architecture-backend-template\nN=$C/src/adapter/outbound/notification\nCACHE=/root/.gradle/caches/modules-2/files-2.1\njava -version 2>&1 | head -1\ncp=\"\"\nwhile IFS= read -r line; do\n coord=${line%%=*}; confs=${line#*=}\n case \",$confs,\" in *\",runtimeClasspath,\"*) ;; *) continue ;; esac\n g=${coord%%:*}; rest=${coord#*:}; n=${rest%%:*}; v=${rest##*:}\n jar=$(find \"$CACHE/$g/$n/$v\" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)\n [ -n \"$jar\" ] && cp=\"$cp:$jar\"\ndone < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' \"$N/gradle.lockfile\")\nCP=$(find \"$N/build/libs\" \"$C/src/application-core/build/libs\" -name '*+21234e38cdb9.jar' | tr '\\n' ':')${cp#:}\ncat > \"$D/SecretResidueProbe.java\" <<'JAVA'\nimport com.sun.management.HotSpotDiagnosticMXBean;\nimport dev.caskeleton.adapter.outbound.notification.platform.provider.ses.AwsSignatureV4Signer;\nimport dev.caskeleton.application.notification.platform.security.SecretKeyMaterial;\nimport dev.caskeleton.application.notification.platform.security.SecretPurpose;\nimport java.lang.management.ManagementFactory;\nimport java.nio.charset.StandardCharsets;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.time.Instant;\nimport java.util.Arrays;\nimport java.util.Map;\n\n/** 제공자 자격증명이 서명 경로를 지난 뒤 힙에 몇 벌 남는지 잰다. */\npublic final class SecretResidueProbe {\n\n /**\n * 이 프로브가 만든 합성 비밀. 아스키 코드로 조립한다.\n *\n * <p>문자열 리터럴로 적으면 그 리터럴이 상수 풀에 남아 측정이 오염된다. 재는 것은 어떤\n * 사본이 남느냐이므로, 프로브 자신은 비밀을 문자열로 만들지 않는다.\n */\n private static byte[] secretBytes() {\n int[] codes = {90, 90, 80, 82, 79, 66, 69, 79, 78, 76, 89, 83, 69, 67, 82, 69, 84,\n 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, 103};\n byte[] bytes = new byte[codes.length];\n for (int at = 0; at < codes.length; at++) {\n bytes[at] = (byte) codes[at];\n }\n return bytes;\n }\n\n private static int occurrences(Path dump, byte[] needle) throws Exception {\n byte[] data = Files.readAllBytes(dump);\n int found = 0;\n outer:\n for (int at = 0; at + needle.length <= data.length; at++) {\n for (int offset = 0; offset < needle.length; offset++) {\n if (data[at + offset] != needle[offset]) {\n continue outer;\n }\n }\n found++;\n }\n return found;\n }\n\n /** 덤프만 뜬다. 찾을 바이트열은 덤프를 뜬 뒤에 만들어야 그 배열 자신이 세어지지 않는다. */\n private static Path dump(Path directory, String name, boolean liveOnly) throws Exception {\n HotSpotDiagnosticMXBean diagnostics =\n ManagementFactory.getPlatformMXBean(HotSpotDiagnosticMXBean.class);\n Path file = directory.resolve(name);\n Files.deleteIfExists(file);\n diagnostics.dumpHeap(file.toString(), liveOnly);\n return file;\n }\n\n private static byte[] hmac(byte[] key, String data) throws Exception {\n javax.crypto.Mac mac = javax.crypto.Mac.getInstance(\"HmacSHA256\");\n mac.init(new javax.crypto.spec.SecretKeySpec(key, \"HmacSHA256\"));\n return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));\n }\n\n /**\n * 원문이 제안하는 수정. 문자열을 거치지 않고 바이트끼리 잇되, 그 뒤 HMAC 네 바퀴는\n * 서명기와 똑같이 돈다. 파생 자체를 빼면 수정의 값을 잴 수 없다.\n *\n * @param wipeJoined 이은 배열까지 지울지. 제안된 수정에는 이 단계가 없다. 그것을\n * 더하면 무엇이 더 사라지는지 따로 재려고 인자로 뺐다.\n */\n private static byte[] derivedWithoutString(byte[] secret, boolean wipeJoined) throws Exception {\n byte[] prefix = \"AWS4\".getBytes(StandardCharsets.US_ASCII);\n byte[] joined = new byte[prefix.length + secret.length];\n System.arraycopy(prefix, 0, joined, 0, prefix.length);\n System.arraycopy(secret, 0, joined, prefix.length, secret.length);\n byte[] dateKey = hmac(joined, \"20260903\");\n byte[] regionKey = hmac(dateKey, \"eu-central-1\");\n byte[] serviceKey = hmac(regionKey, \"ses\");\n byte[] signingKey = hmac(serviceKey, \"aws4_request\");\n if (wipeJoined) {\n Arrays.fill(joined, (byte) 0);\n }\n return signingKey;\n }\n\n public static void main(String[] args) throws Exception {\n Path directory = Path.of(args[0]);\n String mode = args[1];\n\n // 실제 경로가 주는 것과 같은 사본이다. 레코드가 하나를 보관하고 접근자가 한 벌 더 준다.\n SecretKeyMaterial stored =\n new SecretKeyMaterial(\"probe-key\", SecretPurpose.PROVIDER_CREDENTIAL, secretBytes());\n byte[] handed = stored.material();\n\n String outcome;\n switch (mode) {\n case \"sign\" ->\n outcome =\n new AwsSignatureV4Signer()\n .sign(\"POST\", \"/v2/email/outbound-emails\", \"\",\n Map.of(\"host\", \"email.eu-central-1.amazonaws.com\",\n \"content-type\", \"application/json\"),\n \"{}\".getBytes(StandardCharsets.UTF_8),\n \"AKIAPROBEONLY\", handed, \"eu-central-1\", \"ses\",\n Instant.parse(\"2026-09-03T00:00:00Z\"))\n .authorization()\n .substring(0, 16);\n case \"bytes\" -> outcome = \"파생 키 \" + derivedWithoutString(handed, false).length + \"바이트\";\n case \"byteswipe\" ->\n outcome = \"파생 키 \" + derivedWithoutString(handed, true).length + \"바이트\";\n default -> outcome = \"받은 길이 \" + handed.length;\n }\n // 호출자가 할 수 있는 일은 여기까지다. 받은 사본은 지운다.\n Arrays.fill(handed, (byte) 0);\n\n Path all = dump(directory, \"all.hprof\", false);\n Path live = dump(directory, \"live.hprof\", true);\n byte[] needle = secretBytes();\n String label =\n switch (mode) {\n case \"sign\" -> \"서명 키 파생을 지난 뒤\";\n case \"bytes\" -> \"바이트로만 이은 뒤\";\n case \"byteswipe\" -> \"이은 배열까지 지운 뒤\";\n default -> \"받아서 지우기만 한 뒤\";\n };\n System.out.printf(\" %-26s 모든 객체 %-4d 살아 있는 객체만 %-4d (%s)%n\",\n label, occurrences(all, needle), occurrences(live, needle), outcome);\n Files.deleteIfExists(all);\n Files.deleteIfExists(live);\n // 레코드를 여기까지 붙들어 둔다. 네 모드의 기준선을 같게 두려는 것이다.\n if (stored.keyId().isEmpty()) {\n throw new IllegalStateException(\"unreachable\");\n }\n }\n}\nJAVA\njavac -encoding UTF-8 -cp \"$CP\" -d \"$D\" \"$D/SecretResidueProbe.java\"\necho\necho \"[제공자 자격증명 사본이 힙 덤프에서 몇 번 나오는가]\"\n# 네 모드를 각각 새 JVM 에서 돌린다. 한 JVM 에서 이어 돌리면 앞 모드가 남긴 것이 뒤 측정에 섞인다.\njava -Dstdout.encoding=UTF-8 -Xmx256m -cp \"$CP:$D\" SecretResidueProbe \"$D\" read\njava -Dstdout.encoding=UTF-8 -Xmx256m -cp \"$CP:$D\" SecretResidueProbe \"$D\" bytes\njava -Dstdout.encoding=UTF-8 -Xmx256m -cp \"$CP:$D\" SecretResidueProbe \"$D\" byteswipe\njava -Dstdout.encoding=UTF-8 -Xmx256m -cp \"$CP:$D\" SecretResidueProbe \"$D\" sign\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T19:12:29+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a13-f009-sigv4-string-heap.txt",
"svg": "evidence/rendered/a13-f009-sigv4-string-heap.svg",
"rawSha256": "aceaa8889256d7f56708e04e0a9f07070cd1466cfaa580edf9bb0c8e08e3a536",
"lines": 7,
"redaction": "none — 비밀 표식은 이 프로브가 아스키 코드로 조립한 합성 값이다"
}