main.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. // Command sign_server runs a small HTTP server on the host that signs Windows PEs
  2. // with scripts/sign_windows.sh (PKCS#11 / USB token). GoReleaser inside Docker can call
  3. // http://host.docker.internal:<port>/sign so the binary is signed on the host filesystem
  4. // (same volume mount as the container).
  5. package main
  6. import (
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "log"
  11. "net/http"
  12. "os"
  13. "os/exec"
  14. "path/filepath"
  15. "strings"
  16. )
  17. func main() {
  18. addr := os.Getenv("SIGN_SERVER_ADDR")
  19. if addr == "" {
  20. addr = "127.0.0.1:8765"
  21. }
  22. repoRoot := os.Getenv("SIGN_SERVER_REPO_ROOT")
  23. if repoRoot == "" {
  24. wd, err := os.Getwd()
  25. if err != nil {
  26. log.Fatal(err)
  27. }
  28. repoRoot = wd
  29. }
  30. var err error
  31. repoRoot, err = filepath.Abs(repoRoot)
  32. if err != nil {
  33. log.Fatal(err)
  34. }
  35. token := os.Getenv("SIGN_SERVER_TOKEN")
  36. mux := http.NewServeMux()
  37. mux.HandleFunc("/sign", func(w http.ResponseWriter, r *http.Request) {
  38. if r.Method != http.MethodPost {
  39. http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
  40. return
  41. }
  42. if token != "" {
  43. got := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
  44. if got != token {
  45. http.Error(w, "unauthorized", http.StatusUnauthorized)
  46. return
  47. }
  48. }
  49. var body struct {
  50. Path string `json:"path"`
  51. }
  52. if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
  53. http.Error(w, "bad json", http.StatusBadRequest)
  54. return
  55. }
  56. abs, err := safeArtifactPath(repoRoot, body.Path)
  57. if err != nil {
  58. http.Error(w, err.Error(), http.StatusBadRequest)
  59. return
  60. }
  61. script := filepath.Join(repoRoot, "scripts", "sign_windows.sh")
  62. cmd := exec.Command("bash", script, abs)
  63. cmd.Dir = repoRoot
  64. cmd.Env = signEnv(os.Environ())
  65. cmd.Stdout = os.Stdout
  66. cmd.Stderr = os.Stderr
  67. if err := cmd.Run(); err != nil {
  68. log.Printf("sign failed: %v", err)
  69. http.Error(w, "sign failed", http.StatusInternalServerError)
  70. return
  71. }
  72. w.WriteHeader(http.StatusOK)
  73. })
  74. mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
  75. w.WriteHeader(http.StatusOK)
  76. _, _ = w.Write([]byte("ok"))
  77. })
  78. log.Printf("sign_server listening on http://%s (repo %s)", addr, repoRoot)
  79. if err := http.ListenAndServe(addr, mux); err != nil {
  80. if strings.Contains(err.Error(), "address already in use") {
  81. 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)
  82. }
  83. log.Fatal(err)
  84. }
  85. }
  86. func signEnv(environ []string) []string {
  87. out := make([]string, 0, len(environ)+2)
  88. for _, e := range environ {
  89. if strings.HasPrefix(e, "SIGN_HTTP_URL=") {
  90. continue
  91. }
  92. if strings.HasPrefix(e, "SKIP_CODE_SIGN=") {
  93. continue
  94. }
  95. out = append(out, e)
  96. }
  97. out = append(out, "SKIP_CODE_SIGN=0", "SIGN_HTTP_URL=")
  98. return out
  99. }
  100. func safeArtifactPath(repoRoot, rel string) (string, error) {
  101. rel = filepath.Clean(filepath.FromSlash(rel))
  102. if rel == "." || strings.HasPrefix(rel, "..") {
  103. return "", errors.New("invalid path")
  104. }
  105. distPrefix := "dist" + string(os.PathSeparator)
  106. if !strings.HasPrefix(rel, distPrefix) {
  107. return "", errors.New("path must be under dist/")
  108. }
  109. if filepath.Ext(rel) != ".exe" {
  110. return "", errors.New("path must end with .exe")
  111. }
  112. rootClean := filepath.Clean(repoRoot)
  113. abs := filepath.Join(rootClean, rel)
  114. absClean := filepath.Clean(abs)
  115. relTo, err := filepath.Rel(rootClean, absClean)
  116. if err != nil || strings.HasPrefix(relTo, "..") {
  117. return "", errors.New("path escapes repo root")
  118. }
  119. st, err := os.Stat(absClean)
  120. if err != nil {
  121. return "", fmt.Errorf("stat: %w", err)
  122. }
  123. if st.IsDir() {
  124. return "", errors.New("not a file")
  125. }
  126. return absClean, nil
  127. }