feat(build): add Windows signtool signing
This commit is contained in:
parent
efdc252462
commit
72bb94e355
7 changed files with 153 additions and 33 deletions
|
|
@ -160,7 +160,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
}
|
||||
}
|
||||
|
||||
// Sign macOS binaries if enabled
|
||||
// Sign binaries if enabled.
|
||||
signCfg := buildConfig.Sign
|
||||
if notarize {
|
||||
signCfg.MacOS.Notarize = true
|
||||
|
|
@ -169,7 +169,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
signCfg.Enabled = false
|
||||
}
|
||||
|
||||
if signCfg.Enabled && runtime.GOOS == "darwin" {
|
||||
if signCfg.Enabled && (runtime.GOOS == "darwin" || runtime.GOOS == "windows") {
|
||||
if verbose && !ciMode {
|
||||
cli.Blank()
|
||||
cli.Print("%s %s\n", buildHeaderStyle.Render(i18n.T("cmd.build.label.sign")), i18n.T("cmd.build.signing_binaries"))
|
||||
|
|
@ -188,7 +188,7 @@ func runProjectBuild(ctx context.Context, buildType string, ciMode bool, targets
|
|||
return err
|
||||
}
|
||||
|
||||
if signCfg.MacOS.Notarize {
|
||||
if runtime.GOOS == "darwin" && signCfg.MacOS.Notarize {
|
||||
if err := signing.NotarizeBinaries(ctx, filesystem, signCfg, signingArtifacts); err != nil {
|
||||
if !ciMode {
|
||||
cli.Print("%s %s: %v\n", buildErrorStyle.Render(i18n.T("common.label.error")), i18n.T("cmd.build.error.notarization_failed"), err)
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ Three implementations:
|
|||
|
||||
- **GPGSigner** -- `gpg --detach-sign --armor --local-user {key}`. Produces `.asc` files.
|
||||
- **MacOSSigner** -- `codesign --sign {identity} --timestamp --options runtime --force`. Notarisation via `xcrun notarytool submit --wait` then `xcrun stapler staple`.
|
||||
- **WindowsSigner** -- Placeholder (returns `Available() == false`).
|
||||
- **WindowsSigner** -- Uses `signtool` on Windows when a certificate is configured.
|
||||
|
||||
Configuration supports `$ENV` expansion in all credential fields, so secrets can come from environment variables without being written to YAML.
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ description: Build system, release pipeline, and SDK generation for the Core eco
|
|||
|
||||
- **Auto-detecting builders** for Go, Wails, Node, PHP, Docker, LinuxKit, C++, and Taskfile projects
|
||||
- **Cross-compilation** with per-target archiving (tar.gz, tar.xz, zip) and SHA-256 checksums
|
||||
- **Code signing** -- macOS codesign with notarisation, GPG detached signatures, Windows signtool (placeholder)
|
||||
- **Code signing** -- macOS codesign with notarisation, GPG detached signatures, Windows signtool
|
||||
- **Release automation** -- semantic versioning from git tags, conventional-commit changelogs, multi-target publishing
|
||||
- **SDK generation** -- OpenAPI spec diffing for breaking-change detection, code generation for TypeScript, Python, Go, and PHP
|
||||
- **CLI integration** -- registers `core build`, `core ci`, and `core sdk` commands via the Core CLI framework
|
||||
|
|
|
|||
|
|
@ -19,8 +19,9 @@ type Artifact struct {
|
|||
Arch string
|
||||
}
|
||||
|
||||
// SignBinaries signs macOS binaries in the artifacts list.
|
||||
// Only signs darwin binaries when running on macOS with a configured identity.
|
||||
// SignBinaries signs binaries for the current host OS in the artifacts list.
|
||||
// On macOS it signs darwin artifacts with codesign; on Windows it signs windows
|
||||
// artifacts with signtool when the relevant credentials are configured.
|
||||
//
|
||||
// err := signing.SignBinaries(ctx, io.Local, cfg, artifacts)
|
||||
func SignBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifacts []Artifact) error {
|
||||
|
|
@ -28,28 +29,25 @@ func SignBinaries(ctx context.Context, fs io.Medium, cfg SignConfig, artifacts [
|
|||
return nil
|
||||
}
|
||||
|
||||
// Only sign on macOS
|
||||
if runtime.GOOS != "darwin" {
|
||||
var signer Signer
|
||||
var targetOS string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
signer = NewMacOSSigner(cfg.MacOS)
|
||||
targetOS = "darwin"
|
||||
case "windows":
|
||||
signer = NewWindowsSigner(cfg.Windows)
|
||||
targetOS = "windows"
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
signer := NewMacOSSigner(cfg.MacOS)
|
||||
if !signer.Available() {
|
||||
return nil // Silently skip if not configured
|
||||
}
|
||||
|
||||
for _, artifact := range artifacts {
|
||||
if artifact.OS != "darwin" {
|
||||
continue
|
||||
}
|
||||
|
||||
core.Print(nil, " Signing %s...", artifact.Path)
|
||||
if err := signer.Sign(ctx, fs, artifact.Path); err != nil {
|
||||
return coreerr.E("signing.SignBinaries", "failed to sign "+artifact.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return signArtifactsWithSigner(ctx, fs, signer, targetOS, artifacts)
|
||||
}
|
||||
|
||||
// NotarizeBinaries notarizes macOS binaries if enabled.
|
||||
|
|
@ -103,3 +101,20 @@ func SignChecksums(ctx context.Context, fs io.Medium, cfg SignConfig, checksumFi
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func signArtifactsWithSigner(ctx context.Context, fs io.Medium, signer Signer, targetOS string, artifacts []Artifact) error {
|
||||
_ = fs
|
||||
|
||||
for _, artifact := range artifacts {
|
||||
if artifact.OS != targetOS {
|
||||
continue
|
||||
}
|
||||
|
||||
core.Print(nil, " Signing %s...", artifact.Path)
|
||||
if err := signer.Sign(ctx, fs, artifact.Path); err != nil {
|
||||
return coreerr.E("signing.SignBinaries", "failed to sign "+artifact.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ type MacOSConfig struct {
|
|||
AppPassword string `yaml:"app_password"` // App-specific password
|
||||
}
|
||||
|
||||
// WindowsConfig holds Windows signtool configuration (placeholder).
|
||||
// WindowsConfig holds Windows signtool configuration.
|
||||
//
|
||||
// cfg := signing.WindowsConfig{Certificate: "cert.pfx", Password: "secret"}
|
||||
type WindowsConfig struct {
|
||||
|
|
@ -72,6 +72,10 @@ func DefaultSignConfig() SignConfig {
|
|||
TeamID: core.Env("APPLE_TEAM_ID"),
|
||||
AppPassword: core.Env("APPLE_APP_PASSWORD"),
|
||||
},
|
||||
Windows: WindowsConfig{
|
||||
Certificate: core.Env("SIGNTOOL_CERTIFICATE"),
|
||||
Password: core.Env("SIGNTOOL_PASSWORD"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"dappco.re/go/core/io"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestSigning_SignBinariesSkipsNonDarwin_Good(t *testing.T) {
|
||||
|
|
@ -155,10 +156,17 @@ func TestSigning_SignConfigExpandEnv_Good(t *testing.T) {
|
|||
|
||||
func TestSigning_WindowsSigner_Good(t *testing.T) {
|
||||
fs := io.Local
|
||||
s := NewWindowsSigner(WindowsConfig{})
|
||||
s := NewWindowsSigner(WindowsConfig{Certificate: "cert.pfx"})
|
||||
assert.Equal(t, "signtool", s.Name())
|
||||
assert.False(t, s.Available())
|
||||
assert.NoError(t, s.Sign(context.Background(), fs, "test.exe"))
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
assert.False(t, s.Available())
|
||||
assert.Error(t, s.Sign(context.Background(), fs, "test.exe"))
|
||||
return
|
||||
}
|
||||
|
||||
// On Windows, availability depends on the SDK toolchain being installed.
|
||||
_ = s.Available()
|
||||
}
|
||||
|
||||
// mockSigner is a test double that records calls to Sign.
|
||||
|
|
@ -230,6 +238,38 @@ func TestSigning_SignBinariesMockSigner_Good(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestSigning_signArtifactsWithSigner_Good(t *testing.T) {
|
||||
signer := &mockSigner{name: "mock", available: true}
|
||||
artifacts := []Artifact{
|
||||
{Path: "/dist/linux_amd64/myapp", OS: "linux", Arch: "amd64"},
|
||||
{Path: "/dist/windows_amd64/myapp.exe", OS: "windows", Arch: "amd64"},
|
||||
{Path: "/dist/windows_arm64/myapp.exe", OS: "windows", Arch: "arm64"},
|
||||
}
|
||||
|
||||
err := signArtifactsWithSigner(context.Background(), io.Local, signer, "windows", artifacts)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"/dist/windows_amd64/myapp.exe", "/dist/windows_arm64/myapp.exe"}, signer.signedPaths)
|
||||
}
|
||||
|
||||
func TestSigning_ResolveSigntoolCli_Good(t *testing.T) {
|
||||
fallbackDir := t.TempDir()
|
||||
fallbackPath := fallbackDir + "/signtool.exe"
|
||||
require.NoError(t, io.Local.Write(fallbackPath, "#!/bin/sh\nexit 0\n"))
|
||||
t.Setenv("PATH", "")
|
||||
|
||||
command, err := resolveSigntoolCli(fallbackPath)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, fallbackPath, command)
|
||||
}
|
||||
|
||||
func TestSigning_ResolveSigntoolCli_Bad(t *testing.T) {
|
||||
t.Setenv("PATH", "")
|
||||
|
||||
_, err := resolveSigntoolCli(t.TempDir() + "/missing-signtool.exe")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "signtool tool not found")
|
||||
}
|
||||
|
||||
func TestSigning_SignChecksumsMockSigner_Good(t *testing.T) {
|
||||
t.Run("skips when GPG key is empty", func(t *testing.T) {
|
||||
cfg := SignConfig{
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ package signing
|
|||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
|
||||
"dappco.re/go/core/build/internal/ax"
|
||||
"dappco.re/go/core/io"
|
||||
coreerr "dappco.re/go/core/log"
|
||||
)
|
||||
|
||||
// WindowsSigner signs binaries using Windows signtool (placeholder).
|
||||
// WindowsSigner signs binaries using Windows signtool.
|
||||
//
|
||||
// s := signing.NewWindowsSigner(cfg.Windows)
|
||||
type WindowsSigner struct {
|
||||
|
|
@ -30,17 +33,75 @@ func (s *WindowsSigner) Name() string {
|
|||
return "signtool"
|
||||
}
|
||||
|
||||
// Available returns false (not yet implemented).
|
||||
// Available checks if running on Windows with signtool and certificate configured.
|
||||
//
|
||||
// ok := s.Available() // → false (placeholder)
|
||||
// ok := s.Available() // → true if on Windows with certificate configured
|
||||
func (s *WindowsSigner) Available() bool {
|
||||
return false
|
||||
if runtime.GOOS != "windows" {
|
||||
return false
|
||||
}
|
||||
if s.config.Certificate == "" {
|
||||
return false
|
||||
}
|
||||
_, err := resolveSigntoolCli()
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Sign is a placeholder that does nothing.
|
||||
// Sign signs a binary using signtool and a PFX certificate.
|
||||
//
|
||||
// err := s.Sign(ctx, io.Local, "dist/myapp.exe") // no-op until implemented
|
||||
// err := s.Sign(ctx, io.Local, "dist/myapp.exe")
|
||||
func (s *WindowsSigner) Sign(ctx context.Context, fs io.Medium, binary string) error {
|
||||
// TODO: Implement Windows signing
|
||||
_ = fs
|
||||
|
||||
if !s.Available() {
|
||||
if runtime.GOOS != "windows" {
|
||||
return coreerr.E("signtool.Sign", "signtool is only available on Windows", nil)
|
||||
}
|
||||
if s.config.Certificate == "" {
|
||||
return coreerr.E("signtool.Sign", "signtool certificate not configured", nil)
|
||||
}
|
||||
return coreerr.E("signtool.Sign", "signtool tool not found in PATH", nil)
|
||||
}
|
||||
|
||||
signtoolCommand, err := resolveSigntoolCli()
|
||||
if err != nil {
|
||||
return coreerr.E("signtool.Sign", "signtool tool not found in PATH", err)
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"sign",
|
||||
"/f", s.config.Certificate,
|
||||
"/fd", "sha256",
|
||||
"/tr", "http://timestamp.digicert.com",
|
||||
"/td", "sha256",
|
||||
}
|
||||
if s.config.Password != "" {
|
||||
args = append(args, "/p", s.config.Password)
|
||||
}
|
||||
args = append(args, binary)
|
||||
|
||||
output, err := ax.CombinedOutput(ctx, "", nil, signtoolCommand, args...)
|
||||
if err != nil {
|
||||
return coreerr.E("signtool.Sign", output, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveSigntoolCli(paths ...string) (string, error) {
|
||||
if len(paths) == 0 {
|
||||
paths = []string{
|
||||
`C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64\\signtool.exe`,
|
||||
`C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x86\\signtool.exe`,
|
||||
`C:\\Program Files\\Windows Kits\\10\\bin\\x64\\signtool.exe`,
|
||||
`C:\\Program Files\\Windows Kits\\10\\bin\\x86\\signtool.exe`,
|
||||
}
|
||||
}
|
||||
|
||||
command, err := ax.ResolveCommand("signtool", paths...)
|
||||
if err != nil {
|
||||
return "", coreerr.E("signtool.resolveSigntoolCli", "signtool tool not found. Install the Windows SDK.", err)
|
||||
}
|
||||
|
||||
return command, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue