Skip to content
Merged
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
28 changes: 21 additions & 7 deletions .github/actions/ci-gate/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,21 @@ inputs:
required: false
default: "1"
apple-developer-id-p12-base64:
description: Base64-encoded Developer ID certificate.
description: Base64-encoded Developer ID certificate. Pass it when this build signs with Developer ID.
required: false
default: ""
apple-developer-id-p12-password:
description: Password for the Developer ID certificate.
required: false
default: ""
apple-distribution-p12-base64:
description: Base64-encoded Apple Distribution certificate. Pass it when this build runs where development provisioning cannot work, so only App Store profiles sign.
required: false
default: ""
apple-distribution-p12-password:
description: Password for the Apple Distribution certificate.
required: false
default: ""
apple-developer-id-profile-base64:
description: Base64-encoded Developer ID provisioning profile.
required: false
Expand Down Expand Up @@ -174,8 +182,10 @@ runs:
SWIFT_MK_HELPER_ROOT: ${{ github.repository == 'agoodkind/swift-makefile' && '.' || '.swift-makefile' }}
IMPORT_SIGNING_CERT: ${{ inputs.import-signing-cert }}
INSTALL_PROVISIONING_PROFILE: ${{ inputs.install-provisioning-profile }}
HAS_SIGNING_CERT: ${{ inputs.apple-developer-id-p12-base64 != '' }}
HAS_SIGNING_PASSWORD: ${{ inputs.apple-developer-id-p12-password != '' }}
HAS_DEVELOPER_ID_CERT: ${{ inputs.apple-developer-id-p12-base64 != '' }}
HAS_DEVELOPER_ID_PASSWORD: ${{ inputs.apple-developer-id-p12-password != '' }}
HAS_DISTRIBUTION_CERT: ${{ inputs.apple-distribution-p12-base64 != '' }}
HAS_DISTRIBUTION_PASSWORD: ${{ inputs.apple-distribution-p12-password != '' }}
HAS_PROVISIONING_PROFILE: ${{ inputs.apple-developer-id-profile-base64 != '' }}
run: swift "${SWIFT_MK_HELPER_ROOT}/.github/actions/workflow-helper/workflow-helper.swift" validate-signing-inputs

Expand All @@ -184,17 +194,21 @@ runs:
if: ${{ inputs.run != 'false' && (inputs.gate != 'extra-targets' || steps.resolve.outputs.count != '0') && inputs.import-signing-cert == 'true' && github.repository == 'agoodkind/swift-makefile' }}
uses: ./.github/actions/import-signing-cert
with:
p12-base64: ${{ inputs.apple-developer-id-p12-base64 }}
p12-password: ${{ inputs.apple-developer-id-p12-password }}
developer-id-p12-base64: ${{ inputs.apple-developer-id-p12-base64 }}
developer-id-p12-password: ${{ inputs.apple-developer-id-p12-password }}
distribution-p12-base64: ${{ inputs.apple-distribution-p12-base64 }}
distribution-p12-password: ${{ inputs.apple-distribution-p12-password }}
identity-name: ${{ inputs.signing-identity-name }}

- name: Install signing certificate
id: cert-remote
if: ${{ inputs.run != 'false' && (inputs.gate != 'extra-targets' || steps.resolve.outputs.count != '0') && inputs.import-signing-cert == 'true' && github.repository != 'agoodkind/swift-makefile' }}
uses: agoodkind/swift-makefile/.github/actions/import-signing-cert@main
with:
p12-base64: ${{ inputs.apple-developer-id-p12-base64 }}
p12-password: ${{ inputs.apple-developer-id-p12-password }}
developer-id-p12-base64: ${{ inputs.apple-developer-id-p12-base64 }}
developer-id-p12-password: ${{ inputs.apple-developer-id-p12-password }}
distribution-p12-base64: ${{ inputs.apple-distribution-p12-base64 }}
distribution-p12-password: ${{ inputs.apple-distribution-p12-password }}
identity-name: ${{ inputs.signing-identity-name }}

- name: Install provisioning profile (swift-makefile)
Expand Down
108 changes: 94 additions & 14 deletions .github/actions/import-signing-cert/action.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
name: Import Developer ID signing certificate
description: Import the Developer ID Application certificate with apple-actions/import-codesign-certs and resolve its codesigning identity SHA-1.
name: Import signing certificates
description: Import the signing certificates a build needs into one keychain and resolve the codesigning identity SHA-1 for the identity the caller names.

