# Gateway Image Signing Owner Facts

Gateway images are signed by the repository that owns them. The canonical,
machine-readable owner policy is
[`release/gateway-image-signing-policy.json`](../release/gateway-image-signing-policy.json),
and its `public_key_path` identifies the complete verifier input. Consumers must
read both files from the same immutable source commit as the image release. This
document explains the boundary; it does not duplicate those versioned facts.

The owner policy records the stable repository identity, source ref, publisher
workflow, image repository, AWS role and immutable KMS key, signing algorithm,
public-key digest, GitHub OIDC issuer/JWKS/audience policy, verifier-key path,
and canonical SLSA v1 predicate/build/builder identities. The publisher rejects
unknown or missing policy fields, proves the live KMS public key equals the
committed key, and binds the policy digest into every image signature.

The authority workflow was bootstrapped separately from its activation. The
active caller and `provenance_builder_id` now reference the reviewed foundation
merge SHA `c2cedc3d50ef39e79e93b9dd0d0f34c22592d1d6`. The owner policy is
`active`; AWS trust must require the same exact `job_workflow_ref`. A branch or
`main` reference is not an immutable authority pin.

GitHub job boundaries are part of the owner policy's enforcement shape. Product
tests and repository checks run in a validation job with neither package-write
nor OIDC capability. The candidate builder runs on a fresh runner, executes no
product tests or repository scripts, and has package publication access but no
OIDC capability. A
separate signer job starts on a fresh runner, is the only job granted
`id-token: write`, and independently re-resolves and verifies the registry
candidate before assuming the AWS role. It never checks out or executes the
application repository: policy and public-key files are fetched as data and
validated against workflow-owned constants. Tests, dependency downloads, Go
tooling, repository scripts, binary construction, and image construction never
run in that authority-bearing job. A fresh job with no OIDC, KMS, or package-write
authority runs the complete downstream verifier before release discovery is
committed. A final source-free job owns only the exact-SHA tag write; it cannot
sign or execute repository source. Existing releases are revalidated in a
separate job with no OIDC, KMS, or package-write capability.
Ordering steps inside one job is not a security boundary because GitHub grants
OIDC request capability for the complete job lifetime.

## External AWS boundary

The AWS administrator read back the Gateway role and key configuration on
2026-07-15. In account `604175033442`, role
`nexuscrm-api-host-gateway-publisher` restricts trust to the GitHub OIDC
audience, immutable repository ID, owner repository, `main` ref, publisher
workflow, and exact reusable `job_workflow_ref` declared by the owner policy.
The role had no managed policy or grant. Its inline policy
allowed only KMS key description, public-key retrieval, signing, and verification
against the exact key declared by the owner policy, with `ECDSA_SHA_256` required
for signing and verification.

The `StringEquals` readback requires
`token.actions.githubusercontent.com:job_workflow_ref` to equal
`pingcap-inc/nexuscrm-api-host/.github/workflows/trusted-publish-gateway-image.yml@c2cedc3d50ef39e79e93b9dd0d0f34c22592d1d6`.
Repository/ref/workflow-name checks alone identify the caller but do not freeze
the code allowed to use KMS.

This administrative readback is configuration evidence, not a second release
identity. The committed public key proves use of the owner key, but a
signer-authored Cosign annotation cannot independently prove which GitHub
workflow invoked that key. Therefore every accepted payload also carries a
GitHub-signed OIDC JWT with an audience bound to its exact image digest. L2
receives no AWS role, KMS permission, private key, or signing capability.

## Downstream verification

After resolving an exact-main image tag to an immutable digest, check out this
repository at that exact source commit and run:

```bash
./scripts/gateway-image-signing-policy.sh verify-release \
  "$SOURCE_COMMIT" "$IMAGE_DIGEST"
```

The command validates the policy and public-key digest before invoking Cosign,
requires a KMS-signed SLSA v1 provenance statement for the same image and
GitHub run, then requires every owner annotation: repository, source commit, source ref,
publisher workflow, immutable KMS key, and owner-policy digest. From the same
KMS-verified payload it extracts a GitHub OIDC JWT and independently verifies
its RS256 signature against GitHub's declared JWKS. Exact issuer, digest-bound
audience, subject, repository ID/visibility, source SHA/ref, workflow
name/ref/SHA, push event, GitHub-hosted runner, and run identity are mandatory.
The source SHA and workflow SHA are validated independently and preserved as
separate receipt fields; no equality assumption turns two GitHub-owned facts
into one. The policy pins the workflow path/ref, while the signed token proves
the concrete workflow revision used by that run.
It rejects a different repository, source identity, workflow, key, policy, or
image digest. The image publisher disables transparency-log upload because the
repository and image are private; verification uses the committed KMS public
key and GitHub OIDC public key and does not contact AWS.

The owner script explicitly selects Cosign's legacy simple-signing payload on
both sign and verify operations, disables Cosign v3's remote signing-config
default, and sets the critical container identity to the owner image
repository. These are a single versioned payload contract, not ambient Cosign
CLI defaults. The repository verifier consumes that exact `critical` / `optional`
grammar. A future Sigstore bundle migration must change the producer, consumer,
fixture, and tests together; silently inheriting a new Cosign default is
fail-closed. The real v3.0.6 round-trip fixture and reproduction command live in
[`internal/gatewayrelease/testdata`](../internal/gatewayrelease/testdata/README.md).

The verifier emits a receipt without the bearer token. L2 should independently
fetch the JWKS during admission, retain the exact JWKS input, and pin the
receipt's matched OIDC public-key digest alongside the image digest. This makes
key rotation explicit rather than silently converting an owner-provided key
into a second workflow-identity authority. `GATEWAY_OIDC_JWKS_FILE` selects that
captured input for reproducible verification.

The JWT itself is retained only inside the private-registry signature payload.
It is short-lived and its custom audience names one immutable image digest, so
it is not accepted by the AWS role exchange and cannot authorize a different
artifact. The publisher masks the value in logs and removes the runner-local
token file immediately after signing. Preserving the GitHub signature is
necessary: replacing the JWT with owner-authored claims would recreate the
same unverifiable-workflow defect this policy is designed to remove.

The BuildKit-generated maximum-mode provenance and SBOM remain useful build
metadata, but they are candidate-authored and therefore are not release
authority. The isolated signer generates and KMS-signs the canonical predicate
defined by [`gateway-image-build-type-v1.md`](gateway-image-build-type-v1.md).
The authority-bearing job constructs and validates that predicate with pinned
runner tools and workflow code; it does not execute the source checkout to do
so. The signed statement is the release control plane's assertion about the
workflow-owned candidate build, not a claim that arbitrary candidate-authored
attestations were independently trustworthy.
Downstream verification requires that signed statement and pins its digest in
the release receipt. This restores the source/build linkage formerly supplied
by GitHub `actions/attest` without claiming reproducible-build or SLSA Build
Level 3 guarantees.
