fix(wake): #912 exercise the digest/HMAC trust suite in real CI (fix runner divergence + openssl + hard-require) (#921)
Co-authored-by: jason.woltje <jason@diversecanvas.com> Co-committed-by: jason.woltje <jason@diversecanvas.com>
This commit was merged in pull request #921.
This commit is contained in:
@@ -98,6 +98,12 @@ steps:
|
|||||||
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
|
DATABASE_URL: postgresql://mosaic:mosaic@ci-postgres:5432/mosaic
|
||||||
commands:
|
commands:
|
||||||
- *enable_pnpm
|
- *enable_pnpm
|
||||||
|
# openssl (#912) is the wake HMAC signer: the digest H1/H2, beacon B12,
|
||||||
|
# and install I8 legs hard-require it in CI. It is baked into ci-base via
|
||||||
|
# Dockerfile.ci, but ci-base only rebuilds on push-to-main/tag — this
|
||||||
|
# `apk add` guarantees openssl is present on PR pipelines too (and is a
|
||||||
|
# fast no-op once the rebuilt image already ships it).
|
||||||
|
- apk add --no-cache openssl
|
||||||
# postgresql-client (pg_isready) is baked into ci-base.
|
# postgresql-client (pg_isready) is baked into ci-base.
|
||||||
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
|
# Wait up to 60s for CI postgres to be ready; fail fast if it never comes up.
|
||||||
- |
|
- |
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ FROM node:24-alpine
|
|||||||
# postgresql-client used by the test step's pg_isready readiness probe. `bash`,
|
# postgresql-client used by the test step's pg_isready readiness probe. `bash`,
|
||||||
# `git`, and `jq` are baked here too — framework shell tests and the shipped
|
# `git`, and `jq` are baked here too — framework shell tests and the shipped
|
||||||
# Codex review wrappers require them without per-run installation in ci.yml.
|
# Codex review wrappers require them without per-run installation in ci.yml.
|
||||||
RUN apk add --no-cache python3 make g++ postgresql-client bash git jq
|
# `openssl` (#912) is the non-circular HMAC signer for the wake trust layer:
|
||||||
|
# the digest H1/H2, beacon B12, and install I8 legs hard-require it in CI so the
|
||||||
|
# §4 G6 evidence comes from an actually-run HMAC leg, not a skipped one.
|
||||||
|
RUN apk add --no-cache python3 make g++ postgresql-client bash git jq openssl
|
||||||
|
|
||||||
# Pin pnpm to the repo's packageManager version via corepack.
|
# Pin pnpm to the repo's packageManager version via corepack.
|
||||||
RUN corepack enable && corepack prepare pnpm@10.6.2 --activate
|
RUN corepack enable && corepack prepare pnpm@10.6.2 --activate
|
||||||
|
|||||||
@@ -88,16 +88,37 @@ EOF
|
|||||||
# _scrub_ctrl (stdin) — strip ANSI escape sequences, Unicode bidi controls,
|
# _scrub_ctrl (stdin) — strip ANSI escape sequences, Unicode bidi controls,
|
||||||
# zero-width characters, and C0/C1 control bytes. Byte-exact under LC_ALL=C so a
|
# zero-width characters, and C0/C1 control bytes. Byte-exact under LC_ALL=C so a
|
||||||
# multibyte control sequence cannot slip through a locale-dependent class.
|
# multibyte control sequence cannot slip through a locale-dependent class.
|
||||||
|
#
|
||||||
|
# PORTABILITY (#912): the byte patterns are LITERAL bytes (materialized via
|
||||||
|
# `printf %b`), NOT GNU-sed `\xNN` hex escapes. `\xNN` is a GNU-sed extension;
|
||||||
|
# BusyBox sed (the Alpine/musl CI runner, running as root) REJECTS a `\xNN`
|
||||||
|
# character range with "bad regex ... Invalid character range", which aborted
|
||||||
|
# the whole sed and silently VOIDED the scrub in CI — the digest suite's D1/D4/
|
||||||
|
# D5/D6 all failed only in the Woodpecker runner because every scrubbed value
|
||||||
|
# collapsed to empty. Literal bytes match identically under GNU sed (glibc dev)
|
||||||
|
# and BusyBox sed (Alpine CI): a wake digest must render byte-for-byte the same
|
||||||
|
# regardless of the runner's sed implementation. LC_ALL=C keeps every match
|
||||||
|
# byte-exact (no locale-dependent multibyte class).
|
||||||
_scrub_ctrl() {
|
_scrub_ctrl() {
|
||||||
|
local ESC p280 p281 aa ae a6 a9 x8b x8f a0 bom alm
|
||||||
|
ESC="$(printf '%b' '\x1b')" # U+001B ESC
|
||||||
|
p280="$(printf '%b' '\xe2\x80')" # UTF-8 lead bytes for U+2000..U+203F
|
||||||
|
p281="$(printf '%b' '\xe2\x81')" # UTF-8 lead bytes for U+2040..U+207F
|
||||||
|
aa="$(printf '%b' '\xaa')"; ae="$(printf '%b' '\xae')" # U+202A..U+202E bidi
|
||||||
|
a6="$(printf '%b' '\xa6')"; a9="$(printf '%b' '\xa9')" # U+2066..U+2069 isolates
|
||||||
|
x8b="$(printf '%b' '\x8b')"; x8f="$(printf '%b' '\x8f')" # U+200B..U+200F zero-width
|
||||||
|
a0="$(printf '%b' '\xa0')" # U+2060 word joiner
|
||||||
|
bom="$(printf '%b' '\xef\xbb\xbf')" # U+FEFF BOM/ZWNBSP
|
||||||
|
alm="$(printf '%b' '\xd8\x9c')" # U+061C arabic letter mark
|
||||||
LC_ALL=C sed -E \
|
LC_ALL=C sed -E \
|
||||||
-e 's/\x1b\[[0-9;?]*[ -/]*[@-~]//g' \
|
-e 's/'"$ESC"'\[[0-9;?]*[ -/]*[@-~]//g' \
|
||||||
-e 's/\x1b[@-Z\\-_]//g' \
|
-e 's/'"$ESC"'[@-Z\\-_]//g' \
|
||||||
-e 's/\xe2\x80[\xaa-\xae]//g' \
|
-e 's/'"$p280"'['"$aa"'-'"$ae"']//g' \
|
||||||
-e 's/\xe2\x81[\xa6-\xa9]//g' \
|
-e 's/'"$p281"'['"$a6"'-'"$a9"']//g' \
|
||||||
-e 's/\xe2\x80[\x8b-\x8f]//g' \
|
-e 's/'"$p280"'['"$x8b"'-'"$x8f"']//g' \
|
||||||
-e 's/\xe2\x81\xa0//g' \
|
-e 's/'"$p281$a0"'//g' \
|
||||||
-e 's/\xef\xbb\xbf//g' \
|
-e 's/'"$bom"'//g' \
|
||||||
-e 's/\xd8\x9c//g' |
|
-e 's/'"$alm"'//g' |
|
||||||
LC_ALL=C tr -d '\000-\010\013\014\016-\037\177'
|
LC_ALL=C tr -d '\000-\010\013\014\016-\037\177'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,8 +45,20 @@
|
|||||||
# pointer carries a usable (soft) locator instead of rendering
|
# pointer carries a usable (soft) locator instead of rendering
|
||||||
# empty. Display-only: the ACTIONABLE-tier hard-locator FAIL-LOUD
|
# empty. Display-only: the ACTIONABLE-tier hard-locator FAIL-LOUD
|
||||||
# gate (_has_hard_locator, exit 4) is unchanged.
|
# gate (_has_hard_locator, exit 4) is unchanged.
|
||||||
|
# 0.6.3 #912 digest.sh scrub PORTABILITY (no contract change): _scrub_ctrl's
|
||||||
|
# control/bidi/zero-width byte patterns are now LITERAL bytes (via
|
||||||
|
# printf %b) instead of GNU-sed `\xNN` hex escapes. BusyBox sed (the
|
||||||
|
# Alpine/musl CI runner, running as root) rejects a `\xNN` character
|
||||||
|
# range, which aborted the whole scrub sed and silently VOIDED the
|
||||||
|
# scrub in CI — collapsing every scrubbed value to empty and failing
|
||||||
|
# the digest suite's D1/D4/D5/D6 only in the Woodpecker runner. The
|
||||||
|
# scrub now renders byte-identically under GNU sed (glibc dev) and
|
||||||
|
# BusyBox sed (Alpine CI). The two-tier trust, exit-4 hard-locator
|
||||||
|
# FAIL-LOUD, and the secret-scrub/SHA-preservation contract are all
|
||||||
|
# unchanged — this makes the existing scrub deterministic across
|
||||||
|
# runners, it does not weaken it.
|
||||||
component=wake
|
component=wake
|
||||||
version=0.6.2
|
version=0.6.3
|
||||||
|
|
||||||
# Watch-list schema this component consumes, and the INCLUSIVE range of
|
# Watch-list schema this component consumes, and the INCLUSIVE range of
|
||||||
# schema_version values it supports. A wake-watch-list.json whose schema_version
|
# schema_version values it supports. A wake-watch-list.json whose schema_version
|
||||||
|
|||||||
@@ -296,8 +296,15 @@ echo "== B11: staleness from monitor ingested_ts -> a far-future emit_ts STILL g
|
|||||||
) && ok
|
) && ok
|
||||||
|
|
||||||
echo "== B12: HMAC-verify at record -> a spoofed (bad-sig) beacon is REJECTED =="
|
echo "== B12: HMAC-verify at record -> a spoofed (bad-sig) beacon is REJECTED =="
|
||||||
|
# #912: hard-require openssl in CI (Woodpecker sets CI=woodpecker) so the beacon
|
||||||
|
# HMAC-verify leg is actually exercised; keep the skip for openssl-less local dev.
|
||||||
if ! command -v openssl >/dev/null 2>&1; then
|
if ! command -v openssl >/dev/null 2>&1; then
|
||||||
echo "SKIP: openssl not available" >&2
|
if [ -n "${CI:-}" ]; then
|
||||||
|
echo " FAIL: B12 requires openssl in CI (#912) but it is not on PATH — the CI image must provide it" >&2
|
||||||
|
echo "x" >>"$FAILFILE"
|
||||||
|
else
|
||||||
|
echo "SKIP: openssl not available (local dev; CI hard-requires it)" >&2
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
(
|
(
|
||||||
H="$(fresh_home b12)"
|
H="$(fresh_home b12)"
|
||||||
|
|||||||
@@ -41,11 +41,20 @@ command -v jq >/dev/null 2>&1 || {
|
|||||||
echo "SKIP: jq not available" >&2
|
echo "SKIP: jq not available" >&2
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
# NOTE: whole-suite openssl-skip retained; unmasking D1-D6 to run in CI is tracked in #912 (they currently fail under the CI env).
|
# openssl gates the HMAC legs (H1/H2). #912: the wake trust layer MUST be
|
||||||
command -v openssl >/dev/null 2>&1 || {
|
# exercised in real CI, so when running under CI (Woodpecker sets CI=woodpecker)
|
||||||
echo "SKIP: openssl not available" >&2
|
# openssl is HARD-REQUIRED — a missing openssl fails the suite LOUD rather than
|
||||||
|
# silently skipping the signer (the §4 G6 evidence must come from an
|
||||||
|
# actually-run HMAC leg, not a skipped one). In an openssl-less LOCAL DEV env
|
||||||
|
# the whole suite still skips so `pnpm test` stays runnable without openssl.
|
||||||
|
if ! command -v openssl >/dev/null 2>&1; then
|
||||||
|
if [ -n "${CI:-}" ]; then
|
||||||
|
echo "FATAL (#912): openssl is REQUIRED in CI to exercise the wake digest/HMAC trust layer, but is not on PATH. The CI image must provide openssl (see .woodpecker/ci-image.yml)." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "SKIP: openssl not available (local dev; CI hard-requires it)" >&2
|
||||||
exit 0
|
exit 0
|
||||||
}
|
fi
|
||||||
|
|
||||||
TMP_ROOT="$(mktemp -d)"
|
TMP_ROOT="$(mktemp -d)"
|
||||||
trap 'rm -rf "$TMP_ROOT"' EXIT
|
trap 'rm -rf "$TMP_ROOT"' EXIT
|
||||||
@@ -209,10 +218,26 @@ echo "== D4: SCRUB — secret-canary + ANSI/bidi/zero-width in source free-text
|
|||||||
# ANSI escape / CSI must be gone.
|
# ANSI escape / CSI must be gone.
|
||||||
printf '%s' "$out" | LC_ALL=C grep -q "$(printf '\x1b')" && fail_msg "D4: ANSI ESC survived the scrub"
|
printf '%s' "$out" | LC_ALL=C grep -q "$(printf '\x1b')" && fail_msg "D4: ANSI ESC survived the scrub"
|
||||||
# bidi/zero-width/BOM UTF-8 sequences must be gone.
|
# bidi/zero-width/BOM UTF-8 sequences must be gone.
|
||||||
printf '%s' "$out" | LC_ALL=C grep -qP '\xe2\x80[\x8b-\x8f\xaa-\xae]|\xef\xbb\xbf' &&
|
# #912: patterns are LITERAL bytes + `grep -E`, NOT PCRE `grep -P`. BusyBox
|
||||||
|
# grep (Alpine/musl CI) has no `-P` — a `grep -qP` there errors
|
||||||
|
# ("unrecognized option: P"), returns non-zero, and the `&&` silently skips
|
||||||
|
# the assertion, so the scrub was NEVER checked in CI. Literal-byte ranges
|
||||||
|
# under `grep -E` + LC_ALL=C match identically on BusyBox and GNU grep.
|
||||||
|
# Two DISJOINT byte ranges: U+200B..U+200F (E2 80 8B..8F, zero-width) and
|
||||||
|
# U+202A..U+202E (E2 80 AA..AE, bidi). NOT a single 8B..AE range — that would
|
||||||
|
# wrongly flag legitimate E2 80 xx punctuation in between, e.g. U+2014 EM DASH
|
||||||
|
# (E2 80 94) which the digest body uses.
|
||||||
|
_b280="$(printf '%b' '\xe2\x80')"
|
||||||
|
_b8b="$(printf '%b' '\x8b')"; _b8f="$(printf '%b' '\x8f')"
|
||||||
|
_baa="$(printf '%b' '\xaa')"; _bae="$(printf '%b' '\xae')"
|
||||||
|
_bbom="$(printf '%b' '\xef\xbb\xbf')"
|
||||||
|
printf '%s' "$out" | LC_ALL=C grep -qE "${_b280}[${_b8b}-${_b8f}${_baa}-${_bae}]|${_bbom}" &&
|
||||||
fail_msg "D4: bidi/zero-width/BOM survived the scrub"
|
fail_msg "D4: bidi/zero-width/BOM survived the scrub"
|
||||||
# C0 control bytes (except tab/newline) must be gone.
|
# C0 control bytes (except tab/newline) must be gone.
|
||||||
printf '%s' "$out" | LC_ALL=C grep -qP '[\x00-\x08\x0e-\x1f\x7f]' && fail_msg "D4: a C0 control byte survived the scrub"
|
_c00="$(printf '%b' '\x01')"; _c08="$(printf '%b' '\x08')"
|
||||||
|
_c0e="$(printf '%b' '\x0e')"; _c1f="$(printf '%b' '\x1f')"; _c7f="$(printf '%b' '\x7f')"
|
||||||
|
printf '%s' "$out" | LC_ALL=C grep -qE "[${_c00}-${_c08}${_c0e}-${_c1f}${_c7f}]" &&
|
||||||
|
fail_msg "D4: a C0 control byte survived the scrub"
|
||||||
# Secret canaries must be redacted, never inlined.
|
# Secret canaries must be redacted, never inlined.
|
||||||
printf '%s' "$out" | grep -q 'ghp_0123456789' && fail_msg "D4: GitHub-token canary LEAKED into the digest"
|
printf '%s' "$out" | grep -q 'ghp_0123456789' && fail_msg "D4: GitHub-token canary LEAKED into the digest"
|
||||||
printf '%s' "$out" | grep -q 'AKIAIOSFODNN7EXAMPLE' && fail_msg "D4: AWS-key canary LEAKED into the digest"
|
printf '%s' "$out" | grep -q 'AKIAIOSFODNN7EXAMPLE' && fail_msg "D4: AWS-key canary LEAKED into the digest"
|
||||||
|
|||||||
@@ -219,8 +219,15 @@ echo "== I7: ingested_ts staleness — a far-future emit_ts still goes stale (re
|
|||||||
) && ok
|
) && ok
|
||||||
|
|
||||||
echo "== I8: beacon HMAC-verify at record — a spoofed (bad-sig) beacon is REJECTED =="
|
echo "== I8: beacon HMAC-verify at record — a spoofed (bad-sig) beacon is REJECTED =="
|
||||||
|
# #912: hard-require openssl in CI (Woodpecker sets CI=woodpecker) so the install
|
||||||
|
# beacon-sign leg is actually exercised; keep the skip for openssl-less local dev.
|
||||||
if ! command -v openssl >/dev/null 2>&1; then
|
if ! command -v openssl >/dev/null 2>&1; then
|
||||||
echo "SKIP: openssl not available" >&2
|
if [ -n "${CI:-}" ]; then
|
||||||
|
echo " FAIL: I8 requires openssl in CI (#912) but it is not on PATH — the CI image must provide it" >&2
|
||||||
|
echo "x" >>"$FAILFILE"
|
||||||
|
else
|
||||||
|
echo "SKIP: openssl not available (local dev; CI hard-requires it)" >&2
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
(
|
(
|
||||||
H="$(fresh i8)"
|
H="$(fresh i8)"
|
||||||
|
|||||||
Reference in New Issue
Block a user