inputs:
p12-base64:
description: Base64-encoded Developer ID Application .p12.
required: true
p12-password:
description: Import password for the .p12.
required: true
developer-id-p12-base64:
description: Base64-encoded Developer ID Application .p12. Pass it when the build signs anything with Developer ID.
required: false
default: ""
developer-id-p12-password:
description: Import password for the Developer ID .p12.
required: false
default: ""
distribution-p12-base64:
description: Base64-encoded Apple Distribution .p12. Pass it when the build signs on a machine that is not a registered device, where only App Store profiles work.
required: false
default: ""
distribution-p12-password:
description: Import password for the Apple Distribution .p12.
required: false
default: ""
identity-name:
description: 'Full identity name to resolve, e.g. "Developer ID Application: Name (TEAMID)".'
description: 'Full identity name to resolve, e.g. "Developer ID Application: Name (TEAMID)" or "Apple Distribution: Name (TEAMID)". It decides which imported certificate signs.'
required: true

outputs:
Expand Down Expand Up @@ -49,17 +59,82 @@ runs:
printf 'path_noext=%s\n' "$keychain_base" >> "$GITHUB_OUTPUT"
printf 'path=%s\n' "$keychain_path" >> "$GITHUB_OUTPUT"

- name: Require at least one certificate
shell: bash
env:
DEVELOPER_ID_P12: ${{ inputs.developer-id-p12-base64 }}
DISTRIBUTION_P12: ${{ inputs.distribution-p12-base64 }}
run: |
set -euo pipefail

if [[ -z "$DEVELOPER_ID_P12" && -z "$DISTRIBUTION_P12" ]]; then
echo "signing was requested but neither a Developer ID nor an Apple Distribution certificate was passed" >&2
exit 1
fi

- name: Remove stale signing keychain for this runner
shell: bash
env:
KEYCHAIN_PATH: ${{ steps.keychain.outputs.path }}
run: security delete-keychain "$KEYCHAIN_PATH" 2>/dev/null || true

- uses: apple-actions/import-codesign-certs@v3
# The keychain is created here rather than by an import step so that each
# import is guarded only by whether its own certificate was passed. Letting
# the first import create it would make the steps order-dependent: whichever
# ran first would have to create, and the other would have to not, which
# cannot be expressed when either may be absent.
- name: Create the signing keychain
id: create-keychain
shell: bash
env:
KEYCHAIN_NOEXT: ${{ steps.keychain.outputs.path_noext }}
KEYCHAIN_PATH: ${{ steps.keychain.outputs.path }}
run: |
set -euo pipefail

# One command, no pipe. Reading /dev/urandom through a pipe into a
# byte-counting reader makes the reader exit first, which kills the writer
# with a broken pipe; under `pipefail` that fails, and on a runner it hung
# instead. openssl reads its own randomness and stops on its own.
keychain_password="$(openssl rand -hex 24)"
# Masked before it reaches any later step's environment, so no log can
# echo it even if a step runs with the shell tracing on.
echo "::add-mask::${keychain_password}"

# Create with the .keychain suffix, which is what apple-actions passes.
# Under ~/Library/Keychains the Security framework appends -db to whatever
# path it is given, so this produces the .keychain-db file every later
# command names. Passing the extensionless path instead produces a file
# ending in a bare -db, and each later command then names a keychain that
# does not exist.
security create-keychain -p "$keychain_password" "${KEYCHAIN_NOEXT}.keychain"
# Keep it unlocked for the job: the default is a six-hour idle relock,
# which a long build can cross, and a relocked keychain fails signing
# with a prompt nobody can answer.
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$keychain_password" "$KEYCHAIN_PATH"

printf 'password=%s\n' "$keychain_password" >> "$GITHUB_OUTPUT"

- name: Import the Developer ID certificate
if: ${{ inputs.developer-id-p12-base64 != '' }}
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ inputs.developer-id-p12-base64 }}
p12-password: ${{ inputs.developer-id-p12-password }}
keychain: ${{ steps.keychain.outputs.path_noext }}
create-keychain: "false"
keychain-password: ${{ steps.create-keychain.outputs.password }}

