Kaynağa Gözat

fix: macos signing checks

jamesread 3 hafta önce
ebeveyn
işleme
bcf5324dae

+ 7 - 0
.github/workflows/build-and-release.yml

@@ -133,6 +133,13 @@ jobs:
         if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false
         uses: docker/setup-buildx-action@v4
 
+      - name: Verify macOS signing certificate chain
+        if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)
+        env:
+          MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
+          MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
+        run: ./var/macos/verify-macos-sign-p12.sh
+
       - name: release
         id: release
         if: github.ref_type != 'tag' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false)

+ 0 - 2
docs/antora.yml

@@ -11,5 +11,3 @@ asciidoc:
     toclevels: 2
 nav:
 - modules/ROOT/nav.adoc
-
-

+ 2 - 0
docs/modules/dev/nav.adoc

@@ -0,0 +1,2 @@
+* xref:index.adoc[Index]
+* xref:signing.adoc[Signing]

+ 3 - 0
docs/modules/dev/pages/index.adoc

@@ -0,0 +1,3 @@
+= Developer notes
+
+TODO.

+ 33 - 6
docs/modules/dev/pages/signing.adoc

@@ -36,24 +36,39 @@ openssl req -new -key developer_id_app.key -out developer_id_app.csr \
 2. Create a certificate of type **Developer ID Application**. Prefer **G2 Sub-CA** if the portal asks.
 3. Upload `developer_id_app.csr` and download the resulting `.cer` (often named `developerID_application.cer`).
 
-Build a `.p12` that includes Apple's Developer ID G2 intermediate:
+Build a `.p12` that includes the **full** chain: leaf + Developer ID G2 intermediate + Apple Root CA.
+
+The Apple Root **must** be present. With only leaf + G2, quill embeds a designated requirement of the form `certificate root[field.1.2.840.113635.100.6.2.6]`. On macOS that resolves to Apple Root CA (which does not have that OID), so AMFI SIGKILLs the binary with `does not satisfy its designated Requirement` even though notarization still succeeds. A correct chain produces `certificate 1[...]` instead.
 
 ```sh
 curl -fsSLO https://www.apple.com/certificateauthority/DeveloperIDG2CA.cer
+curl -fsSLO https://www.apple.com/appleca/AppleIncRootCertificate.cer
 
 openssl x509 -inform DER -in developerID_application.cer -out developerID_application.pem
 openssl x509 -inform DER -in DeveloperIDG2CA.cer -out DeveloperIDG2CA.pem
+openssl x509 -inform DER -in AppleIncRootCertificate.cer -out AppleRootCA.pem
+
+# Chain file: intermediate then root (leaf is passed separately via -in).
+cat DeveloperIDG2CA.pem AppleRootCA.pem > chain.pem
 
 # Export password becomes MACOS_SIGN_PASSWORD.
 # On OpenSSL 3 (e.g. Fedora), -legacy improves compatibility with some tooling:
 openssl pkcs12 -export -legacy \
   -inkey developer_id_app.key \
   -in developerID_application.pem \
-  -certfile DeveloperIDG2CA.pem \
+  -certfile chain.pem \
   -out Certificates.p12
 ```
 
-If you already have a Mac with the certificate in Keychain Access, you can export a `.p12` from there instead; the OpenSSL path above is enough when you do not.
+Confirm the `.p12` has three certificates before base64-encoding:
+
+```sh
+openssl pkcs12 -in Certificates.p12 -nodes -passin pass:"$MACOS_SIGN_PASSWORD" 2>/dev/null \
+  | grep -c "BEGIN CERTIFICATE"
+# expect: 3
+```
+
+If you already have a Mac with the certificate in Keychain Access, you can export a `.p12` from there instead — include the full chain when exporting.
 
 #### 2. Create the notarization API key
 
