diff --git a/Dockerfile b/Dockerfile index a3d536a3dbf..8a26032602c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -239,9 +239,16 @@ RUN --mount=type=cache,id=openclaw-bookworm-apt-cache,target=/var/cache/apt,shar ca-certificates curl gnupg && \ install -m 0755 -d /etc/apt/keyrings && \ # Verify Docker apt signing key fingerprint before trusting it as a root key. + # Require exactly one primary key (`pub` in --with-colons; subkeys use `sub`) so we + # never pin the first fingerprint while apt trusts extra keys from the same file. # Update OPENCLAW_DOCKER_GPG_FINGERPRINT when Docker rotates release keys. curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg.asc && \ expected_fingerprint="$(printf '%s' "$OPENCLAW_DOCKER_GPG_FINGERPRINT" | tr '[:lower:]' '[:upper:]' | tr -d '[:space:]')" && \ + docker_gpg_pub_count="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "pub" { c++ } END { print c+0 }')" && \ + if [ "$docker_gpg_pub_count" != "1" ]; then \ + echo "ERROR: Docker apt key must contain exactly one public key (found $docker_gpg_pub_count); refusing a multi-key file." >&2; \ + exit 1; \ + fi && \ actual_fingerprint="$(gpg --batch --show-keys --with-colons /tmp/docker.gpg.asc | awk -F: '$1 == "fpr" { print toupper($10); exit }')" && \ if [ -z "$actual_fingerprint" ] || [ "$actual_fingerprint" != "$expected_fingerprint" ]; then \ echo "ERROR: Docker apt key fingerprint mismatch (expected $expected_fingerprint, got ${actual_fingerprint:-})" >&2; \ diff --git a/src/dockerfile.test.ts b/src/dockerfile.test.ts index 5aa781df259..efeb15d1161 100644 --- a/src/dockerfile.test.ts +++ b/src/dockerfile.test.ts @@ -146,6 +146,24 @@ describe("Dockerfile", () => { expect(dockerfile).not.toContain('\\"fpr\\"'); }); + it("counts primary pub keys before Docker apt fingerprint compare and dearmor", async () => { + const dockerfile = collapseDockerContinuations(await readFile(dockerfilePath, "utf8")); + const anchor = dockerfile.indexOf( + "curl -fsSL https://download.docker.com/linux/debian/gpg -o /tmp/docker.gpg.asc", + ); + expect(anchor).toBeGreaterThan(-1); + const slice = dockerfile.slice(anchor); + expect(slice).toContain("docker_gpg_pub_count="); + expect(slice).toContain('$1 == "pub"'); + expect(slice).not.toContain('\\"pub\\"'); + const pubCountIdx = slice.indexOf("docker_gpg_pub_count="); + const fpIdx = slice.indexOf("actual_fingerprint="); + const dearmorIdx = slice.indexOf("gpg --dearmor"); + expect(pubCountIdx).toBeLessThan(fpIdx); + expect(fpIdx).toBeLessThan(dearmorIdx); + expect(slice).toContain('[ "$docker_gpg_pub_count" != "1" ]'); + }); + it("keeps runtime pnpm available", async () => { const dockerfile = await readFile(dockerfilePath, "utf8"); expect(dockerfile).toContain("ENV COREPACK_HOME=/usr/local/share/corepack");