Skip to content

Commit 754fdc0

Browse files
authored
Replace Quill CLI with focused Mach-O tool (#63954)
1 parent e9e4774 commit 754fdc0

5 files changed

Lines changed: 227 additions & 362 deletions

File tree

Herebyfile.mjs

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1523,24 +1523,6 @@ ${entries}
15231523
`;
15241524
}
15251525

1526-
/**
1527-
* @param {string} filePath
1528-
*/
1529-
async function verifyTypeScriptMacEntitlements(filePath) {
1530-
const { stdout } = await $pipe`go -C ./tools tool quill describe --quiet --output json ${filePath}`;
1531-
const details = JSON.parse(stdout);
1532-
const entitlements = details[0]?.superBlob?.entitlements?.entitlements;
1533-
if (typeof entitlements !== "string") {
1534-
throw new Error(`Signed file has no macOS entitlements: ${filePath}`);
1535-
}
1536-
for (const entitlement of typescriptMacEntitlements) {
1537-
const escapedEntitlement = entitlement.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1538-
if (!new RegExp(`<key>\\s*${escapedEntitlement}\\s*</key>\\s*<true\\s*/>`).test(entitlements)) {
1539-
throw new Error(`Signed file is missing macOS entitlement '${entitlement}': ${filePath}`);
1540-
}
1541-
}
1542-
}
1543-
15441526
/**
15451527
* @typedef {"win32" | "linux" | "darwin" | "aix" | "android" | "freebsd" | "netbsd" | "openbsd" | "sunos"} OS
15461528
* @typedef {"x64" | "arm" | "arm64" | "ia32" | "ppc64" | "loong64" | "mips64el" | "riscv64" | "s390x"} Arch
@@ -2096,7 +2078,7 @@ async function runSignNativePreviewPackages() {
20962078
// along with a notarization step.
20972079
for (const p of filelistPaths) {
20982080
// ESRP preserves entitlements from an existing ad-hoc signature.
2099-
await $pipe`go -C ./tools tool quill sign --quiet --ad-hoc --identity ${path.basename(p.path)} --entitlements ${typescriptMacEntitlementsPath} ${p.path}`;
2081+
await $pipe`go -C ./tools run ./cmd/machotool sign ${typescriptMacEntitlementsPath} ${p.path}`;
21002082

21012083
const unsignedZipPath = path.join(tmp, `${p.tmpName}.unsigned.zip`);
21022084
const signedZipPath = path.join(tmp, `${p.tmpName}.signed.zip`);
@@ -2157,7 +2139,7 @@ async function runSignNativePreviewPackages() {
21572139

21582140
for (const p of macZips) {
21592141
await fs.promises.chmod(p.path, 0o755);
2160-
await verifyTypeScriptMacEntitlements(p.path);
2142+
await $pipe`go -C ./tools run ./cmd/machotool verify ${typescriptMacEntitlementsPath} ${p.path}`;
21612143
}
21622144
}
21632145
}

tools/cmd/machotool/main.go

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Portions of this file are derived from github.com/anchore/quill v0.7.1.
2+
// The original code is licensed under the Apache License, Version 2.0.
3+
// This version is modified to support only the operations used by TypeScript.
4+
5+
package main
6+
7+
import (
8+
"encoding/xml"
9+
"errors"
10+
"fmt"
11+
"os"
12+
"path/filepath"
13+
"strings"
14+
15+
"github.com/anchore/quill/quill/macho"
16+
"github.com/anchore/quill/quill/pki"
17+
"github.com/anchore/quill/quill/sign"
18+
blacktopmacho "github.com/blacktop/go-macho"
19+
)
20+
21+
func main() {
22+
if err := run(os.Args[1:]); err != nil {
23+
fmt.Fprintln(os.Stderr, err)
24+
os.Exit(1)
25+
}
26+
}
27+
28+
func run(args []string) error {
29+
if len(args) != 3 {
30+
return errors.New("usage: machotool <sign|verify> <entitlements> <binary>")
31+
}
32+
33+
entitlements, err := os.ReadFile(args[1])
34+
if err != nil {
35+
return fmt.Errorf("read entitlements: %w", err)
36+
}
37+
switch args[0] {
38+
case "sign":
39+
return signBinary(args[2], string(entitlements))
40+
case "verify":
41+
return verifyBinary(args[2], string(entitlements))
42+
default:
43+
return fmt.Errorf("unknown command %q", args[0])
44+
}
45+
}
46+
47+
func verifyBinary(path, expectedEntitlements string) error {
48+
file, err := blacktopmacho.Open(path)
49+
if err != nil {
50+
return err
51+
}
52+
defer file.Close()
53+
54+
codeSignature := file.CodeSignature()
55+
if codeSignature == nil || codeSignature.Entitlements == "" {
56+
return fmt.Errorf("signed file has no macOS entitlements: %s", path)
57+
}
58+
if err := verifyEntitlements(codeSignature.Entitlements, expectedEntitlements); err != nil {
59+
return fmt.Errorf("verify entitlements in %s: %w", path, err)
60+
}
61+
return nil
62+
}
63+
64+
type plistElement struct {
65+
XMLName xml.Name `xml:""`
66+
Value string `xml:",chardata"`
67+
}
68+
69+
type entitlementsPlist struct {
70+
XMLName xml.Name `xml:"plist"`
71+
Dict struct {
72+
Elements []plistElement `xml:",any"`
73+
} `xml:"dict"`
74+
}
75+
76+
func parseBooleanEntitlements(contents string) (map[string]bool, error) {
77+
var plist entitlementsPlist
78+
if err := xml.Unmarshal([]byte(contents), &plist); err != nil {
79+
return nil, err
80+
}
81+
82+
entitlements := map[string]bool{}
83+
key := ""
84+
for _, element := range plist.Dict.Elements {
85+
switch element.XMLName.Local {
86+
case "key":
87+
key = strings.TrimSpace(element.Value)
88+
case "true", "false":
89+
if key != "" {
90+
entitlements[key] = element.XMLName.Local == "true"
91+
}
92+
key = ""
93+
default:
94+
key = ""
95+
}
96+
}
97+
return entitlements, nil
98+
}
99+
100+
func verifyEntitlements(actual, expected string) error {
101+
expectedValues, err := parseBooleanEntitlements(expected)
102+
if err != nil {
103+
return fmt.Errorf("parse expected entitlements: %w", err)
104+
}
105+
if len(expectedValues) == 0 {
106+
return errors.New("expected plist contains no boolean entitlements")
107+
}
108+
109+
actualValues, err := parseBooleanEntitlements(actual)
110+
if err != nil {
111+
return fmt.Errorf("parse signed entitlements: %w", err)
112+
}
113+
for key, expectedValue := range expectedValues {
114+
if actualValue, ok := actualValues[key]; !ok || actualValue != expectedValue {
115+
return fmt.Errorf("entitlement %q does not have expected value %t", key, expectedValue)
116+
}
117+
}
118+
return nil
119+
}
120+
121+
func signBinary(path, entitlements string) error {
122+
file, err := macho.NewFile(path)
123+
if err != nil {
124+
return err
125+
}
126+
defer file.Close()
127+
128+
if file.HasCodeSigningCmd() {
129+
err = file.RemoveSigningContent()
130+
if err != nil {
131+
return fmt.Errorf("remove existing signature: %w", err)
132+
}
133+
}
134+
signingMaterial := pki.SigningMaterial{}
135+
136+
err = file.AddEmptyCodeSigningCmd()
137+
if err != nil {
138+
return err
139+
}
140+
identity := filepath.Base(path)
141+
superBlobSize, superBlob, err := sign.GenerateSigningSuperBlob(identity, file, signingMaterial, entitlements, 0)
142+
if err != nil {
143+
return fmt.Errorf("generate initial signature: %w", err)
144+
}
145+
err = sign.UpdateSuperBlobOffsetReferences(file, uint64(len(superBlob)))
146+
if err != nil {
147+
return fmt.Errorf("update signature offsets: %w", err)
148+
}
149+
_, superBlob, err = sign.GenerateSigningSuperBlob(identity, file, signingMaterial, entitlements, superBlobSize)
150+
if err != nil {
151+
return fmt.Errorf("generate final signature: %w", err)
152+
}
153+
154+
command, _, err := file.CodeSigningCmd()
155+
if err != nil {
156+
return err
157+
}
158+
if err := file.Patch(superBlob, len(superBlob), uint64(command.DataOffset)); err != nil {
159+
return fmt.Errorf("write signature: %w", err)
160+
}
161+
return nil
162+
}

tools/cmd/machotool/main_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package main
2+
3+
import "testing"
4+
5+
func TestVerifyEntitlements(t *testing.T) {
6+
t.Parallel()
7+
8+
expected := `<?xml version="1.0" encoding="UTF-8"?>
9+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
10+
<plist><dict>
11+
<key>first</key><true/>
12+
<key>second</key><true/>
13+
</dict></plist>`
14+
15+
for _, test := range []struct {
16+
name string
17+
actual string
18+
wantErr bool
19+
}{
20+
{
21+
name: "equivalent formatting and order",
22+
actual: `<plist>
23+
<dict>
24+
<key>second</key>
25+
<true></true>
26+
<key>extra</key><true/>
27+
<key>first</key>
28+
<true />
29+
</dict>
30+
</plist>`,
31+
},
32+
{
33+
name: "missing",
34+
actual: `<plist><dict><key>first</key><true/></dict></plist>`,
35+
wantErr: true,
36+
},
37+
{
38+
name: "disabled",
39+
actual: `<plist><dict><key>first</key><true/><key>second</key><false/></dict></plist>`,
40+
wantErr: true,
41+
},
42+
} {
43+
t.Run(test.name, func(t *testing.T) {
44+
t.Parallel()
45+
err := verifyEntitlements(test.actual, expected)
46+
if (err != nil) != test.wantErr {
47+
t.Fatalf("verifyEntitlements() error = %v, wantErr %v", err, test.wantErr)
48+
}
49+
})
50+
}
51+
}
52+
53+
func TestVerifyEntitlementsRejectsNestedValue(t *testing.T) {
54+
t.Parallel()
55+
56+
expected := `<plist><dict><key>first</key><true/></dict></plist>`
57+
actual := `<plist><dict><key>other</key><dict><key>first</key><true/></dict></dict></plist>`
58+
if err := verifyEntitlements(actual, expected); err == nil {
59+
t.Fatal("verifyEntitlements() succeeded for a nested entitlement")
60+
}
61+
}

tools/go.mod

Lines changed: 2 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -3,117 +3,25 @@ module github.com/microsoft/TypeScript/tools
33
go 1.26
44

55
require (
6+
github.com/anchore/quill v0.7.1
7+
github.com/blacktop/go-macho v1.1.263
68
github.com/golangci/plugin-module-register v0.1.2
79
golang.org/x/mod v0.37.0
810
golang.org/x/tools v0.47.0
911
gotest.tools/v3 v3.5.2
1012
)
1113

1214
require (
13-
dario.cat/mergo v1.0.2 // indirect
14-
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
15-
github.com/adrg/xdg v0.5.3 // indirect
16-
github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf // indirect
17-
github.com/anchore/clio v0.0.0-20251028140840-721eea5390a4 // indirect
18-
github.com/anchore/fangs v0.0.0-20251204220743-df7ac3652565 // indirect
19-
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d // indirect
2015
github.com/anchore/go-logger v0.0.0-20251106021608-a5b0513fa9a9 // indirect
2116
github.com/anchore/go-macholibre v0.0.0-20250826193721-3cd206ca93aa // indirect
22-
github.com/anchore/quill v0.7.1 // indirect
23-
github.com/atotto/clipboard v0.1.4 // indirect
24-
github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
25-
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
26-
github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect
27-
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
28-
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
29-
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 // indirect
30-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
31-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
32-
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
33-
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
34-
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
35-
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
36-
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
37-
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
38-
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
39-
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
40-
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
41-
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
42-
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
43-
github.com/aws/smithy-go v1.24.2 // indirect
44-
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
4517
github.com/blacktop/go-dwarf v1.0.14 // indirect
46-
github.com/blacktop/go-macho v1.1.263 // indirect
47-
github.com/charmbracelet/bubbles v0.21.0 // indirect
48-
github.com/charmbracelet/bubbletea v1.3.10 // indirect
49-
github.com/charmbracelet/colorprofile v0.4.1 // indirect
50-
github.com/charmbracelet/harmonica v0.2.0 // indirect
51-
github.com/charmbracelet/lipgloss v1.1.0 // indirect
52-
github.com/charmbracelet/x/ansi v0.11.3 // indirect
53-
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
54-
github.com/charmbracelet/x/term v0.2.2 // indirect
55-
github.com/clipperhouse/displaywidth v0.6.2 // indirect
56-
github.com/clipperhouse/stringish v0.1.1 // indirect
57-
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
58-
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
59-
github.com/erikgeiser/promptkit v0.9.0 // indirect
60-
github.com/felixge/fgprof v0.9.5 // indirect
61-
github.com/fsnotify/fsnotify v1.9.0 // indirect
62-
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
6318
github.com/github/smimesign v0.2.0 // indirect
64-
github.com/go-openapi/errors v0.22.5 // indirect
65-
github.com/go-openapi/strfmt v0.25.0 // indirect
6619
github.com/go-restruct/restruct v1.2.0-alpha // indirect
67-
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
68-
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
6920
github.com/google/go-cmp v0.7.0 // indirect
70-
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect
7121
github.com/google/uuid v1.6.0 // indirect
72-
github.com/gookit/color v1.6.0 // indirect
73-
github.com/hashicorp/errwrap v1.1.0 // indirect
74-
github.com/hashicorp/go-multierror v1.1.1 // indirect
75-
github.com/iancoleman/strcase v0.3.0 // indirect
76-
github.com/inconshreveable/mousetrap v1.1.0 // indirect
77-
github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect
78-
github.com/klauspost/compress v1.18.4 // indirect
79-
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
80-
github.com/mattn/go-colorable v0.1.14 // indirect
81-
github.com/mattn/go-isatty v0.0.20 // indirect
82-
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
83-
github.com/mattn/go-runewidth v0.0.19 // indirect
84-
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
85-
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
86-
github.com/muesli/cancelreader v0.2.2 // indirect
87-
github.com/muesli/reflow v0.3.0 // indirect
88-
github.com/muesli/termenv v0.16.0 // indirect
89-
github.com/oklog/ulid v1.3.1 // indirect
90-
github.com/pborman/indent v1.2.1 // indirect
91-
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
9222
github.com/pkg/errors v0.9.1 // indirect
93-
github.com/pkg/profile v1.7.0 // indirect
94-
github.com/rivo/uniseg v0.4.7 // indirect
95-
github.com/sagikazarmark/locafero v0.12.0 // indirect
9623
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect
97-
github.com/sirupsen/logrus v1.9.3 // indirect
98-
github.com/spf13/afero v1.15.0 // indirect
99-
github.com/spf13/cast v1.10.0 // indirect
100-
github.com/spf13/cobra v1.10.2 // indirect
101-
github.com/spf13/pflag v1.0.10 // indirect
102-
github.com/spf13/viper v1.21.0 // indirect
103-
github.com/subosito/gotenv v1.6.0 // indirect
104-
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect
105-
github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect
106-
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
107-
go.mongodb.org/mongo-driver v1.17.6 // indirect
108-
go.yaml.in/yaml/v3 v3.0.4 // indirect
10924
golang.org/x/crypto v0.53.0 // indirect
110-
golang.org/x/net v0.56.0 // indirect
11125
golang.org/x/sync v0.21.0 // indirect
112-
golang.org/x/sys v0.46.0 // indirect
113-
golang.org/x/term v0.44.0 // indirect
114-
golang.org/x/text v0.38.0 // indirect
115-
gopkg.in/yaml.v3 v3.0.1 // indirect
11626
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
11727
)
118-
119-
tool github.com/anchore/quill/cmd/quill

0 commit comments

Comments
 (0)