@@ -90,7 +105,7 @@ All five must be present for signing to run. GoReleaser enables the step when `M
 
 | Item | Typical lifetime | What to do |
 |------|------------------|------------|
-| Developer ID Application certificate | ~5 years | Create a new certificate in the Apple portal, export a new `.p12`, update `MACOS_SIGN_P12` and `MACOS_SIGN_PASSWORD`. |
+| Developer ID Application certificate | ~5 years | Create a new certificate in the Apple portal, export a new **full-chain** `.p12` (leaf + G2 intermediate + Apple Root CA), update `MACOS_SIGN_P12` and `MACOS_SIGN_PASSWORD`. |
 | App Store Connect API key | Does not expire, but can be revoked | Create a new key if compromised or lost; update `MACOS_NOTARY_KEY`, `MACOS_NOTARY_KEY_ID`, and optionally `MACOS_NOTARY_ISSUER_ID`. |
 | Apple Developer Program | Annual subscription | Renew membership before it lapses; existing certificates stop working if the account is inactive. |
 
@@ -98,19 +113,31 @@ After updating secrets, the next release on `main` (via semantic-release) will u
 
 ### Verifying a signed release
 
-On a Mac, download a `OliveTin-darwin-*.tar.gz` release artifact and run:
+From any platform (no Mac required), check that quill did **not** emit the broken `certificate root[...]` designated requirement:
+
+```sh
+go install github.com/anchore/quill/cmd/quill@latest
+quill describe OliveTin-darwin-arm64/OliveTin
+```
+
+The requirements line must contain `certificate 1[field.1.2.840.113635.100.6.2.6]`. If it says `certificate root[field.1.2.840.113635.100.6.2.6]`, the `.p12` is missing Apple Root CA — rebuild it and update `MACOS_SIGN_P12`.
+
+On a Mac, also run:
 
 ```sh
 tar -xzf OliveTin-darwin-arm64.tar.gz
+codesign --verify --strict -vvvv OliveTin-darwin-arm64/OliveTin
 spctl -a -vv -t execute OliveTin-darwin-arm64/OliveTin
 ```
 
-A signed and notarized binary should report `accepted` with `source=Notarized Developer ID`.
+`codesign` should report both `valid on disk` and `satisfies its Designated Requirement`. `spctl` should report `accepted` with `source=Notarized Developer ID`.
 
 ### Configuration reference
 
 - GoReleaser: `notarize.macos` in link:https://github.com/OliveTin/OliveTin/blob/main/.goreleaser.yml[`.goreleaser.yml`]
 - CI secrets: link:https://github.com/OliveTin/OliveTin/blob/main/.github/workflows/build-and-release.yml[`.github/workflows/build-and-release.yml`] (`release` step)
+- CI preflight (3-cert P12): link:https://github.com/OliveTin/OliveTin/blob/main/var/macos/verify-macos-sign-p12.sh[`var/macos/verify-macos-sign-p12.sh`]
+- CI post-sign check (designated requirement): link:https://github.com/OliveTin/OliveTin/blob/main/var/macos/verify-signed-darwin.sh[`var/macos/verify-signed-darwin.sh`]
 - link:https://goreleaser.com/customization/notarize/[GoReleaser notarization docs]
 
 ## Windows release signing (SignPath)

+ 10 - 9
var/macos/install.md

@@ -52,14 +52,14 @@ properly, see step 6 — you can install it **as your own user (no root)** or
 
 ---
 
-## 3. Clear the Gatekeeper quarantine
+## 3. Gatekeeper and notarization
 
-Because the binary is downloaded from the internet and is **not notarized by
-Apple**, macOS Gatekeeper will block the first run with a message like
-*"OliveTin can't be opened because Apple cannot check it for malicious
-software."*
+Current release binaries are **Developer ID signed and notarized** by Apple.
+After extract, you should be able to run `./OliveTin` normally.
 
-Remove the quarantine attribute so it will run:
+If Gatekeeper still blocks an older (unsigned) build, or you see a prompt that
+Apple cannot check the binary for malicious software, clear the quarantine
+attribute:
 
 ```sh
 xattr -dr com.apple.quarantine ./OliveTin
@@ -356,9 +356,10 @@ tail -f /usr/local/var/log/olivetin.log
 **"Bad CPU type in executable"** — you downloaded the wrong architecture. Get
 the `arm64` build for Apple Silicon, `amd64` for Intel (see step 1).
 
-**Gatekeeper still blocks it** — re-run the `xattr -dr com.apple.quarantine`
-command in step 3, or approve the app under **System Settings → Privacy &
-Security**.
+**Gatekeeper still blocks it** — for older unsigned builds, re-run
+`xattr -dr com.apple.quarantine ./OliveTin` (see step 3), or approve the app
+under **System Settings → Privacy & Security**. Current signed releases should
+not need this.
 
 **It runs but the page won't load** — check that nothing else is using port
 1337 (`lsof -i :1337`), and that you're browsing to `http://` (not `https://`).

+ 65 - 0
var/macos/verify-macos-sign-p12.sh

@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+# Preflight: ensure MACOS_SIGN_P12 has the full Developer ID chain
+# (leaf + Developer ID G2 + Apple Root CA). Quill embeds an unsatisfiable
+# designated requirement when Apple Root CA is missing — see signing.adoc.
+set -euo pipefail
+
+if [[ -z "${MACOS_SIGN_P12:-}" ]]; then
+  echo "MACOS_SIGN_P12 unset; skipping macOS signing preflight."
+  exit 0
+fi
+
+if [[ -z "${MACOS_SIGN_PASSWORD:-}" ]]; then
+  echo "MACOS_SIGN_PASSWORD is required when MACOS_SIGN_P12 is set." >&2
+  exit 1
+fi
+
+tmpdir="$(mktemp -d)"
+trap 'rm -rf "${tmpdir}"' EXIT
+
+p12_path="${tmpdir}/Certificates.p12"
+# GitHub secrets may include trailing newlines; strip them before decode.
+printf '%s' "${MACOS_SIGN_P12}" | tr -d '\n\r' | base64 -d >"${p12_path}"
+
+pem_out="${tmpdir}/certs.pem"
+extract_p12() {
+  local extra_args=("${@}")
+  openssl pkcs12 -in "${p12_path}" -nodes -passin "pass:${MACOS_SIGN_PASSWORD}" \
+    "${extra_args[@]}" -out "${pem_out}" 2>/dev/null
+}
+
+if ! extract_p12; then
+  # OpenSSL 3 may need -legacy for older P12 exports.
+  extract_p12 -legacy
+fi
+
+cert_dir="${tmpdir}/certs"
+mkdir -p "${cert_dir}"
+awk -v dir="${cert_dir}" '
+  /-----BEGIN CERTIFICATE-----/ { n++; file = sprintf("%s/cert-%02d.pem", dir, n) }
+  n { print > file }
+' "${pem_out}"
+
+cert_count="$(find "${cert_dir}" -name 'cert-*.pem' | wc -l | tr -d ' ')"
+if [[ "${cert_count}" -ne 3 ]]; then
+  echo "MACOS_SIGN_P12 must contain exactly 3 certificates (leaf + G2 + Apple Root CA); found ${cert_count}." >&2
+  echo "Rebuild the .p12 per docs/modules/dev/pages/signing.adoc." >&2
+  exit 1
+fi
+
+found_apple_root=0
+for cert in "${cert_dir}"/cert-*.pem; do
+  subject="$(openssl x509 -in "${cert}" -noout -subject 2>/dev/null || true)"
+  if [[ "${subject}" == *"Apple Root CA"* ]]; then
+    found_apple_root=1
+    break
+  fi
+done
+
+if [[ "${found_apple_root}" -ne 1 ]]; then
+  echo "MACOS_SIGN_P12 is missing Apple Root CA in the certificate chain." >&2
+  echo "Rebuild the .p12 per docs/modules/dev/pages/signing.adoc." >&2
+  exit 1
+fi
+
+echo "MACOS_SIGN_P12 preflight OK (3 certificates, including Apple Root CA)."

