Files
document-haness/docs/clean-architecture-backend-template/final/evidence/rendered/a13-f009-sigv4-string-heap.svg
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

183 lines
13 KiB
XML

<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1602" viewBox="0 0 1200 1602" role="img">
<title>terminal evidence</title>
<desc>Terminal-style rendering generated from retained command output. Sensitive-looking values are redacted in the visual asset.</desc>
<rect x="1" y="1" width="1198" height="1600" rx="14" fill="#0d1117" stroke="#30363d"/>
<rect x="1" y="1" width="1198" height="44" rx="14" fill="#161b22"/>
<rect x="1" y="30" width="1198" height="14" fill="#161b22"/>
<circle cx="24" cy="22" r="6" fill="#ff5f57"/>
<circle cx="44" cy="22" r="6" fill="#febc2e"/>
<circle cx="64" cy="22" r="6" fill="#28c840"/>
<text x="92" y="27" fill="#8b949e" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">terminal evidence</text>
<text x="24" y="68" fill="#c9d1d9" font-size="15" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">$ set -e
set -o pipefail
D=$(mktemp -d); trap &#x27;rm -rf &quot;$D&quot;&#x27; EXIT
C=/shared/codebase/clean-architecture-backend-template
</tspan><tspan x="24" dy="22">N=$C/src/adapter/outbound/notification
CACHE=/root/.gradle/caches/modules-2/files-2.1
java -version 2&gt;&amp;1 | head -1
cp=&quot;&quot;</tspan><tspan x="24" dy="22">
while IFS= read -r line; do
coord=${line%%=*}; confs=${line#*=}
case &quot;,$confs,&quot; in *&quot;,runtimeClasspath,&quot;*) ;; *) co</tspan><tspan x="24" dy="22">ntinue ;; esac
g=${coord%%:*}; rest=${coord#*:}; n=${rest%%:*}; v=${rest##*:}
jar=$(find &quot;$CACHE/$g/$n/$v&quot; -name &#x27;*.</tspan><tspan x="24" dy="22">jar&#x27; ! -name &#x27;*sources*&#x27; ! -name &#x27;*javadoc*&#x27; 2&gt;/dev/null | head -1)
[ -n &quot;$jar&quot; ] &amp;&amp; cp=&quot;$cp:$jar&quot;
done &lt; &lt;(grep -E &#x27;^</tspan><tspan x="24" dy="22">[a-zA-Z0-9._-]+:[^=]+=&#x27; &quot;$N/gradle.lockfile&quot;)
CP=$(find &quot;$N/build/libs&quot; &quot;$C/src/application-core/build/libs&quot; -name &#x27;*+21</tspan><tspan x="24" dy="22">234e38cdb9.jar&#x27; | tr &#x27;\n&#x27; &#x27;:&#x27;)${cp#:}
cat &gt; &quot;$D/SecretResidueProbe.java&quot; &lt;&lt;&#x27;JAVA&#x27;
import com.sun.management.HotSpotDiagn</tspan><tspan x="24" dy="22">osticMXBean;
import dev.caskeleton.adapter.outbound.notification.platform.provider.ses.AwsSignatureV4Signer;
import dev.</tspan><tspan x="24" dy="22">caskeleton.application.notification.platform.security.SecretKeyMaterial;
import dev.caskeleton.application.notification.</tspan><tspan x="24" dy="22">platform.security.SecretPurpose;
import java.lang.management.ManagementFactory;
import java.nio.charset.StandardCharsets</tspan><tspan x="24" dy="22">;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.util.Arrays;
import java.</tspan><tspan x="24" dy="22">util.Map;
/** 제공자 자격증명이 서명 경로를 지난 뒤 힙에 몇 벌 남는지 잰다. */
public final class SecretResidueProbe {
</tspan><tspan x="24" dy="22">
/**
* 이 프로브가 만든 합성 비밀. 아스키 코드로 조립한다.
*
* &lt;p&gt;문자열 리터럴로 적으면 그 리터럴이 상수 풀</tspan><tspan x="24" dy="22">에 남아 측정이 오염된다. 재는 것은 어떤
* 사본이 남느냐이므로, 프로브 자신은 비밀을 문자열로 만들지 않는다.
*/
p</tspan><tspan x="24" dy="22">rivate static byte[] secretBytes() {
int[] codes = {90, 90, 80, 82, 79, 66, 69, 79, 78, 76, 89, 83, 69, 67, 82, 69, </tspan><tspan x="24" dy="22">84,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102, 103};
byte[] bytes = new b</tspan><tspan x="24" dy="22">yte[codes.length];
for (int at = 0; at &lt; codes.length; at++) {
bytes[at] = (byte) codes[at];
}
return </tspan><tspan x="24" dy="22">bytes;
}
private static int occurrences(Path dump, byte[] needle) throws Exception {
byte[] data = Files.readAl</tspan><tspan x="24" dy="22">lBytes(dump);
int found = 0;
outer:
for (int at = 0; at + needle.length &lt;= data.length; at++) {
for (i</tspan><tspan x="24" dy="22">nt offset = 0; offset &lt; needle.length; offset++) {
if (data[at + offset] != needle[offset]) {
continue</tspan><tspan x="24" dy="22"> outer;
}
}
found++;
}
return found;
}
/** 덤프만 뜬다. 찾을 바이트열은 덤프를 뜬 뒤에 </tspan><tspan x="24" dy="22">만들어야 그 배열 자신이 세어지지 않는다. */
private static Path dump(Path directory, String name, boolean liveOnly) th</tspan><tspan x="24" dy="22">rows Exception {
HotSpotDiagnosticMXBean diagnostics =
ManagementFactory.getPlatformMXBean(HotSpotDiagnostic</tspan><tspan x="24" dy="22">MXBean.class);
Path file = directory.resolve(name);
Files.deleteIfExists(file);
diagnostics.dumpHeap(file.to</tspan><tspan x="24" dy="22">String(), liveOnly);
return file;
}
private static byte[] hmac(byte[] key, String data) throws Exception {
</tspan><tspan x="24" dy="22">javax.crypto.Mac mac = javax.crypto.Mac.getInstance(&quot;HmacSHA256&quot;);
mac.init(new javax.crypto.spec.SecretKeySpec(key,</tspan><tspan x="24" dy="22"> &quot;HmacSHA256&quot;));
return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
}
/**
* 원문이 제안하는 수정. 문</tspan><tspan x="24" dy="22">자열을 거치지 않고 바이트끼리 잇되, 그 뒤 HMAC 네 바퀴는
* 서명기와 똑같이 돈다. 파생 자체를 빼면 수정의 값을 잴 수 </tspan><tspan x="24" dy="22">없다.
*
* @param wipeJoined 이은 배열까지 지울지. 제안된 수정에는 이 단계가 없다. 그것을
* 더하면 무엇이 더</tspan><tspan x="24" dy="22"> 사라지는지 따로 재려고 인자로 뺐다.
*/
private static byte[] derivedWithoutString(byte[] secret, boolean wipeJoine</tspan><tspan x="24" dy="22">d) throws Exception {
byte[] prefix = &quot;AWS4&quot;.getBytes(StandardCharsets.US_ASCII);
byte[] joined = new byte[prefi</tspan><tspan x="24" dy="22">x.length + secret.length];
System.arraycopy(prefix, 0, joined, 0, prefix.length);
System.arraycopy(secret, 0, jo</tspan><tspan x="24" dy="22">ined, prefix.length, secret.length);
byte[] dateKey = hmac(joined, &quot;20260903&quot;);
byte[] regionKey = hmac(dateKey,</tspan><tspan x="24" dy="22"> &quot;eu-central-1&quot;);
byte[] serviceKey = hmac(regionKey, &quot;ses&quot;);
byte[] signingKey = hmac(serviceKey, &quot;aws4_request</tspan><tspan x="24" dy="22">&quot;);
if (wipeJoined) {
Arrays.fill(joined, (byte) 0);
}
return signingKey;
}
public static void ma</tspan><tspan x="24" dy="22">in(String[] args) throws Exception {
Path directory = Path.of(args[0]);
String mode = args[1];
// 실제 경로</tspan><tspan x="24" dy="22">가 주는 것과 같은 사본이다. 레코드가 하나를 보관하고 접근자가 한 벌 더 준다.
SecretKeyMaterial stored =
new </tspan><tspan x="24" dy="22">SecretKeyMaterial(&quot;probe-key&quot;, SecretPurpose.PROVIDER_CREDENTIAL, secretBytes());
byte[] handed = stored.material();</tspan><tspan x="24" dy="22">
String outcome;
switch (mode) {
case &quot;sign&quot; -&gt;
outcome =
new AwsSignatureV4Signe</tspan><tspan x="24" dy="22">r()
.sign(&quot;POST&quot;, &quot;/v2/email/outbound-emails&quot;, &quot;&quot;,
Map.of(&quot;host&quot;, &quot;email.eu-cent</tspan><tspan x="24" dy="22">ral-1.amazonaws.com&quot;,
&quot;content-type&quot;, &quot;application/json&quot;),
&quot;{}&quot;.getBytes</tspan><tspan x="24" dy="22">(StandardCharsets.UTF_8),
&quot;AKIAPROBEONLY&quot;, handed, &quot;eu-central-1&quot;, &quot;ses&quot;,
In</tspan><tspan x="24" dy="22">stant.parse(&quot;2026-09-03T00:00:00Z&quot;))
.authorization()
.substring(0, 16);
case </tspan><tspan x="24" dy="22">&quot;bytes&quot; -&gt; outcome = &quot;파생 키 &quot; + derivedWithoutString(handed, false).length + &quot;바이트&quot;;
case &quot;byteswipe&quot; -&gt;
</tspan><tspan x="24" dy="22"> outcome = &quot;파생 키 &quot; + derivedWithoutString(handed, true).length + &quot;바이트&quot;;
default -&gt; outcome = &quot;받은 길이 </tspan><tspan x="24" dy="22">&quot; + handed.length;
}
// 호출자가 할 수 있는 일은 여기까지다. 받은 사본은 지운다.
Arrays.fill(handed, (byte) </tspan><tspan x="24" dy="22">0);
Path all = dump(directory, &quot;all.hprof&quot;, false);
Path live = dump(directory, &quot;live.hprof&quot;, true);
byte[]</tspan><tspan x="24" dy="22"> needle = secretBytes();
String label =
switch (mode) {
case &quot;sign&quot; -&gt; &quot;서명 키 파생을 지난 뒤&quot;;
</tspan><tspan x="24" dy="22"> case &quot;bytes&quot; -&gt; &quot;바이트로만 이은 뒤&quot;;
case &quot;byteswipe&quot; -&gt; &quot;이은 배열까지 지운 뒤&quot;;
default </tspan><tspan x="24" dy="22">-&gt; &quot;받아서 지우기만 한 뒤&quot;;
};
System.out.printf(&quot; %-26s 모든 객체 %-4d 살아 있는 객체만 %-4d (%s)%n&quot;,
</tspan><tspan x="24" dy="22"> label, occurrences(all, needle), occurrences(live, needle), outcome);
Files.deleteIfExists(all);
Files.delet</tspan><tspan x="24" dy="22">eIfExists(live);
// 레코드를 여기까지 붙들어 둔다. 네 모드의 기준선을 같게 두려는 것이다.
if (stored.keyId().isE</tspan><tspan x="24" dy="22">mpty()) {
throw new IllegalStateException(&quot;unreachable&quot;);
}
}
}
JAVA
javac -encoding UTF-8 -cp &quot;$CP&quot; -d &quot;$D&quot;</tspan><tspan x="24" dy="22"> &quot;$D/SecretResidueProbe.java&quot;
echo
echo &quot;[제공자 자격증명 사본이 힙 덤프에서 몇 번 나오는가]&quot;
# 네 모드를 각각 새 JVM 에</tspan><tspan x="24" dy="22">서 돌린다. 한 JVM 에서 이어 돌리면 앞 모드가 남긴 것이 뒤 측정에 섞인다.
java -Dstdout.encoding=UTF-8 -Xmx256m -cp &quot;$CP:</tspan><tspan x="24" dy="22">$D&quot; SecretResidueProbe &quot;$D&quot; read
java -Dstdout.encoding=UTF-8 -Xmx256m -cp &quot;$CP:$D&quot; SecretResidueProbe &quot;$D&quot; bytes
java -</tspan><tspan x="24" dy="22">Dstdout.encoding=UTF-8 -Xmx256m -cp &quot;$CP:$D&quot; SecretResidueProbe &quot;$D&quot; byteswipe
java -Dstdout.encoding=UTF-8 -Xmx256m -cp</tspan><tspan x="24" dy="22"> &quot;$CP:$D&quot; SecretResidueProbe &quot;$D&quot; sign
</tspan></text>
<text x="24" y="1344" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">cwd: /shared/codebase/clean-architecture-backend-template/src</tspan></text>
<text x="24" y="1366" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">time: 2026-09-02T19:12:29+00:00 · exit 0</tspan></text>
<line x1="24" y1="1380" x2="1176" y2="1380" stroke="#30363d"/>
<text x="24" y="1420" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">openjdk version &quot;21.0.12&quot; 2026-07-21</tspan></text>
<text x="24" y="1442" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
<text x="24" y="1464" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[제공자 자격증명 사본이 힙 덤프에서 몇 번 나오는가]</tspan></text>
<text x="24" y="1486" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 받아서 지우기만 한 뒤 모든 객체 2 살아 있는 객체만 1 (받은 길이 34)</tspan></text>
<text x="24" y="1508" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 바이트로만 이은 뒤 모든 객체 4 살아 있는 객체만 1 (파생 키 32바이트)</tspan></text>
<text x="24" y="1530" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 이은 배열까지 지운 뒤 모든 객체 3 살아 있는 객체만 1 (파생 키 32바이트)</tspan></text>
<text x="24" y="1552" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 서명 키 파생을 지난 뒤 모든 객체 6 살아 있는 객체만 1 (AWS4-HMAC-SHA256)</tspan></text>
</svg>