diff --git a/.github/workflows/build-attest-dist.yaml b/.github/workflows/build-attest-dist.yaml index da9b179..ac3a328 100644 --- a/.github/workflows/build-attest-dist.yaml +++ b/.github/workflows/build-attest-dist.yaml @@ -9,6 +9,7 @@ on: - 'attest/index.js' - 'attest/package.json' - 'attest/package-lock.json' + - '.github/workflows/build-attest-dist.yaml' workflow_dispatch: permissions: @@ -50,6 +51,8 @@ jobs: if: github.event_name == 'pull_request' env: PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} + PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} + PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }} BASE_REPO: ${{ github.repository }} run: | # SECURITY: only act if the rebuild produced exactly one @@ -88,4 +91,9 @@ jobs: git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add attest/dist/index.js git commit -m "chore(attest): rebuild dist/index.js" - git push + # The checkout is the PR head SHA, a detached HEAD, so name the + # PR branch. The lease makes the push land only while the branch + # still points at the SHA this run built from; if it moved in any + # direction or was deleted, the push is rejected. + git push --force-with-lease="refs/heads/$PR_HEAD_REF:$PR_HEAD_SHA" \ + origin "HEAD:refs/heads/$PR_HEAD_REF" diff --git a/attest/README.md b/attest/README.md index cf59fbe..224cd59 100644 --- a/attest/README.md +++ b/attest/README.md @@ -47,18 +47,41 @@ If your environment cannot submit to the public Sigstore transparency log (e.g. customers in the USA / EU with data-residency requirements), use one of: -- **KMS signing**: `--kms vault://` (HashiCorp Vault transit). - The signature stays in your control. Vault transit is the only KMS - backend implemented today. +- **KMS signing**: `kms: vault://` (HashiCorp Vault transit). + The private key never leaves Vault; cimon sends the digest and receives + the signature. Vault transit is the only KMS backend implemented today, + and it requires cimon v1.0.28 or later. - **Keyed signing with checked-in private key**: `sign-key: private-key.pem`. - **Private Sigstore**: deploy your own Fulcio + Rekor and pass `fulcio-server-url` + `rekor-server-url`. See the inputs section below for the full list. +### KMS signing with HashiCorp Vault transit + +cimon reads the Vault connection from the standard Vault environment +variables (`VAULT_ADDR`, `VAULT_TOKEN`, and optionally `VAULT_NAMESPACE` +and `VAULT_CACERT`). The transit engine must be mounted at `transit/`, and +the key must support signing (for example `ecdsa-p256` or `rsa-3072`): + +```yaml +- uses: cycodelabs/cimon-action/attest@v1 + env: + VAULT_ADDR: https://vault.example.com:8200 + VAULT_TOKEN: ${{ secrets.VAULT_TOKEN }} + with: + subjects: dist\my-app.msi + kms: vault://cimon-signing + fail-on-error: true +``` + +Verifiers need the key's public half, which Vault returns from +`transit/keys/`: +`cimon attest verify --key public-key.pem --signed-prov provenance.intoto.jsonl.sig`. + ### Timestamping key-based signatures -A signature made with `sign-key` carries no time anchor unless you add an +A signature made with `sign-key` or `kms` carries no time anchor unless you add an RFC 3161 timestamp, which lets a verifier show it was made before the key was rotated or revoked. Request one explicitly; `timestamp-server-url` accepts a Sigstore timestamp authority or a standard RFC 3161 service: @@ -89,12 +112,13 @@ releases accept these inputs and produce no timestamp. | `source-path` | Path to the source checkout recorded in the attestation. Unset means automatic discovery (subject paths, run-id directory, workspace). Requires cimon v1.0.24 or later | — | | `skip-source-tree-check` | Skip verifying the source tree matches the recorded commit | `false` | | `sign-key` | Path to a private ECDSA/RSA/ED25519 PEM key | — | +| `kms` | KMS key URI for signing. HashiCorp Vault transit: `vault://`, configured through `VAULT_ADDR` and `VAULT_TOKEN`. Requires cimon v1.0.28 or later | — | | `keyless` | Use keyless (Sigstore) signing | `false` | | `tlog-upload` | **Keyless only.** Upload signature to Rekor transparency log | `true` | -| `include-timestamp` | Request an RFC 3161 timestamp and embed it in the signature. Applies to `keyless` and `sign-key` signing | `false` | +| `include-timestamp` | Request an RFC 3161 timestamp and embed it in the signature. Applies to `keyless`, `sign-key` and `kms` signing | `false` | | `fulcio-server-url` | **Keyless only.** Fulcio server URL | `https://fulcio.sigstore.dev` | | `rekor-server-url` | **Keyless only.** Rekor server URL | `https://rekor.sigstore.dev` | -| `timestamp-server-url` | RFC 3161 timestamp server URL: a Sigstore timestamp authority or a standard RFC 3161 service. With `sign-key`, forwarded only when `include-timestamp: true` | — | +| `timestamp-server-url` | RFC 3161 timestamp server URL: a Sigstore timestamp authority or a standard RFC 3161 service. With `sign-key` or `kms`, forwarded only when `include-timestamp: true` | — | | `allow-submit-data-to-public-sigstore` | **Keyless only.** Required when using public Sigstore | `false` | | `provenance-output` | Path for unsigned provenance | `provenance.intoto.jsonl` | | `signed-provenance-output` | Path for signed provenance | `provenance.intoto.jsonl.sig` | diff --git a/attest/action.yml b/attest/action.yml index 387fe0a..87383f9 100644 --- a/attest/action.yml +++ b/attest/action.yml @@ -43,6 +43,10 @@ inputs: description: Input path to a private ECDSA/RSA/ED25519 key used to sign provenance statement required: false default: '' + kms: + description: 'KMS key used to sign provenance statement, as a provider URI. HashiCorp Vault transit: vault://, with the Vault address and token in the VAULT_ADDR and VAULT_TOKEN environment variables. Requires cimon v1.0.28 or later.' + required: false + default: '' keyless: description: Allow keyless signature if no signing key is provided required: false @@ -52,7 +56,7 @@ inputs: required: false default: 'true' include-timestamp: - description: 'Request an RFC 3161 timestamp of the artifact signature from a timestamping authority. Applies to keyless and sign-key signing.' + description: 'Request an RFC 3161 timestamp of the artifact signature from a timestamping authority. Applies to keyless, sign-key and kms signing.' required: false default: 'false' fulcio-server-url: @@ -64,7 +68,7 @@ inputs: required: false default: 'https://rekor.sigstore.dev' timestamp-server-url: - description: 'RFC 3161 timestamp server URL: a Sigstore timestamp authority or a standard RFC 3161 service. With sign-key, forwarded only when include-timestamp is true.' + description: 'RFC 3161 timestamp server URL: a Sigstore timestamp authority or a standard RFC 3161 service. With sign-key or kms, forwarded only when include-timestamp is true.' required: false allow-submit-data-to-public-sigstore: description: 'Keyless signing only: agree to submit data to an immutable public transparency log (needed for public Sigstore). Ignored unless keyless is true.' diff --git a/attest/dist/index.js b/attest/dist/index.js index 0769217..72cba5a 100644 --- a/attest/dist/index.js +++ b/attest/dist/index.js @@ -127691,6 +127691,7 @@ function getActionConfig() { subjects: _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('subjects'), imageRef: _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('image-ref'), signKey: _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('sign-key'), + kms: _actions_core__WEBPACK_IMPORTED_MODULE_0__.getInput('kms'), keyless: _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('keyless'), tlogUpload: _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('tlog-upload'), includeTimestamp: _actions_core__WEBPACK_IMPORTED_MODULE_0__.getBooleanInput('include-timestamp'), @@ -127793,6 +127794,7 @@ async function run(config) { if (config.attest.signedProvenanceOutput !== '') args.push('--output-signed-prov', config.attest.signedProvenanceOutput); if (config.attest.signKey !== '') args.push('--key', config.attest.signKey); + if (config.attest.kms !== '') args.push('--kms', config.attest.kms); if (config.attest.sourcePath !== '') args.push('--source-path', config.attest.sourcePath); if (config.attest.skipSourceTreeCheck) @@ -127804,6 +127806,8 @@ async function run(config) { if (config.cimon.logLevel !== '') args.push('--log-level', config.cimon.logLevel); if (config.report.reportJobSummary) args.push('--report-job-summary'); + const keyBasedSigning = + config.attest.signKey !== '' || config.attest.kms !== ''; if (config.attest.keyless) { args.push('--keyless'); @@ -127824,8 +127828,8 @@ async function run(config) { `--timestamp-server-url=${config.attest.timestampServerUrl}` ); } - } else if (config.attest.signKey !== '' && config.attest.includeTimestamp) { - // Key-based signing only timestamps on an explicit request, so the + } else if (keyBasedSigning && config.attest.includeTimestamp) { + // Key and KMS signing only timestamp on an explicit request, so the // inputs are forwarded only when include-timestamp is true. Older // cimon releases accept and ignore them. args.push('--include-timestamp=true'); @@ -127855,7 +127859,7 @@ async function run(config) { path__WEBPACK_IMPORTED_MODULE_5__.dirname(config.attest.provenanceOutput), { continueOnError: true } ); - if (config.attest.signKey !== '') { + if (keyBasedSigning) { client.uploadArtifact( `signed-provenance-${jobId}-${randomSuffix}`, [config.attest.signedProvenanceOutput], diff --git a/attest/index.js b/attest/index.js index a98d0df..ccfc0b2 100644 --- a/attest/index.js +++ b/attest/index.js @@ -131,6 +131,7 @@ function getActionConfig() { subjects: core.getInput('subjects'), imageRef: core.getInput('image-ref'), signKey: core.getInput('sign-key'), + kms: core.getInput('kms'), keyless: core.getBooleanInput('keyless'), tlogUpload: core.getBooleanInput('tlog-upload'), includeTimestamp: core.getBooleanInput('include-timestamp'), @@ -233,6 +234,7 @@ async function run(config) { if (config.attest.signedProvenanceOutput !== '') args.push('--output-signed-prov', config.attest.signedProvenanceOutput); if (config.attest.signKey !== '') args.push('--key', config.attest.signKey); + if (config.attest.kms !== '') args.push('--kms', config.attest.kms); if (config.attest.sourcePath !== '') args.push('--source-path', config.attest.sourcePath); if (config.attest.skipSourceTreeCheck) @@ -244,6 +246,8 @@ async function run(config) { if (config.cimon.logLevel !== '') args.push('--log-level', config.cimon.logLevel); if (config.report.reportJobSummary) args.push('--report-job-summary'); + const keyBasedSigning = + config.attest.signKey !== '' || config.attest.kms !== ''; if (config.attest.keyless) { args.push('--keyless'); @@ -264,8 +268,8 @@ async function run(config) { `--timestamp-server-url=${config.attest.timestampServerUrl}` ); } - } else if (config.attest.signKey !== '' && config.attest.includeTimestamp) { - // Key-based signing only timestamps on an explicit request, so the + } else if (keyBasedSigning && config.attest.includeTimestamp) { + // Key and KMS signing only timestamp on an explicit request, so the // inputs are forwarded only when include-timestamp is true. Older // cimon releases accept and ignore them. args.push('--include-timestamp=true'); @@ -295,7 +299,7 @@ async function run(config) { path.dirname(config.attest.provenanceOutput), { continueOnError: true } ); - if (config.attest.signKey !== '') { + if (keyBasedSigning) { client.uploadArtifact( `signed-provenance-${jobId}-${randomSuffix}`, [config.attest.signedProvenanceOutput],