- name: Import the Apple Distribution certificate
if: ${{ inputs.distribution-p12-base64 != '' }}
uses: apple-actions/import-codesign-certs@v3
with:
p12-file-base64: ${{ inputs.p12-base64 }}
p12-password: ${{ inputs.p12-password }}
p12-file-base64: ${{ inputs.distribution-p12-base64 }}
p12-password: ${{ inputs.distribution-p12-password }}
keychain: ${{ steps.keychain.outputs.path_noext }}
create-keychain: "false"
keychain-password: ${{ steps.create-keychain.outputs.password }}

- name: Scope the user keychain search list to the signing keychain
shell: bash
Expand All @@ -78,7 +153,7 @@ runs:
# "No certificate for team". Set the list explicitly to the signing
# keychain plus the System keychain: the signing keychain supplies the
# identity and the System keychain supplies the trust anchors that
# validate the Developer ID chain.
# validate the certificate chain.
security list-keychains -d user -s "$KEYCHAIN_PATH" /Library/Keychains/System.keychain

- name: Resolve identity SHA-1
Expand All @@ -99,7 +174,12 @@ runs:
)"

if [[ -z "$identity_sha1" ]]; then
echo "Developer ID Application identity '$IDENTITY_NAME' not found in $KEYCHAIN_PATH" >&2
echo "signing identity '$IDENTITY_NAME' not found in $KEYCHAIN_PATH" >&2
echo "identities that were imported:" >&2
# Names only. This is what tells a reader whether the wrong
# certificate was supplied rather than the right one failing to
# validate, which is otherwise indistinguishable from this failure.
security find-identity -p codesigning "$KEYCHAIN_PATH" >&2
exit 1
fi

