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).