Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 2 additions & 20 deletions Herebyfile.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1523,24 +1523,6 @@ ${entries}
`;
}

/**
* @param {string} filePath
*/
async function verifyTypeScriptMacEntitlements(filePath) {
const { stdout } = await $pipe`go -C ./tools tool quill describe --quiet --output json ${filePath}`;
const details = JSON.parse(stdout);
const entitlements = details[0]?.superBlob?.entitlements?.entitlements;
if (typeof entitlements !== "string") {
throw new Error(`Signed file has no macOS entitlements: ${filePath}`);
}
for (const entitlement of typescriptMacEntitlements) {
const escapedEntitlement = entitlement.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
if (!new RegExp(`<key>\\s*${escapedEntitlement}\\s*</key>\\s*<true\\s*/>`).test(entitlements)) {
throw new Error(`Signed file is missing macOS entitlement '${entitlement}': ${filePath}`);
}
}
}

/**
* @typedef {"win32" | "linux" | "darwin" | "aix" | "android" | "freebsd" | "netbsd" | "openbsd" | "sunos"} OS
* @typedef {"x64" | "arm" | "arm64" | "ia32" | "ppc64" | "loong64" | "mips64el" | "riscv64" | "s390x"} Arch
Expand Down Expand Up @@ -2096,7 +2078,7 @@ async function runSignNativePreviewPackages() {
// along with a notarization step.
for (const p of filelistPaths) {
// ESRP preserves entitlements from an existing ad-hoc signature.
await $pipe`go -C ./tools tool quill sign --quiet --ad-hoc --identity ${path.basename(p.path)} --entitlements ${typescriptMacEntitlementsPath} ${p.path}`;
await $pipe`go -C ./tools run ./cmd/machotool sign ${typescriptMacEntitlementsPath} ${p.path}`;

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

for (const p of macZips) {
await fs.promises.chmod(p.path, 0o755);
await verifyTypeScriptMacEntitlements(p.path);
await $pipe`go -C ./tools run ./cmd/machotool verify ${typescriptMacEntitlementsPath} ${p.path}`;
}
}
}
Expand Down
162 changes: 162 additions & 0 deletions tools/cmd/machotool/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
// Portions of this file are derived from github.com/anchore/quill v0.7.1.
// The original code is licensed under the Apache License, Version 2.0.
// This version is modified to support only the operations used by TypeScript.

package main

import (
"encoding/xml"
"errors"
"fmt"
"os"
"path/filepath"
"strings"

"github.com/anchore/quill/quill/macho"
"github.com/anchore/quill/quill/pki"
"github.com/anchore/quill/quill/sign"
blacktopmacho "github.com/blacktop/go-macho"
)

func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

func run(args []string) error {
if len(args) != 3 {
return errors.New("usage: machotool <sign|verify> <entitlements> <binary>")
}

entitlements, err := os.ReadFile(args[1])
if err != nil {
return fmt.Errorf("read entitlements: %w", err)
}
switch args[0] {
case "sign":
return signBinary(args[2], string(entitlements))
case "verify":
return verifyBinary(args[2], string(entitlements))
default:
return fmt.Errorf("unknown command %q", args[0])
}
}

func verifyBinary(path, expectedEntitlements string) error {
file, err := blacktopmacho.Open(path)
if err != nil {
return err
}
defer file.Close()

codeSignature := file.CodeSignature()
if codeSignature == nil || codeSignature.Entitlements == "" {
return fmt.Errorf("signed file has no macOS entitlements: %s", path)
}
if err := verifyEntitlements(codeSignature.Entitlements, expectedEntitlements); err != nil {
return fmt.Errorf("verify entitlements in %s: %w", path, err)
}
return nil
}

type plistElement struct {
XMLName xml.Name `xml:""`
Value string `xml:",chardata"`
}

type entitlementsPlist struct {
XMLName xml.Name `xml:"plist"`
Dict struct {
Elements []plistElement `xml:",any"`
} `xml:"dict"`
}

func parseBooleanEntitlements(contents string) (map[string]bool, error) {
var plist entitlementsPlist
if err := xml.Unmarshal([]byte(contents), &plist); err != nil {
return nil, err
}

entitlements := map[string]bool{}
key := ""
for _, element := range plist.Dict.Elements {
switch element.XMLName.Local {
case "key":
key = strings.TrimSpace(element.Value)
case "true", "false":
if key != "" {
entitlements[key] = element.XMLName.Local == "true"
}
key = ""
default:
key = ""
}
}
return entitlements, nil
}

func verifyEntitlements(actual, expected string) error {
expectedValues, err := parseBooleanEntitlements(expected)
if err != nil {
return fmt.Errorf("parse expected entitlements: %w", err)
}
if len(expectedValues) == 0 {
return errors.New("expected plist contains no boolean entitlements")
}

actualValues, err := parseBooleanEntitlements(actual)
if err != nil {
return fmt.Errorf("parse signed entitlements: %w", err)
}
for key, expectedValue := range expectedValues {
if actualValue, ok := actualValues[key]; !ok || actualValue != expectedValue {
return fmt.Errorf("entitlement %q does not have expected value %t", key, expectedValue)
}
}
return nil
}

func signBinary(path, entitlements string) error {
file, err := macho.NewFile(path)
if err != nil {
return err
}
defer file.Close()

if file.HasCodeSigningCmd() {
err = file.RemoveSigningContent()
if err != nil {
return fmt.Errorf("remove existing signature: %w", err)
}
}
signingMaterial := pki.SigningMaterial{}

err = file.AddEmptyCodeSigningCmd()
if err != nil {
return err
}
identity := filepath.Base(path)
superBlobSize, superBlob, err := sign.GenerateSigningSuperBlob(identity, file, signingMaterial, entitlements, 0)
if err != nil {
return fmt.Errorf("generate initial signature: %w", err)
}
err = sign.UpdateSuperBlobOffsetReferences(file, uint64(len(superBlob)))
if err != nil {
return fmt.Errorf("update signature offsets: %w", err)
}
_, superBlob, err = sign.GenerateSigningSuperBlob(identity, file, signingMaterial, entitlements, superBlobSize)
if err != nil {
return fmt.Errorf("generate final signature: %w", err)
}

command, _, err := file.CodeSigningCmd()
if err != nil {
return err
}
if err := file.Patch(superBlob, len(superBlob), uint64(command.DataOffset)); err != nil {
return fmt.Errorf("write signature: %w", err)
}
return nil
}
61 changes: 61 additions & 0 deletions tools/cmd/machotool/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package main

import "testing"

func TestVerifyEntitlements(t *testing.T) {
t.Parallel()

expected := `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist><dict>
<key>first</key><true/>
<key>second</key><true/>
</dict></plist>`

for _, test := range []struct {
name string
actual string
wantErr bool
}{
{
name: "equivalent formatting and order",
actual: `<plist>
<dict>
<key>second</key>
<true></true>
<key>extra</key><true/>
<key>first</key>
<true />
</dict>
</plist>`,
},
{
name: "missing",
actual: `<plist><dict><key>first</key><true/></dict></plist>`,
wantErr: true,
},
{
name: "disabled",
actual: `<plist><dict><key>first</key><true/><key>second</key><false/></dict></plist>`,
wantErr: true,
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
err := verifyEntitlements(test.actual, expected)
if (err != nil) != test.wantErr {
t.Fatalf("verifyEntitlements() error = %v, wantErr %v", err, test.wantErr)
}
})
}
}

func TestVerifyEntitlementsRejectsNestedValue(t *testing.T) {
t.Parallel()

expected := `<plist><dict><key>first</key><true/></dict></plist>`
actual := `<plist><dict><key>other</key><dict><key>first</key><true/></dict></dict></plist>`
if err := verifyEntitlements(actual, expected); err == nil {
t.Fatal("verifyEntitlements() succeeded for a nested entitlement")
}
}
96 changes: 2 additions & 94 deletions tools/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,117 +3,25 @@ module github.com/microsoft/TypeScript/tools
go 1.26

require (
github.com/anchore/quill v0.7.1
github.com/blacktop/go-macho v1.1.263
github.com/golangci/plugin-module-register v0.1.2
golang.org/x/mod v0.37.0
golang.org/x/tools v0.47.0
gotest.tools/v3 v3.5.2
)

require (
dario.cat/mergo v1.0.2 // indirect
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d // indirect
github.com/adrg/xdg v0.5.3 // indirect
github.com/anchore/bubbly v0.0.0-20250717181826-8a411f9d8cbf // indirect
github.com/anchore/clio v0.0.0-20251028140840-721eea5390a4 // indirect
github.com/anchore/fangs v0.0.0-20251204220743-df7ac3652565 // indirect
github.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d // indirect
github.com/anchore/go-logger v0.0.0-20251106021608-a5b0513fa9a9 // indirect
github.com/anchore/go-macholibre v0.0.0-20250826193721-3cd206ca93aa // indirect
github.com/anchore/quill v0.7.1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aws/aws-sdk-go-v2 v1.41.3 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect
github.com/aws/aws-sdk-go-v2/config v1.32.10 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.21.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
github.com/aws/smithy-go v1.24.2 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/blacktop/go-dwarf v1.0.14 // indirect
github.com/blacktop/go-macho v1.1.263 // indirect
github.com/charmbracelet/bubbles v0.21.0 // indirect
github.com/charmbracelet/bubbletea v1.3.10 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/lipgloss v1.1.0 // indirect
github.com/charmbracelet/x/ansi v0.11.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/erikgeiser/promptkit v0.9.0 // indirect
github.com/felixge/fgprof v0.9.5 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/github/smimesign v0.2.0 // indirect
github.com/go-openapi/errors v0.22.5 // indirect
github.com/go-openapi/strfmt v0.25.0 // indirect
github.com/go-restruct/restruct v1.2.0-alpha // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20251213031049-b05bdaca462f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gookit/color v1.6.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.16.0 // indirect
github.com/oklog/ulid v1.3.1 // indirect
github.com/pborman/indent v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pkg/profile v1.7.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sagikazarmark/locafero v0.12.0 // indirect
github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/spf13/viper v1.21.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect
github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.mongodb.org/mongo-driver v1.17.6 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/term v0.44.0 // indirect
golang.org/x/text v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
software.sslmate.com/src/go-pkcs12 v0.7.0 // indirect
)

tool github.com/anchore/quill/cmd/quill
Loading