diff --git a/cmd/fleetctl/package.go b/cmd/fleetctl/package.go
index fd2983158a..a0cb77c07f 100644
--- a/cmd/fleetctl/package.go
+++ b/cmd/fleetctl/package.go
@@ -150,6 +150,30 @@ func packageCommand() *cli.Command {
EnvVars: []string{"FLEETCTL_NATIVE_TOOLING"},
Destination: &opt.NativeTooling,
},
+ &cli.StringFlag{
+ Name: "macos-devid-pem-content",
+ Usage: "Dev ID certificate keypair content in PEM format",
+ EnvVars: []string{"FLEETCTL_MACOS_DEVID_PEM_CONTENT"},
+ Destination: &opt.MacOSDevIDCertificateContent,
+ },
+ &cli.StringFlag{
+ Name: "app-store-connect-api-key-id",
+ Usage: "App Store Connect API key used for notarization",
+ EnvVars: []string{"FLEETCTL_APP_STORE_CONNECT_API_KEY_ID"},
+ Destination: &opt.AppStoreConnectAPIKeyID,
+ },
+ &cli.StringFlag{
+ Name: "app-store-connect-api-key-issuer",
+ Usage: "Issuer of the App Store Connect API key",
+ EnvVars: []string{"FLEETCTL_APP_STORE_CONNECT_API_KEY_ISSUER"},
+ Destination: &opt.AppStoreConnectAPIKeyIssuer,
+ },
+ &cli.StringFlag{
+ Name: "app-store-connect-api-key-content",
+ Usage: "Contents of the .p8 App Store Connect API key",
+ EnvVars: []string{"FLEETCTL_APP_STORE_CONNECT_API_KEY_CONTENT"},
+ Destination: &opt.AppStoreConnectAPIKeyContent,
+ },
},
Action: func(c *cli.Context) error {
if opt.FleetURL != "" || opt.EnrollSecret != "" {
diff --git a/orbit/pkg/packaging/macos.go b/orbit/pkg/packaging/macos.go
index 89b0916a50..c092eecf4a 100644
--- a/orbit/pkg/packaging/macos.go
+++ b/orbit/pkg/packaging/macos.go
@@ -2,6 +2,7 @@ package packaging
import (
"bytes"
+ "errors"
"fmt"
"io/ioutil"
"os"
@@ -116,17 +117,46 @@ func BuildPkg(opt Options) (string, error) {
}
generatedPath := filepath.Join(tmpDir, "orbit.pkg")
+ isDarwin := runtime.GOOS == "darwin"
+ isLinuxNative := runtime.GOOS == "linux" && opt.NativeTooling
if len(opt.SignIdentity) != 0 {
+ if len(opt.MacOSDevIDCertificateContent) != 0 {
+ return "", errors.New("providing a sign identity and a Dev ID certificate is not supported")
+ }
+
log.Info().Str("identity", opt.SignIdentity).Msg("productsign package")
if err := signPkg(generatedPath, opt.SignIdentity); err != nil {
return "", fmt.Errorf("productsign: %w", err)
}
}
+ if isLinuxNative && len(opt.MacOSDevIDCertificateContent) > 0 {
+ if len(opt.SignIdentity) != 0 {
+ return "", errors.New("providing a sign identity and a Dev ID certificate is not supported")
+ }
+
+ if err := rSign(generatedPath, opt.MacOSDevIDCertificateContent); err != nil {
+ return "", fmt.Errorf("rcodesign: %w", err)
+ }
+ }
+
if opt.Notarize {
- if err := NotarizeStaple(generatedPath, "com.fleetdm.orbit"); err != nil {
- return "", err
+ switch {
+ case isDarwin:
+ if err := NotarizeStaple(generatedPath, "com.fleetdm.orbit"); err != nil {
+ return "", err
+ }
+ case isLinuxNative:
+ if len(opt.AppStoreConnectAPIKeyID) == 0 || len(opt.AppStoreConnectAPIKeyIssuer) == 0 {
+ return "", errors.New("both an App Store Connect API key and issuer must be set for native notarization")
+ }
+
+ if err := rNotarizeStaple(generatedPath, opt.AppStoreConnectAPIKeyID, opt.AppStoreConnectAPIKeyIssuer, opt.AppStoreConnectAPIKeyContent); err != nil {
+ return "", err
+ }
+ default:
+ return "", errors.New("notarization is not supported in this platform")
}
}
diff --git a/orbit/pkg/packaging/macos_rcodesign.go b/orbit/pkg/packaging/macos_rcodesign.go
new file mode 100644
index 0000000000..9d8acc2fb2
--- /dev/null
+++ b/orbit/pkg/packaging/macos_rcodesign.go
@@ -0,0 +1,79 @@
+package packaging
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+
+ "github.com/fleetdm/fleet/v4/pkg/secure"
+)
+
+func rSign(pkgPath, cert string) error {
+ pemPath := filepath.Join(os.TempDir(), "cert.pem")
+ defer os.Remove(pemPath)
+ err := os.WriteFile(pemPath, []byte(cert), 0o600)
+ if err != nil {
+ return fmt.Errorf("writing cert data: %e", err)
+ }
+
+ var outBuf bytes.Buffer
+ cmd := exec.Command(
+ "rcodesign",
+ "sign",
+ pkgPath,
+ "--pem-source", pemPath,
+ )
+ cmd.Stdout = &outBuf
+ cmd.Stderr = &outBuf
+ if err := cmd.Run(); err != nil {
+ fmt.Println(outBuf.String())
+ return fmt.Errorf("rcodesign: %w", err)
+ }
+ return nil
+}
+
+func rNotarizeStaple(pkg, apiKeyID, apiKeyIssuer, apiKeyContent string) error {
+ path, err := writeAPIKeys(apiKeyIssuer, apiKeyID, apiKeyContent)
+ defer os.Remove(path)
+ if err != nil {
+ return fmt.Errorf("writing API keys: %e", err)
+ }
+ var outBuf bytes.Buffer
+ cmd := exec.Command("rcodesign",
+ "notarize",
+ pkg,
+ "--api-issuer", apiKeyIssuer,
+ "--api-key", apiKeyID,
+ "--staple",
+ )
+ cmd.Stdout = &outBuf
+ cmd.Stderr = &outBuf
+ if err := cmd.Run(); err != nil {
+ fmt.Println(outBuf.String())
+ return fmt.Errorf("rcodesign notarize: %w", err)
+ }
+ return nil
+}
+
+func writeAPIKeys(issuer, id, content string) (string, error) {
+ homedir, err := os.UserHomeDir()
+ if err != nil {
+ return "", fmt.Errorf("finding home dir: %e", err)
+ }
+
+ // The underliying tools (rcodesign and Transporter) expect to find a
+ // certificate key in this path.
+ path := filepath.Join(homedir, ".appstoreconnect", "private_keys")
+ if err = secure.MkdirAll(path, 0o600); err != nil {
+ return "", fmt.Errorf("finding home dir: %e", err)
+ }
+
+ keyPath := filepath.Join(path, fmt.Sprintf("AuthKey_%s.p8", id))
+ if err = os.WriteFile(keyPath, []byte(content), 0o600); err != nil {
+ return "", fmt.Errorf("writing api key contents: %e", err)
+ }
+
+ return keyPath, nil
+}
diff --git a/orbit/pkg/packaging/macos_templates.go b/orbit/pkg/packaging/macos_templates.go
index a9eb75d790..8d219d6770 100644
--- a/orbit/pkg/packaging/macos_templates.go
+++ b/orbit/pkg/packaging/macos_templates.go
@@ -14,6 +14,10 @@ var macosPackageInfoTemplate = template.Must(template.New("").Option("missingkey
`))
+// This template is used to generate a Distribution Definition file, which
+// controls the experience of the installer (the default dir, what options the
+// user has, etc.)
+//
// Reference:
// https://developer.apple.com/library/archive/documentation/DeveloperTools/Reference/DistributionDefinitionRef/Chapters/Distribution_XML_Ref.html
var macosDistributionTemplate = template.Must(template.New("").Option("missingkey=error").Parse(
@@ -26,7 +30,14 @@ var macosDistributionTemplate = template.Must(template.New("").Option("missingke
+ {{/* base.pkg specified here is the foldername that contains the package contents */}}
#base.pkg
+ {{/* this ref is collapsed with the previous, having a bundle version helps our notarization tools */}}
+
+
+
+
+
`))
diff --git a/orbit/pkg/packaging/packaging.go b/orbit/pkg/packaging/packaging.go
index e952fb3e87..ab43134c7e 100644
--- a/orbit/pkg/packaging/packaging.go
+++ b/orbit/pkg/packaging/packaging.go
@@ -66,6 +66,15 @@ type Options struct {
// Native tooling is used to determine if the package should be built
// natively instead of via Docker images.
NativeTooling bool
+ // MacOSDevIDCertificateContent is a string containing a PEM keypair used to
+ // sign a macOS package via NativeTooling
+ MacOSDevIDCertificateContent string
+ // AppStoreConnectAPIKeyID is the Appstore Connect API key provided by Apple
+ AppStoreConnectAPIKeyID string
+ // AppStoreConnectAPIKeyIssuer is the issuer of App Store API Key
+ AppStoreConnectAPIKeyIssuer string
+ // AppStoreConnectAPIKeyContent is the content of the App Store API Key
+ AppStoreConnectAPIKeyContent string
}
func initializeTempDir() (string, error) {
diff --git a/tools/fleetctl-docker/Dockerfile b/tools/fleetctl-docker/Dockerfile
index 787e21d503..a19cdac7dd 100644
--- a/tools/fleetctl-docker/Dockerfile
+++ b/tools/fleetctl-docker/Dockerfile
@@ -1,3 +1,11 @@
+FROM rust:latest AS builder
+
+ARG transporter_url=https://itunesconnect.apple.com/WebObjects/iTunesConnect.woa/ra/resources/download/public/Transporter__Linux/bin
+
+RUN cargo install apple-codesign \
+ && curl -sSf $transporter_url -o transporter_install.sh \
+ && sh transporter_install.sh --target transporter --accept --noexec
+
FROM debian:stable-slim
RUN apt-get update \
@@ -9,6 +17,8 @@ RUN apt-get update \
# copy macOS dependencies
COPY --from=fleetdm/bomutils:latest /usr/bin/mkbom /usr/local/bin/xar /usr/bin/
COPY --from=fleetdm/bomutils:latest /usr/local/lib /usr/local/lib/
+COPY --from=builder /transporter/itms /usr/local/
+COPY --from=builder /usr/local/cargo/bin/rcodesign /usr/local/bin
# copy Windows dependencies
COPY --from=fleetdm/wix:latest /home/wine /home/wine
diff --git a/tools/fleetctl-docker/README.md b/tools/fleetctl-docker/README.md
index 048d6f63bc..8e17095306 100644
--- a/tools/fleetctl-docker/README.md
+++ b/tools/fleetctl-docker/README.md
@@ -24,6 +24,34 @@ context to have access to the `fleetctl` binary. To build the image, run:
make fleetctl-docker
```
+#### macOS signing + notarization
+
+To sign and notarize a generated `pkg` you must have:
+
+1. A Developer ID certificate in PEM format
+2. An Apple Store Connect API key
+
+> Note: the Developer ID certificate must be in PEM format because this image
+> can be run in automated enviroments where secrets are passed via environment
+> variables, and thus they must be in plain text.
+>
+> To convert a PKCS 12 certificate to PEM, you can run the following command:
+>
+> ```
+> openssl pkcs12 -in /path/to/cert.p12 -out signing-keypair.pem -nodes
+> ```
+
+Once you are set, you can build and notarize/staple your package with:
+
+```
+docker run -v "$(pwd):/build" fleetdm/fleetctl package --type=pkg \
+ --macos-devid-pem-content="$(cat /path/to/signing-keypair.pem)" \
+ --notarize \
+ --app-store-connect-api-key-id="A6DX865SKS" \
+ --app-store-connect-api-key-issuer="68911d4c-110c-4172-b9f7-b7efa30f9680 " \
+ --app-store-connect-api-key-content="$(cat /path/to/AuthKey_A6DX865SKS.p8)"
+```
+
### Publishing
There's a GitHub workflow to build and publish this image to Docker Hub, currently it has to be triggered [manually](https://docs.github.com/en/actions/managing-workflow-runs/manually-running-a-workflow).