Skip to content

About

Fully hermetic, cross-build and remote-friendly `codesign` for Apple-targeted binaries

Resources

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

codesign.bzl

codesign.bzl provides a hermetic Bazel rule for signing Apple binaries.

It wraps the rcodesign binary from the apple-codesign project and ships prebuilt binaries for macOS, Linux, and Windows. That means you can sign Mach-O binaries from any supported execution platform without depending on Apple's /usr/bin/codesign being installed on the machine running Bazel.

This is useful when you want:

  • reproducible codesigning actions in Bazel
  • remote execution for Apple packaging pipelines
  • codesigning from Linux or Windows build workers
  • a toolchain-backed rule instead of ad hoc genrule shelling out to host tools

Setup

Add the module to your MODULE.bazel and register the prebuilt toolchains:

bazel_dep(name = "codesign.bzl", version = "0.0.1")

register_toolchains("@codesign.bzl//toolchain:all")

Usage

Load the rule:

load("@codesign.bzl//:codesign.bzl", "codesign")

Then use it to sign a binary:

codesign(
    name = "signed_app",
    binary = ":unsigned_app",
    binary_identifier = "com.example.myapp",
    entitlements = "entitlements.plist",
)

The rule produces a signed executable target named signed_app.

Attributes

binary : The unsigned Mach-O binary to sign.

binary_identifier : The bundle or binary identifier to embed in the signature, for example com.example.myapp.

entitlements : An XML entitlements plist to apply while signing.

Developer ID release signing

The codesign build rule above creates an ad-hoc signature. For distribution with a Developer ID Application identity, use codesign_developer_id:

load("@codesign.bzl//:codesign.bzl", "codesign_developer_id")

codesign_developer_id(
    name = "sign_release",
    binary = ":unsigned_app",
    binary_identifier = "com.example.myapp",
    # Optional; omit when no additional entitlements are needed.
    entitlements = "entitlements.plist",
)

bazel build //:sign_release builds the unsigned binary and a signing launcher. It does not read credentials, contact Apple's timestamp service, or sign anything. Run the launcher to sign to a new, absolute output path:

bazel run //:sign_release -- \
  --p12-file /secure/developer-id.p12 \
  --p12-password-file /secure/password.txt \
  --output /releases/signed_app

The launcher uses rcodesign sign --for-notarization, which requires a Developer ID certificate, enables hardened runtime, and requires a secure timestamp. It then runs rcodesign verify and propagates either command's failure. It disables implicit rcodesign configuration files. Existing output paths are rejected; callers must choose a fresh path and only consume the output after success.

Provide secret files at runtime from your CI secret store with restricted file permissions. Do not declare them as Bazel inputs, commit them, or pass password contents on the command line. For a separate credential-free build phase, use bazel run --script_path=/path/to/sign-release.sh //:sign_release, then execute that script in the credential-bearing phase with the arguments above. Keep its Bazel output tree and runfiles available at the same paths.

This launcher requires Bash on Linux or macOS, with the signer toolchain selected for that machine's execution platform. The unsigned input can be cross-compiled for macOS. Native Windows launchers and directory/app-bundle inputs are not supported by this rule.

Notarization and reproducibility

Signing does not notarize the output. Submit the signed artifact to Apple's notary service, require acceptance, and validate with Apple's native tools on macOS before distributing. Notarization orchestration and storage policy belong to the release pipeline; this rule does not implement them.

The unsigned build and launcher can use Bazel's cache. Credentials and the signed output are outside build actions and are not uploaded to the action cache by this rule. Fresh secure timestamps can change the signed bytes on every run: cache eviction must not be treated as permission to replace an immutable release. Persist and reuse the accepted signed artifact when retrying a release.

See the upstream rcodesign signing documentation and notarization documentation.

Testing the release launcher

Run python3 tests/developer_id_test.py on Linux or macOS with Bazel/Bazelisk, Bash, and Python 3 installed. The consumer integration tests build the actual rule using a recording signer and check that the packaged signer rejects an invalid identity; they need no Apple credentials. They test runfiles, argument handling, failure propagation, and the build/publication boundary, not Apple notarization acceptance or Gatekeeper behavior. The tests ignore user and repository bazelrc files and do not use remote execution, caching, or BES.

About

Fully hermetic, cross-build and remote-friendly `codesign` for Apple-targeted binaries

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages