Mike 3 месяцев назад
Родитель
Сommit
8076d10833
5 измененных файлов с 278 добавлено и 9 удалено
  1. 4 0
      .goreleaser.yaml
  2. 52 0
      Makefile
  3. 133 0
      cmd/sign_server/main.go
  4. 7 9
      docs/RELEASE.md
  5. 82 0
      scripts/sign_windows.sh

+ 4 - 0
.goreleaser.yaml

@@ -36,6 +36,10 @@ builds:
     main: ./cmd/server
     env:
       - CGO_ENABLED=0
+    hooks:
+      post:
+        - cmd: bash ./scripts/sign_windows.sh "{{ .Path }}"
+          output: true
 
 archives:
   - id: linux

+ 52 - 0
Makefile

@@ -3,9 +3,22 @@
 ################################################################################
 
 DOCKER_IMAGE_TAG_GO_RELEASER := goreleaser/goreleaser:v2.13.1
+# Docker builds cannot load a macOS USB PKCS#11 token. Options: (1) SKIP_CODE_SIGN=1 and skip;
+# (2) SIGN_HTTP_URL=http://host.docker.internal:8765 plus `make sign-server` on the host to
+# sign via HTTP; (3) run release-sign with host goreleaser (no Docker).
+SKIP_CODE_SIGN ?= 1
+# When set (e.g. http://host.docker.internal:8765), GoReleaser in Docker calls the host
+# sign_server to run PKCS#11 signing on the same bind-mounted dist/ tree.
+SIGN_HTTP_URL ?=
+SIGN_SERVER_TOKEN ?=
+GORELEASER ?= goreleaser
+
 DOCKER_RUN_GO_RELEASER := @docker run \
 	--env CGO_ENABLED=0 \
 	--env GITHUB_TOKEN=$(GITHUB_TOKEN) \
+	--env SKIP_CODE_SIGN=$(SKIP_CODE_SIGN) \
+	--env SIGN_HTTP_URL=$(SIGN_HTTP_URL) \
+	--env SIGN_SERVER_TOKEN=$(SIGN_SERVER_TOKEN) \
 	--rm \
 	--volume `pwd`:/go/src/open-oscar-server \
 	--workdir /go/src/open-oscar-server \
@@ -29,6 +42,45 @@ release: ## Run a clean, full GoReleaser run (publish + validate)
 release-dry-run: ## GoReleaser dry-run (skips validate & publish)
 	$(DOCKER_RUN_GO_RELEASER) --clean --skip=validate --skip=publish
 
+SIGN_SERVER_PORT ?= 8765
+
+.PHONY: sign-server
+sign-server: ## Local HTTP signer for Windows PE (run before Docker release if using SIGN_HTTP_URL)
+	go run ./cmd/sign_server
+
+.PHONY: sign-server-stop
+sign-server-stop: ## Stop whatever is listening on SIGN_SERVER_PORT (usually a leftover sign_server)
+	-@kill $$(lsof -t -iTCP:$(SIGN_SERVER_PORT) -sTCP:LISTEN) 2>/dev/null || true
+
+# Default URL for GoReleaser-in-Docker → host signing (Docker Desktop Mac/Win).
+# On Linux Docker, use host.docker.internal:8765 only if you add
+# --add-host=host.docker.internal:host-gateway to the docker run (or set SIGN_DOCKER_URL).
+SIGN_DOCKER_URL ?= http://host.docker.internal:8765
+
+.PHONY: release-dry-run-sign-docker
+release-dry-run-sign-docker: ## Dry-run in Docker; Windows Authenticode via host sign_server (run `make sign-server` first)
+	@$(MAKE) release-dry-run SIGN_HTTP_URL=$(SIGN_DOCKER_URL)
+
+.PHONY: release-sign-docker
+release-sign-docker: ## Full release in Docker; Windows Authenticode via host sign_server (run `make sign-server` first)
+	@$(MAKE) release SIGN_HTTP_URL=$(SIGN_DOCKER_URL)
+
+.PHONY: release-dry-run-nosign
+release-dry-run-nosign: ## GoReleaser dry-run on host without Windows Authenticode
+	SKIP_CODE_SIGN=1 $(GORELEASER) --clean --skip=validate --skip=publish
+
+.PHONY: release-nosign
+release-nosign: ## Full GoReleaser on host without Windows Authenticode
+	SKIP_CODE_SIGN=1 $(GORELEASER) --clean
+
+.PHONY: release-dry-run-sign
+release-dry-run-sign: ## GoReleaser dry-run on host with Windows signing (needs $(GORELEASER), PKCS#11 env)
+	SKIP_CODE_SIGN=0 $(GORELEASER) --clean --skip=validate --skip=publish
+
+.PHONY: release-sign
+release-sign: ## Full GoReleaser on host with Windows signing (needs $(GORELEASER), PKCS#11 env)
+	SKIP_CODE_SIGN=0 $(GORELEASER) --clean
+
 .PHONY: docker-image-ras
 docker-image-ras: ## Build Open OSCAR Server image
 	docker build -t ras:latest -f Dockerfile .

+ 133 - 0
cmd/sign_server/main.go

@@ -0,0 +1,133 @@
+// Command sign_server runs a small HTTP server on the host that signs Windows PEs
+// with scripts/sign_windows.sh (PKCS#11 / USB token). GoReleaser inside Docker can call
+// http://host.docker.internal:<port>/sign so the binary is signed on the host filesystem
+// (same volume mount as the container).
+package main
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strings"
+)
+
+func main() {
+	addr := os.Getenv("SIGN_SERVER_ADDR")
+	if addr == "" {
+		addr = "127.0.0.1:8765"
+	}
+	repoRoot := os.Getenv("SIGN_SERVER_REPO_ROOT")
+	if repoRoot == "" {
+		wd, err := os.Getwd()
+		if err != nil {
+			log.Fatal(err)
+		}
+		repoRoot = wd
+	}
+	var err error
+	repoRoot, err = filepath.Abs(repoRoot)
+	if err != nil {
+		log.Fatal(err)
+	}
+	token := os.Getenv("SIGN_SERVER_TOKEN")
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/sign", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodPost {
+			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
+			return
+		}
+		if token != "" {
+			got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
+			if got != token {
+				http.Error(w, "unauthorized", http.StatusUnauthorized)
+				return
+			}
+		}
+		var body struct {
+			Path string `json:"path"`
+		}
+		if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+			http.Error(w, "bad json", http.StatusBadRequest)
+			return
+		}
+		abs, err := safeArtifactPath(repoRoot, body.Path)
+		if err != nil {
+			http.Error(w, err.Error(), http.StatusBadRequest)
+			return
+		}
+		script := filepath.Join(repoRoot, "scripts", "sign_windows.sh")
+		cmd := exec.Command("bash", script, abs)
+		cmd.Dir = repoRoot
+		cmd.Env = signEnv(os.Environ())
+		cmd.Stdout = os.Stdout
+		cmd.Stderr = os.Stderr
+		if err := cmd.Run(); err != nil {
+			log.Printf("sign failed: %v", err)
+			http.Error(w, "sign failed", http.StatusInternalServerError)
+			return
+		}
+		w.WriteHeader(http.StatusOK)
+	})
+	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		_, _ = w.Write([]byte("ok"))
+	})
+
+	log.Printf("sign_server listening on http://%s (repo %s)", addr, repoRoot)
+	if err := http.ListenAndServe(addr, mux); err != nil {
+		if strings.Contains(err.Error(), "address already in use") {
+			log.Fatalf("%v: another sign_server may still be running; try `make sign-server-stop` or set SIGN_SERVER_ADDR to a free port", err)
+		}
+		log.Fatal(err)
+	}
+}
+
+func signEnv(environ []string) []string {
+	out := make([]string, 0, len(environ)+2)
+	for _, e := range environ {
+		if strings.HasPrefix(e, "SIGN_HTTP_URL=") {
+			continue
+		}
+		if strings.HasPrefix(e, "SKIP_CODE_SIGN=") {
+			continue
+		}
+		out = append(out, e)
+	}
+	out = append(out, "SKIP_CODE_SIGN=0", "SIGN_HTTP_URL=")
+	return out
+}
+
+func safeArtifactPath(repoRoot, rel string) (string, error) {
+	rel = filepath.Clean(filepath.FromSlash(rel))
+	if rel == "." || strings.HasPrefix(rel, "..") {
+		return "", errors.New("invalid path")
+	}
+	distPrefix := "dist" + string(os.PathSeparator)
+	if !strings.HasPrefix(rel, distPrefix) {
+		return "", errors.New("path must be under dist/")
+	}
+	if filepath.Ext(rel) != ".exe" {
+		return "", errors.New("path must end with .exe")
+	}
+	rootClean := filepath.Clean(repoRoot)
+	abs := filepath.Join(rootClean, rel)
+	absClean := filepath.Clean(abs)
+	relTo, err := filepath.Rel(rootClean, absClean)
+	if err != nil || strings.HasPrefix(relTo, "..") {
+		return "", errors.New("path escapes repo root")
+	}
+	st, err := os.Stat(absClean)
+	if err != nil {
+		return "", fmt.Errorf("stat: %w", err)
+	}
+	if st.IsDir() {
+		return "", errors.New("not a file")
+	}
+	return absClean, nil
+}

+ 7 - 9
docs/RELEASE.md

@@ -51,14 +51,12 @@ The following is the procedure that builds Open OSCAR Server and uploads the bui
    Now run the release process. Once its complete, a private draft [release](https://github.com/mk6i/open-oscar-server/releases)
    should appear with attached build artifacts.
 
-    ```shell
-   make release
-    ```
-
-5. **Sign It!**
-
-   Download the Windows release, sign it, and re-upload the `.zip` to the draft release created in the previous step.
+   Run the following two commands in separate terminals.
 
-6. **Publish It**
+   ```shell
+   make sign-server    # terminal 1, with SIGN_* env for your token
+   make release-sign-docker   # terminal 2
+   ```
 
-   Publish the draft release.
+5. **Publish It**
+   Publish the draft release.

+ 82 - 0
scripts/sign_windows.sh

@@ -0,0 +1,82 @@
+#!/usr/bin/env bash
+# Authenticode-sign the Windows PE produced by GoReleaser (osslsigncode + PKCS#11 token).
+#
+# Usage: sign_windows.sh <artifact.exe>
+#
+# Remote signing (e.g. GoReleaser in Docker → host USB token):
+#   Set SIGN_HTTP_URL to the host signing server, e.g. http://host.docker.internal:8765
+#   The server must see the same repo mount (path under .../open-oscar-server/...).
+#
+# When SKIP_CODE_SIGN=1, exits 0 without signing (unless SIGN_HTTP_URL is set).
+#
+# For local signing, set:
+#   SIGN_PKCS11_ENGINE     - path to libpkcs11 engine (e.g. Homebrew libp11)
+#   SIGN_PKCS11_MODULE     - path to vendor PKCS#11 module (.dylib / .so)
+#   SIGN_CERT_PEM          - path to signer certificate chain PEM
+#   SIGN_KEY_ID            - PKCS#11 key identifier (hex)
+#   SIGN_PASSWORD          - token/PIN (optional if not required)
+# Optional:
+#   SIGN_TIMESTAMP_URL     - default http://time.certum.pl/
+#   SIGN_SERVER_TOKEN      - Bearer token if sign_server uses SIGN_SERVER_TOKEN
+#
+# Example (match your osslsigncode flags; do not commit secrets):
+#   export SIGN_PKCS11_ENGINE=/opt/homebrew/.../libpkcs11.dylib
+#   export SIGN_PKCS11_MODULE=/usr/local/lib/crypto3PKCS/...dylib
+#   export SIGN_CERT_PEM=/path/to/chain.pem
+#   export SIGN_KEY_ID=<pkcs11-key-id-hex>
+#   export SIGN_PASSWORD='...'
+#   make release-sign
+
+set -euo pipefail
+
+artifact="${1:?artifact path required}"
+
+if [[ -n "${SIGN_HTTP_URL:-}" ]]; then
+	rel="${artifact#*/open-oscar-server/}"
+	if [[ "$rel" == "$artifact" ]]; then
+		echo "cannot derive repo-relative path from ${artifact} (expected .../open-oscar-server/...)" >&2
+		exit 1
+	fi
+	url="${SIGN_HTTP_URL%/}/sign"
+	payload=$(printf '{"path":"%s"}' "${rel}")
+	hdr=()
+	if [[ -n "${SIGN_SERVER_TOKEN:-}" ]]; then
+		hdr=( -H "Authorization: Bearer ${SIGN_SERVER_TOKEN}" )
+	fi
+	curl -fsS "${hdr[@]}" -X POST -H "Content-Type: application/json" -d "${payload}" "${url}"
+	exit 0
+fi
+
+if [[ "${SKIP_CODE_SIGN:-}" == "1" ]]; then
+	echo "Skipping Windows Authenticode (SKIP_CODE_SIGN=1)"
+	exit 0
+fi
+
+: "${SIGN_PKCS11_ENGINE:?set SIGN_PKCS11_ENGINE}"
+: "${SIGN_PKCS11_MODULE:?set SIGN_PKCS11_MODULE}"
+: "${SIGN_CERT_PEM:?set SIGN_CERT_PEM}"
+: "${SIGN_KEY_ID:?set SIGN_KEY_ID}"
+
+timestamp_url="${SIGN_TIMESTAMP_URL:-http://time.certum.pl/}"
+
+tmp="${artifact}.~signing~"
+rm -f "${tmp}"
+
+pass_args=()
+if [[ -n "${SIGN_PASSWORD:-}" ]]; then
+	pass_args+=( -pass "${SIGN_PASSWORD}" )
+fi
+
+osslsigncode sign \
+	-verbose \
+	-pkcs11engine "${SIGN_PKCS11_ENGINE}" \
+	-pkcs11module "${SIGN_PKCS11_MODULE}" \
+	-certs "${SIGN_CERT_PEM}" \
+	-key "${SIGN_KEY_ID}" \
+	"${pass_args[@]}" \
+	-h sha256 \
+	-t "${timestamp_url}" \
+	-in "${artifact}" \
+	-out "${tmp}"
+
+mv "${tmp}" "${artifact}"