+ 72 - 0
var/macos/verify-signed-darwin.sh

@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# Post-sign: fail if quill embedded the broken designated requirement
+# certificate root[field.1.2.840.113635.100.6.2.6] (missing Apple Root in P12).
+set -euo pipefail
+
+if [[ -z "${MACOS_SIGN_P12:-}" ]]; then
+  echo "MACOS_SIGN_P12 unset; skipping darwin signature check."
+  exit 0
+fi
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
+DIST_DIR="${REPO_ROOT}/dist"
+
+BAD_REQ='certificate root\[field\.1\.2\.840\.113635\.100\.6\.2\.6\]'
+GOOD_REQ='certificate 1\[field\.1\.2\.840\.113635\.100\.6\.2\.6\]'
+
+if ! command -v quill >/dev/null 2>&1; then
+  echo "Installing quill..."
+  go install github.com/anchore/quill/cmd/quill@latest
+  export PATH="$(go env GOPATH)/bin:${PATH}"
+fi
+
+shopt -s nullglob
+archives=("${DIST_DIR}"/OliveTin-darwin-*.tar.gz)
+if [[ "${#archives[@]}" -eq 0 ]]; then
+  echo "No OliveTin-darwin-*.tar.gz archives found under ${DIST_DIR}." >&2
+  exit 1
+fi
+
+tmpdir="$(mktemp -d)"
+trap 'rm -rf "${tmpdir}"' EXIT
+
+checked=0
+for archive in "${archives[@]}"; do
+  name="$(basename "${archive}" .tar.gz)"
+  extract_dir="${tmpdir}/${name}"
+  mkdir -p "${extract_dir}"
+  tar -xzf "${archive}" -C "${extract_dir}"
+
+  # Prefer the top-level binary; archives also ship helper scripts named OliveTin.
+  binary="${extract_dir}/${name}/OliveTin"
+  if [[ ! -f "${binary}" ]]; then
+    binary="$(find "${extract_dir}" -type f -path "*/OliveTin" ! -path "*/var/*" | head -n 1)"
+  fi
+  if [[ -z "${binary}" || ! -f "${binary}" ]]; then
+    echo "OliveTin binary not found inside ${archive}." >&2
+    exit 1
+  fi
+  if ! file "${binary}" | grep -qi 'Mach-O'; then
+    echo "Expected a Mach-O binary at ${binary}, got: $(file "${binary}")" >&2
+    exit 1
+  fi
+
+
+  echo "Checking designated requirement in ${archive}..."
+  describe_out="$(quill describe "${binary}")"
+  if echo "${describe_out}" | grep -qE "${BAD_REQ}"; then
+    echo "Broken designated requirement in ${archive}:" >&2
+    echo "  found certificate root[field.1.2.840.113635.100.6.2.6]" >&2
+    echo "MACOS_SIGN_P12 is missing Apple Root CA. Rebuild per docs/modules/dev/pages/signing.adoc." >&2
+    exit 1
+  fi
+  if ! echo "${describe_out}" | grep -qE "${GOOD_REQ}"; then
+    echo "Expected designated requirement with certificate 1[...] not found in ${archive}." >&2
+    echo "${describe_out}" >&2
+    exit 1
+  fi
+  checked=$((checked + 1))
+done
+
+echo "Darwin signature check OK (${checked} archive(s); designated requirement uses certificate 1[...])."

+ 3 - 0
var/windows/goreleaser-release-with-msi.sh

@@ -51,3 +51,6 @@ done
 wait_for_stable_file "${ZIP_PATH}"
 "${SCRIPT_DIR}/build-msi.sh"
 wait "${goreleaser_pid}"
+
+# Fail the release job if macOS signing produced an unsatisfiable designated requirement.
+"${REPO_ROOT}/var/macos/verify-signed-darwin.sh"