Expand Down
46 changes: 39 additions & 7 deletions .github/actions/workflow-helper/workflow-helper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ private enum WorkflowHelperError: LocalizedError {
case invalidJSONShape(label: String)
case invalidJSONElement(label: String)
case failedCommand(command: String, exitStatus: Int32)
case missingSigningCertificate
case missingSigningSecret(String)

var errorDescription: String? {
Expand All @@ -41,6 +42,14 @@ private enum WorkflowHelperError: LocalizedError {
return "\(label) must contain only strings"
case let .failedCommand(command, exitStatus):
return "\(command) exited with status \(exitStatus)"
case .missingSigningCertificate:
return
"workflow-helper: signed CI needs a certificate, and neither "
+ "APPLE_DEVELOPER_ID_P12_BASE64 nor APPLE_DISTRIBUTION_P12_BASE64 is set. "
+ "Set the one this repository signs CI with: Apple Distribution when the "
+ "runner is not a registered device, Developer ID otherwise. "
+ "If this fails in a Dependabot run, check that the same secret name is "
+ "available as a Dependabot secret"
case let .missingSigningSecret(name):
return
"workflow-helper: \(name) is required for signed CI; "
Expand Down Expand Up @@ -142,20 +151,43 @@ private func requireSigningSecret(_ present: Bool, name: String) throws {

private func validateSigningInputs(environment: Environment) throws {
if environment.bool("IMPORT_SIGNING_CERT") {
try requireSigningCertificate(environment: environment)
}

if environment.bool("INSTALL_PROVISIONING_PROFILE") {
try requireSigningSecret(
environment.bool("HAS_SIGNING_CERT"),
name: "APPLE_DEVELOPER_ID_P12_BASE64"
environment.bool("HAS_PROVISIONING_PROFILE"),
name: "APPLE_DEVELOPER_ID_PROFILE_BASE64"
)
}
}

/// A signed build needs at least one certificate, and each certificate it does
/// supply needs its password.
///
/// Which certificate is right depends on where the build runs. A machine that is
/// not a registered device cannot use development provisioning, so it signs with
/// Apple Distribution and App Store profiles; a build producing something a person
/// downloads signs with Developer ID. A repository that does both supplies both,
/// and `signing-identity-name` decides which one a given build uses. So this
/// refuses only the case where neither was supplied, rather than naming one.
private func requireSigningCertificate(environment: Environment) throws {
let hasDeveloperID = environment.bool("HAS_DEVELOPER_ID_CERT")
let hasDistribution = environment.bool("HAS_DISTRIBUTION_CERT")

guard hasDeveloperID || hasDistribution else {
throw WorkflowHelperError.missingSigningCertificate
}
if hasDeveloperID {
try requireSigningSecret(
environment.bool("HAS_SIGNING_PASSWORD"),
environment.bool("HAS_DEVELOPER_ID_PASSWORD"),
name: "APPLE_DEVELOPER_ID_P12_PASSWORD"
)
}

if environment.bool("INSTALL_PROVISIONING_PROFILE") {
if hasDistribution {
try requireSigningSecret(
environment.bool("HAS_PROVISIONING_PROFILE"),
name: "APPLE_DEVELOPER_ID_PROFILE_BASE64"
environment.bool("HAS_DISTRIBUTION_PASSWORD"),
name: "APPLE_DISTRIBUTION_P12_PASSWORD"
)
}
}
Expand Down
22 changes: 22 additions & 0 deletions .github/workflows/_ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,24 @@ on:
type: string
default: agk-local-macos-26
secrets:
# The two certificates a build can sign with. Set whichever ones this
# repository actually signs with; `signing-identity-name` decides which one
# a given build uses, so passing both is normal for a repository that signs
# its CI and its release differently.
#
# A repository whose CI runner is not a registered device cannot use
# development provisioning, so its CI signs with Apple Distribution and App
# Store profiles, which carry no device list. It still releases with
# Developer ID, because a downloadable app cannot be App Store signed. Such a
# repository sets both.
APPLE_DEVELOPER_ID_P12_BASE64:
required: false
APPLE_DEVELOPER_ID_P12_PASSWORD:
required: false
APPLE_DISTRIBUTION_P12_BASE64:
required: false
APPLE_DISTRIBUTION_P12_PASSWORD:
required: false
APPLE_DEVELOPER_ID_PROFILE_BASE64:
required: false
# App Store Connect API key for iOS automatic signing
Expand Down Expand Up @@ -353,6 +367,8 @@ jobs:
fetch-depth: ${{ inputs.fetch-depth }}
apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }}
apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
apple-distribution-p12-base64: ${{ secrets.APPLE_DISTRIBUTION_P12_BASE64 }}
apple-distribution-p12-password: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }}
apple-developer-id-profile-base64: ${{ secrets.APPLE_DEVELOPER_ID_PROFILE_BASE64 }}
apple-notary-key-base64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }}
apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }}
Expand Down Expand Up @@ -400,6 +416,8 @@ jobs:
fetch-depth: ${{ inputs.fetch-depth }}
apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }}
apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
apple-distribution-p12-base64: ${{ secrets.APPLE_DISTRIBUTION_P12_BASE64 }}
apple-distribution-p12-password: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }}
apple-developer-id-profile-base64: ${{ secrets.APPLE_DEVELOPER_ID_PROFILE_BASE64 }}
apple-notary-key-base64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }}
apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }}
Expand Down Expand Up @@ -526,6 +544,8 @@ jobs:
fetch-depth: ${{ inputs.fetch-depth }}
apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }}
apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
apple-distribution-p12-base64: ${{ secrets.APPLE_DISTRIBUTION_P12_BASE64 }}
apple-distribution-p12-password: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }}
apple-developer-id-profile-base64: ${{ secrets.APPLE_DEVELOPER_ID_PROFILE_BASE64 }}
apple-notary-key-base64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }}
apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }}
Expand Down Expand Up @@ -573,6 +593,8 @@ jobs:
fetch-depth: ${{ inputs.fetch-depth }}
apple-developer-id-p12-base64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }}
apple-developer-id-p12-password: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
apple-distribution-p12-base64: ${{ secrets.APPLE_DISTRIBUTION_P12_BASE64 }}
apple-distribution-p12-password: ${{ secrets.APPLE_DISTRIBUTION_P12_PASSWORD }}
apple-developer-id-profile-base64: ${{ secrets.APPLE_DEVELOPER_ID_PROFILE_BASE64 }}
apple-notary-key-base64: ${{ secrets.APPLE_NOTARY_KEY_BASE64 }}
apple-notary-key-id: ${{ secrets.APPLE_NOTARY_KEY_ID }}
Expand Down
Loading
Loading