Add temporary keychain lifecycle for codesigning

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leon Mika 2026-05-02 10:13:37 +10:00
parent 0d12173ff9
commit eefce13a7c
2 changed files with 133 additions and 0 deletions

View file

@ -0,0 +1,60 @@
package codesign
import (
"context"
"fmt"
"github.com/leonmika/wails-release/internal/runner"
)
// Keychain identifies a temporary keychain we created.
type Keychain struct {
Path string
Password string
}
// CreateKeychain creates a new keychain at path with the given password
// and unlocks it. The keychain's auto-lock timeout is set to 6 hours so
// it does not relock during a long notarization.
func CreateKeychain(ctx context.Context, r runner.Runner, path, password string) (*Keychain, error) {
steps := [][]string{
{"create-keychain", "-p", password, path},
{"set-keychain-settings", "-lut", "21600", path},
{"unlock-keychain", "-p", password, path},
}
for _, args := range steps {
if _, err := r.Run(ctx, runner.Spec{Name: "security", Args: args}); err != nil {
return nil, fmt.Errorf("security %s: %w", args[0], err)
}
}
return &Keychain{Path: path, Password: password}, nil
}
// ImportP12 imports the .p12 at certPath into kc using certPassword and
// authorises codesign to use the resulting key without prompting.
func ImportP12(ctx context.Context, r runner.Runner, kc Keychain, certPath, certPassword string) error {
if _, err := r.Run(ctx, runner.Spec{
Name: "security",
Args: []string{"import", certPath, "-k", kc.Path, "-P", certPassword, "-T", "/usr/bin/codesign"},
}); err != nil {
return fmt.Errorf("security import: %w", err)
}
if _, err := r.Run(ctx, runner.Spec{
Name: "security",
Args: []string{"set-key-partition-list", "-S", "apple-tool:,apple:,codesign:", "-s", "-k", kc.Password, kc.Path},
}); err != nil {
return fmt.Errorf("security set-key-partition-list: %w", err)
}
return nil
}
// DeleteKeychain removes the keychain. Safe to call from cleanup.
func DeleteKeychain(ctx context.Context, r runner.Runner, kc Keychain) error {
if _, err := r.Run(ctx, runner.Spec{
Name: "security",
Args: []string{"delete-keychain", kc.Path},
}); err != nil {
return fmt.Errorf("security delete-keychain: %w", err)
}
return nil
}

View file

@ -0,0 +1,73 @@
package codesign_test
import (
"context"
"reflect"
"testing"
"github.com/leonmika/wails-release/internal/codesign"
"github.com/leonmika/wails-release/internal/runner"
)
func TestCreateKeychain_BuildsCorrectArgs(t *testing.T) {
f := &runner.Fake{}
f.On("security", nil).Return(nil, nil)
kc, err := codesign.CreateKeychain(context.Background(), f, "/tmp/foo.keychain", "pw")
if err != nil {
t.Fatalf("unexpected: %v", err)
}
if kc.Path != "/tmp/foo.keychain" {
t.Fatalf("kc.Path got %q want /tmp/foo.keychain", kc.Path)
}
want := [][]string{
{"create-keychain", "-p", "pw", "/tmp/foo.keychain"},
{"set-keychain-settings", "-lut", "21600", "/tmp/foo.keychain"},
{"unlock-keychain", "-p", "pw", "/tmp/foo.keychain"},
}
for i, w := range want {
if !reflect.DeepEqual(f.Calls[i].Args, w) {
t.Fatalf("call %d: got %v want %v", i, f.Calls[i].Args, w)
}
}
}
func TestImportP12_BuildsCorrectArgs(t *testing.T) {
f := &runner.Fake{}
f.On("security", nil).Return(nil, nil)
err := codesign.ImportP12(context.Background(), f, codesign.Keychain{Path: "/tmp/k", Password: "pw"}, "/tmp/c.p12", "certpw")
if err != nil {
t.Fatalf("unexpected: %v", err)
}
want := []string{
"import", "/tmp/c.p12", "-k", "/tmp/k", "-P", "certpw",
"-T", "/usr/bin/codesign",
}
if !reflect.DeepEqual(f.Calls[0].Args, want) {
t.Fatalf("args got %v want %v", f.Calls[0].Args, want)
}
// Partition list set so codesign can use the imported key non-interactively.
wantPartition := []string{
"set-key-partition-list", "-S", "apple-tool:,apple:,codesign:",
"-s", "-k", "pw", "/tmp/k",
}
if !reflect.DeepEqual(f.Calls[1].Args, wantPartition) {
t.Fatalf("partition args got %v want %v", f.Calls[1].Args, wantPartition)
}
}
func TestDeleteKeychain_BuildsCorrectArgs(t *testing.T) {
f := &runner.Fake{}
f.On("security", nil).Return(nil, nil)
err := codesign.DeleteKeychain(context.Background(), f, codesign.Keychain{Path: "/tmp/k"})
if err != nil {
t.Fatalf("unexpected: %v", err)
}
want := []string{"delete-keychain", "/tmp/k"}
if !reflect.DeepEqual(f.Calls[0].Args, want) {
t.Fatalf("args got %v want %v", f.Calls[0].Args, want)
}
}