@@ -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 != "" {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -14,6 +14,10 @@ var macosPackageInfoTemplate = template.Must(template.New("").Option("missingkey
|
||||
</pkg-info>
|
||||
`))
|
||||
|
||||
// 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
|
||||
<choice id="choiceBase" title="Fleet osquery" enabled="false" selected="true" description="Standard installation for Fleet osquery.">
|
||||
<pkg-ref id="{{.Identifier}}.base.pkg"/>
|
||||
</choice>
|
||||
{{/* base.pkg specified here is the foldername that contains the package contents */}}
|
||||
<pkg-ref id="{{.Identifier}}.base.pkg" version="{{.Version}}" auth="root">#base.pkg</pkg-ref>
|
||||
{{/* this ref is collapsed with the previous, having a bundle version helps our notarization tools */}}
|
||||
<pkg-ref id="{{.Identifier}}.base.pkg">
|
||||
<bundle-version>
|
||||
<bundle id="{{.Identifier}}" path="" />
|
||||
</bundle-version>
|
||||
</pkg-ref>
|
||||
</installer-gui-script>
|
||||
`))
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user