ci(ocr): verify the baked trust file, not unset image env vars (#666)

Fourth latent bug in the OCR publish chain (#649/#519 added it, never ran).
verify-ocr's trust-identity step read OCR_RUNTIME_INDEX_KEY_ID from the image env,
which the official image leaves unset by design (operator-override path;
runtime-index.ts reads the baked file when env is unset). It compared "" to
snapotter-ocr-2026-07 and failed a correct image. Now verifies the baked
/app/docker/ocr-runtime-trust.json, proven against the built release image.

A full audit of the remaining chain (verify, sign, verify-signed, publish)
reproduced the sign+verify path end-to-end inside the image and confirmed nothing
else breaks for v2.2.0. One latent non-ASCII canonicalization mismatch filed as
#667 (cannot fire on v2.2.0). Non-releasable type so the re-dispatch re-runs 2.2.0.
This commit is contained in:
SnapOtter
2026-07-29 20:21:16 +08:00
committed by GitHub
parent c2903872bb
commit 44081e6a3f
2 changed files with 22 additions and 9 deletions
+14 -7
View File
@@ -358,18 +358,25 @@ jobs:
run: |
: "${OCR_RUNTIME_INDEX_KEY_ID:?Set repository variable OCR_RUNTIME_INDEX_KEY_ID}"
: "${OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64:?Set repository variable OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64}"
actual_key_id="$(docker run --rm --entrypoint sh "${SNAPOTTER_BUNDLE_IMAGE}" \
-c 'printf %s "$OCR_RUNTIME_INDEX_KEY_ID"')"
actual_public_key="$(docker run --rm --entrypoint sh "${SNAPOTTER_BUNDLE_IMAGE}" \
-c 'printf %s "$OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64"')"
# The official image bakes the trust store as a FILE
# (/app/docker/ocr-runtime-trust.json, written by write-ocr-runtime-trust.mjs)
# and deliberately leaves OCR_RUNTIME_INDEX_* unset in the image env: those
# are the operator-override path, and loadOcrRuntimeTrustKeys
# (packages/ai/src/runtime-index.ts:52-71) reads env only when set, else the
# baked file. So verify the file the runtime actually trusts, not the env.
trust_json="$(docker run --rm --entrypoint sh "${SNAPOTTER_BUNDLE_IMAGE}" \
-c 'cat /app/docker/ocr-runtime-trust.json')"
actual_key_id="$(printf '%s' "${trust_json}" | jq -r '.keys[0].keyId')"
actual_public_key="$(printf '%s' "${trust_json}" | jq -r '.keys[0].publicKey')"
expected_public_key="$(printf '%s' "${OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64}" | base64 --decode)"
actual_official_container="$(docker run --rm --entrypoint sh "${SNAPOTTER_BUNDLE_IMAGE}" \
-c 'printf %s "$SNAPOTTER_OFFICIAL_CONTAINER"')"
[[ "${actual_key_id}" == "${OCR_RUNTIME_INDEX_KEY_ID}" ]] || {
echo "::error::Verification image OCR key ID does not match the release identity"
echo "::error::Baked OCR trust key ID (${actual_key_id}) does not match the release identity"
exit 1
}
[[ "${actual_public_key}" == "${OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64}" ]] || {
echo "::error::Verification image OCR public key does not match the release identity"
[[ "${actual_public_key}" == "${expected_public_key}" ]] || {
echo "::error::Baked OCR trust public key does not match the release identity"
exit 1
}
[[ "${actual_official_container}" == "1" ]] || {
@@ -203,8 +203,14 @@ describe("OCR v3 bundle release workflow", () => {
expect(verifyJob).toContain("timeout-minutes: 90");
expect(verifyJob).toContain("docker/verify-ocr-runtime.sh");
expect(verifyJob).toContain("Verify image has the release trust identity");
expect(verifyJob).toContain("Verification image OCR key ID does not match");
expect(verifyJob).toContain("Verification image OCR public key does not match");
// The identity is verified against the baked trust FILE, which the runtime
// reads, not the OCR_RUNTIME_INDEX_* env vars, which the official image leaves
// unset by design. Reading the env compared "" and failed a correct image.
expect(verifyJob).toContain("cat /app/docker/ocr-runtime-trust.json");
expect(verifyJob).toContain(".keys[0].keyId");
expect(verifyJob).toContain("Baked OCR trust key ID");
expect(verifyJob).toContain("Baked OCR trust public key does not match");
expect(verifyJob).not.toContain('printf %s "$OCR_RUNTIME_INDEX_KEY_ID"');
expect(verifyJob).toContain(
'actual_official_container="$(docker run --rm --entrypoint sh "${SNAPOTTER_BUNDLE_IMAGE}"',
);