diff --git a/docs/developer-guide.md b/docs/developer-guide.md index eece419a..2b8c490e 100644 --- a/docs/developer-guide.md +++ b/docs/developer-guide.md @@ -118,6 +118,112 @@ with a renewed cert. Ship `signature.json` inside your app. On install/update, ownCloud servers verify it. +> **`--path` is the packaged app payload, not your repository checkout.** The +> manifest hashes *everything* under `--path`, so signing a checkout makes +> `.git`, `tests/` and `.github/` part of the signed app — and it still verifies, +> because the manifest genuinely describes what was signed. Your users would +> install your git history. `ocsign` therefore refuses a `--path` that contains a +> `.git` entry at any depth and exits 1. `--allow-vcs` overrides that for one +> purpose only: signing a development checkout in place to try verification +> locally. Never use it in a release pipeline. + +### 4.1 Signing in CI + +Copy this into your app repo as `.github/workflows/release.yml`. It packages the +payload, signs the staging directory, and only then rolls the tarball — so the +tree you ship is exactly the tree you signed. + +If your app uses the stock ownCloud Makefile, `make appstore` already does this in +the right order: it stages the payload into `build/artifacts/appstore/`, +signs that directory, then tars it. To drive it from CI, either split the staging +part into its own target (`appstore-dir` below) and let the workflow sign and tar, +or keep the target whole and replace its legacy `occ integrity:sign-app` hook with +`ocsign`. Never tar before signing. + +Staging inside your checkout is fine — `ocsign` only looks for `.git` **under** +`--path`, so `build/artifacts/appstore/` passes the check. + +```yaml +name: release + +on: + release: + types: [published] + +permissions: + contents: write # to attach the signed tarball to the release + +jobs: + sign: + runs-on: ubuntu-latest + environment: release # a CI key is "warm": require reviewers on this job + env: + APP_ID: example-app + OCSIGN_VERSION: v0.3.0 + PAYLOAD: build/artifacts/appstore/example-app + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install ocsign (pinned, checksum-verified) + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + archive="ocsign_${OCSIGN_VERSION}_linux_amd64.tar.gz" + gh release download "$OCSIGN_VERSION" --repo owncloud/ocsign \ + --dir "$RUNNER_TEMP" --pattern "$archive" --pattern SHA256SUMS + cd "$RUNNER_TEMP" + sha256sum --ignore-missing --check SHA256SUMS + tar -xzf "$archive" ./ocsign + install -m 0755 ocsign /usr/local/bin/ocsign + + - name: Build the app payload + # Stages the release tree into $PAYLOAD and stops there: no tarball, no + # signing. Everything the app ships and nothing else — no .git, no + # tests/, no .github/, no build scratch. + run: make appstore-dir + + - name: Write the key material + env: + OCSIGN_KEY: ${{ secrets.OCSIGN_KEY }} + OCSIGN_LEAF: ${{ secrets.OCSIGN_LEAF }} + OCSIGN_CHAIN: ${{ secrets.OCSIGN_CHAIN }} + run: | + set -euo pipefail + umask 077 + printf '%s\n' "$OCSIGN_KEY" > "$RUNNER_TEMP/signing.key" + printf '%s\n' "$OCSIGN_LEAF" > "$RUNNER_TEMP/leaf.crt" + printf '%s\n' "$OCSIGN_CHAIN" > "$RUNNER_TEMP/intermediate.crt" + + - name: Sign the payload + run: | + ocsign --path "$PAYLOAD" \ + --key "$RUNNER_TEMP/signing.key" \ + --cert "$RUNNER_TEMP/leaf.crt" \ + --chain "$RUNNER_TEMP/intermediate.crt" + + # Mode-2 attestation is not yet available (see §5). Once it is, add + # --attest --attest-repo owncloud/developer-certificates to the command + # above; nothing else in this workflow changes. + + - name: Roll the tarball from the signed payload + run: tar --format=gnu -czf "$APP_ID.tar.gz" -C "$(dirname "$PAYLOAD")" "$APP_ID" + + - name: Attach it to the release + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "${{ github.event.release.tag_name }}" "$APP_ID.tar.gz" +``` + +- `OCSIGN_VERSION` pins the `ocsign` release; integrity comes from that release's + `SHA256SUMS`. For higher assurance, hardcode the archive's SHA-256 instead of + fetching the sums file from the same release. +- `actions/checkout` is pinned to a full commit SHA, and the upload uses the + pre-installed `gh` rather than a third-party action, so there is exactly one + action SHA to keep current. +- Store your leaf and intermediate as secrets as well, so nothing about signing + has to be committed to your repo. + --- ## 5. Optional: Mode-2 attestation (longevity) @@ -197,8 +303,9 @@ security team verifies and revokes. - **Baseline:** never commit or bundle your private key; restrict file permissions; keep it passphrase-encrypted at rest. - **CI signing (recommended for most):** store the key as an encrypted CI secret - and sign in your release workflow. Protect the signing job (environment - protection rules / required reviewers) — a CI secret is a "warm" key. + and sign in your release workflow (reference workflow in §4.1). Protect the + signing job (environment protection rules / required reviewers) — a CI secret is + a "warm" key. - **High assurance:** hardware-backed keys (HSM, cloud KMS, hardware token). EC P-384 is widely supported. - Consider **separate keys** for CI vs. manual release (you can hold multiple diff --git a/docs/specs/owncloud-code-signing-pki-design.md b/docs/specs/owncloud-code-signing-pki-design.md index c3f59bbc..cc2fd310 100644 --- a/docs/specs/owncloud-code-signing-pki-design.md +++ b/docs/specs/owncloud-code-signing-pki-design.md @@ -753,7 +753,8 @@ may physically delete the inert G1 files (cosmetic). - *Baseline (all):* never commit/bundle the key; restricted permissions; passphrase-encrypted at rest. - *CI signing (most):* key as an encrypted CI secret used by a release - workflow (reference GitHub Actions workflow provided). This is a "warm" key — + workflow (reference GitHub Actions workflow provided — developer-documentation + spec §4.1). This is a "warm" key — use environment protection rules / required reviewers on the signing job. - *High-assurance (partners, high-value apps):* hardware-backed keys — HSM, cloud KMS (AWS/GCP KMS), or hardware token. EC P-384 is well-supported. diff --git a/docs/specs/spec-attestation-and-crl-workflows.md b/docs/specs/spec-attestation-and-crl-workflows.md index 88097ae9..f554101a 100644 --- a/docs/specs/spec-attestation-and-crl-workflows.md +++ b/docs/specs/spec-attestation-and-crl-workflows.md @@ -84,18 +84,41 @@ A **copy-paste reference** developers add to their app repo to sign releases in (design §16, CI-signing tier). Not run by us — documentation output. It: 1. Installs the `ocsign` Go binary (pinned version/checksum). -2. Loads the developer's private key from a **repo/environment secret** (guidance: +2. **Builds the app payload** into a staging directory — the tree that becomes the + release tarball and nothing else, e.g. `build/artifacts/appstore/` (the + `make appstore` convention). No `.git`, no `tests/`, no `.github/`, no build + scratch. +3. Loads the developer's private key from a **repo/environment secret** (guidance: protect the signing job with environment protection rules / required reviewers — the key is "warm"). -3. Runs `ocsign --path . --key --cert leaf.crt --chain intermediate.crt` - to produce Mode-1 `signature.json`. -4. Optionally runs with `--attest --attest-repo ` (or calls the +4. Runs `ocsign --path build/artifacts/appstore/ --key --cert + leaf.crt --chain intermediate.crt` to produce Mode-1 + `build/artifacts/appstore//appinfo/signature.json`. +5. Optionally runs with `--attest --attest-repo ` (or calls the attestation workflow via `repository_dispatch`) to obtain and embed a Mode-2 token. -5. Packages/publishes the signed app (developer's own distribution — not through - ownCloud). - -The full YAML lives in the developer-documentation spec. +6. **Rolls the tarball from the signed staging directory** and publishes it + (developer's own distribution — not through ownCloud). + +**The signed tree and the shipped tree must be the same tree.** Package first, +point `--path` at the staging directory, tar afterwards: + +- `--path .` in a CI job signs the `actions/checkout` tree. In app mode the + manifest hashes everything under `--path` bar `appinfo/signature.json` and OS + cruft (Go tool spec §3.2), so the app's signature would legitimize `.git` + internals, tests and CI config — and it verifies, because the manifest + genuinely describes the tree that was signed. `ocsign` therefore refuses a + `--path` holding a `.git` entry at any depth (owncloud/ocsign#21), and + `--allow-vcs` MUST NOT appear in a release pipeline — it exists only for + signing a development checkout in place to exercise verification locally. +- A staging directory *inside* the checkout (`build/artifacts/…`) is fine: the + guard only looks for `.git` **under** `--path`. There is no need to stage + outside the workspace. +- Apps whose Makefile stages, signs and tars in one `appstore` target already + have the right order — keep it, and swap the legacy `occ integrity:sign-app` + hook for `ocsign`. Never tar before signing. + +The full YAML lives in the developer-documentation spec (§4.1). --- diff --git a/docs/specs/spec-developer-documentation.md b/docs/specs/spec-developer-documentation.md index 172f1c84..aa4e2bbf 100644 --- a/docs/specs/spec-developer-documentation.md +++ b/docs/specs/spec-developer-documentation.md @@ -20,7 +20,7 @@ External app developers. Sections: 1. Overview — why apps must be signed, what you get, the two modes. 2. Generate your key and CSR. 3. Request a certificate (issue form + nonce challenge). -4. Sign your app (the `ocsign` tool). +4. Sign your app (the `ocsign` tool), including the reference CI workflow. 5. (Optional) Get a Mode-2 attestation for longevity. 6. Renew / re-sign. 7. Revoke a certificate. @@ -111,6 +111,114 @@ with a renewed cert. Ship `signature.json` inside your app. On install/update, ownCloud servers verify it. +> **`--path` is the packaged app payload, not your repository checkout.** The +> manifest hashes *everything* under `--path`, so signing a checkout makes +> `.git`, `tests/` and `.github/` part of the signed app — and it verifies, +> because the manifest genuinely describes what was signed. `ocsign` refuses a +> `--path` that contains a `.git` entry at any depth and exits 1. `--allow-vcs` +> overrides that for one purpose only: signing a development checkout in place to +> try verification locally. It must never appear in a release pipeline. + +### 4.1 Signing in CI (reference GitHub Actions workflow) + +Copy this into your app repo as `.github/workflows/release.yml`. It packages the +payload, signs the staging directory, and only then rolls the tarball — so the +tree you ship is exactly the tree you signed. + +`make appstore` in the stock ownCloud app Makefile stages the payload into +`build/artifacts/appstore/`, signs that directory, and tars it, all in one +target. That order is correct; keep it. To use it from CI, either split the +staging part into its own target (`appstore-dir` below) and let the workflow sign +and tar, or leave the target whole and replace its legacy +`occ integrity:sign-app` hook with `ocsign`. Never tar before signing. + +A staging directory *inside* the checkout is fine — `ocsign` only looks for +`.git` **under** `--path`, so `build/artifacts/appstore/` passes. You do not +need to stage outside the workspace. + +```yaml +name: release + +on: + release: + types: [published] + +permissions: + contents: write # to attach the signed tarball to the release + +jobs: + sign: + runs-on: ubuntu-latest + environment: release # a CI key is "warm": require reviewers on this job + env: + APP_ID: example-app + OCSIGN_VERSION: v0.3.0 + PAYLOAD: build/artifacts/appstore/example-app + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install ocsign (pinned, checksum-verified) + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + archive="ocsign_${OCSIGN_VERSION}_linux_amd64.tar.gz" + gh release download "$OCSIGN_VERSION" --repo owncloud/ocsign \ + --dir "$RUNNER_TEMP" --pattern "$archive" --pattern SHA256SUMS + cd "$RUNNER_TEMP" + sha256sum --ignore-missing --check SHA256SUMS + tar -xzf "$archive" ./ocsign + install -m 0755 ocsign /usr/local/bin/ocsign + + - name: Build the app payload + # Stages the release tree into $PAYLOAD and stops there: no tarball, no + # signing. Everything the app ships and nothing else — no .git, no + # tests/, no .github/, no build scratch. + run: make appstore-dir + + - name: Write the key material + env: + OCSIGN_KEY: ${{ secrets.OCSIGN_KEY }} + OCSIGN_LEAF: ${{ secrets.OCSIGN_LEAF }} + OCSIGN_CHAIN: ${{ secrets.OCSIGN_CHAIN }} + run: | + set -euo pipefail + umask 077 + printf '%s\n' "$OCSIGN_KEY" > "$RUNNER_TEMP/signing.key" + printf '%s\n' "$OCSIGN_LEAF" > "$RUNNER_TEMP/leaf.crt" + printf '%s\n' "$OCSIGN_CHAIN" > "$RUNNER_TEMP/intermediate.crt" + + - name: Sign the payload + run: | + ocsign --path "$PAYLOAD" \ + --key "$RUNNER_TEMP/signing.key" \ + --cert "$RUNNER_TEMP/leaf.crt" \ + --chain "$RUNNER_TEMP/intermediate.crt" + + # Mode-2 attestation is not yet available (attestation/CRL spec ITEM #2). + # Once it is, add --attest --attest-repo owncloud/developer-certificates to + # the command above; nothing else in this workflow changes. + + - name: Roll the tarball from the signed payload + run: tar --format=gnu -czf "$APP_ID.tar.gz" -C "$(dirname "$PAYLOAD")" "$APP_ID" + + - name: Attach it to the release + env: + GH_TOKEN: ${{ github.token }} + run: gh release upload "${{ github.event.release.tag_name }}" "$APP_ID.tar.gz" +``` + +Notes to state alongside the YAML: + +- `OCSIGN_VERSION` pins the `ocsign` release; integrity comes from that release's + `SHA256SUMS`. For higher assurance, hardcode the archive's SHA-256 instead of + fetching the sums file from the same release. +- `actions/checkout` is pinned to a full commit SHA, and the release upload uses + the pre-installed `gh` rather than a third-party action — so there is exactly + one action SHA to keep current. +- Store the leaf and intermediate as secrets too, so the workflow needs nothing + committed to the repo. + --- ## 5. Optional: Mode-2 attestation (longevity) @@ -256,9 +364,10 @@ body: - **Baseline:** never commit or bundle your private key; restrict file permissions; keep it passphrase-encrypted at rest. - **CI signing (recommended for most):** store the key as an encrypted CI secret - and sign in your release workflow (reference workflow provided — attestation/CRL - spec §2). Protect the signing job (environment protection rules / required - reviewers) — a CI secret is a "warm" key. + and sign in your release workflow (reference workflow in §4.1; its place in the + overall design is attestation/CRL spec §2). Protect the signing job + (environment protection rules / required reviewers) — a CI secret is a "warm" + key. - **High assurance:** hardware-backed keys (HSM, cloud KMS, hardware token). EC P-384 is widely supported. - Consider **separate keys** for CI vs. manual release (you can hold multiple