diff --git a/.github/workflows/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md similarity index 100% rename from .github/workflows/PULL_REQUEST_TEMPLATE.md rename to .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/workflows/label-add-to-project.yml b/.github/workflows/label-add-to-project.yml index 3e0baed..b1ce6e1 100644 --- a/.github/workflows/label-add-to-project.yml +++ b/.github/workflows/label-add-to-project.yml @@ -9,7 +9,6 @@ on: jobs: add_to_project: - if: github.event_name != 'pull_request' || !startsWith(github.head_ref, 'dependabot/') runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -22,7 +21,7 @@ jobs: steps: - name: Add issue/PR to GitHub Project if: github.event.label.name == '${{ matrix.flag }}' - uses: actions/add-to-project@v1.0.0 + uses: actions/add-to-project@v2 with: project-url: https://github.com/orgs/lando/projects/10 github-token: ${{ secrets.SHADOW_COUNCIL_ESCALATOR }} diff --git a/.github/workflows/pr-azure-tests.yaml b/.github/workflows/pr-azure-tests.yaml new file mode 100644 index 0000000..d08af42 --- /dev/null +++ b/.github/workflows/pr-azure-tests.yaml @@ -0,0 +1,49 @@ +name: PR Azure Tests + +on: + pull_request: + +permissions: + contents: read + id-token: write + +jobs: + azure-artifact-signing-tests: + runs-on: windows-2025 + env: + term: xterm + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Install node + uses: actions/setup-node@v6 + with: + node-version-file: .node-version + cache: npm + - name: Install dependencies + shell: bash + run: npm ci --prefer-offline + - name: Package node binary + shell: bash + run: npm run build + - name: Codesign + id: code-sign-action + uses: ./ + with: + file: ${{ github.workspace }}\dist\@lando\code-sign-action.exe + certificate-id: lando + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + azure-signing-account-name: lando + azure-signing-endpoint: https://eus.codesigning.azure.net/ + - name: Test output + shell: bash + run: | + echo "${{ steps.code-sign-action.outputs.file }}" + stat "${{ steps.code-sign-action.outputs.file }}" + - name: Execute file + shell: bash + run: chmod +x "${{ steps.code-sign-action.outputs.file }}" && "${{ steps.code-sign-action.outputs.file }}" diff --git a/.github/workflows/pr-default-tests.yaml b/.github/workflows/pr-default-tests.yaml new file mode 100644 index 0000000..41d378b --- /dev/null +++ b/.github/workflows/pr-default-tests.yaml @@ -0,0 +1,67 @@ +name: PR Default Tests + +on: + pull_request: + +jobs: + default-signtool-tests: + runs-on: ${{ matrix.os }} + env: + term: xterm + strategy: + fail-fast: false + matrix: + include: + - os: macos-15 + file: dist/@lando/code-sign-action + - os: macos-15 + file: /Users/runner/work/code-sign-action/code-sign-action/dist/@lando/code-sign-action + - os: windows-2025 + file: dist\@lando\code-sign-action.exe + - os: windows-2025 + file: D:\a\code-sign-action\code-sign-action/dist\@lando\code-sign-action.exe + + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Install node + uses: actions/setup-node@v6 + with: + node-version-file: .node-version + cache: npm + - name: Install dependencies + shell: bash + run: npm ci --prefer-offline + - name: Package node binary + shell: bash + run: npm run build + - name: Trust Apple signer test anchor on Windows + if: runner.os == 'Windows' + shell: powershell + env: + APPLE_CERT_DATA: ${{ secrets.APPLE_CERT_DATA }} + APPLE_CERT_PASSWORD: ${{ secrets.APPLE_CERT_PASSWORD }} + run: | + .\scripts\trust-apple-signer.ps1 ` + -CertificateData $env:APPLE_CERT_DATA ` + -CertificatePassword $env:APPLE_CERT_PASSWORD ` + -ExpectedSubject "*466VYKC9T5*" + + - name: Codesign + id: code-sign-action + uses: ./ + with: + file: ${{ matrix.file }} + certificate-data: ${{ secrets.APPLE_CERT_DATA }} + certificate-id: 466VYKC9T5 + certificate-password: ${{ secrets.APPLE_CERT_PASSWORD }} + - name: Test output + shell: bash + run: | + echo "${{ steps.code-sign-action.outputs.file }}" + stat "${{ steps.code-sign-action.outputs.file }}" + - name: Execute file + shell: bash + run: chmod +x "${{ steps.code-sign-action.outputs.file }}" && "${{ steps.code-sign-action.outputs.file }}" diff --git a/.github/workflows/pr-keylocker-tests.yaml b/.github/workflows/pr-keylocker-tests.yaml new file mode 100644 index 0000000..e4ef62f --- /dev/null +++ b/.github/workflows/pr-keylocker-tests.yaml @@ -0,0 +1,49 @@ +name: PR KeyLocker Tests + +# PR triggering is disabled because the current KeyLocker account/secrets are no longer valid. +on: + workflow_dispatch: + # pull_request: + +jobs: + keylocker-tests: + runs-on: windows-2025 + env: + term: xterm + strategy: + fail-fast: false + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Install node + uses: actions/setup-node@v6 + with: + node-version-file: .node-version + cache: npm + - name: Install dependencies + shell: bash + run: npm ci --prefer-offline + - name: Package node binary + shell: bash + run: npm run build + - name: Codesign + id: code-sign-action + uses: ./ + with: + file: dist/@lando/code-sign-action.exe + certificate-data: ${{ secrets.KEYLOCKER_CLIENT_CERT }} + certificate-password: ${{ secrets.KEYLOCKER_CLIENT_CERT_PASSWORD }} + keylocker-host: https://clientauth.one.digicert.com + keylocker-api-key: ${{ secrets.KEYLOCKER_API_KEY }} + keylocker-cert-sha1-hash: ${{ secrets.KEYLOCKER_CERT_SHA1_HASH }} + keylocker-keypair-alias: ${{ secrets.KEYLOCKER_KEYPAIR_ALIAS }} + - name: Test output + shell: bash + run: | + echo "${{ steps.code-sign-action.outputs.file }}" + stat "${{ steps.code-sign-action.outputs.file }}" + - name: Execute file + shell: bash + run: chmod +x "${{ steps.code-sign-action.outputs.file }}" && "${{ steps.code-sign-action.outputs.file }}" diff --git a/.github/workflows/pr-notarization-tests.yaml b/.github/workflows/pr-notarization-tests.yaml new file mode 100644 index 0000000..96b4653 --- /dev/null +++ b/.github/workflows/pr-notarization-tests.yaml @@ -0,0 +1,46 @@ +name: PR Notarization Tests + +on: + pull_request: + +jobs: + notarize-tests: + runs-on: macos-15 + env: + term: xterm + steps: + - name: Checkout code + uses: actions/checkout@v7 + with: + fetch-depth: 0 + - name: Install node + uses: actions/setup-node@v6 + with: + node-version-file: .node-version + cache: npm + - name: Install dependencies + shell: bash + run: npm ci --prefer-offline + - name: Package node binary + shell: bash + run: npm run build + - name: Codesign + id: code-sign-action + uses: ./ + with: + file: dist/@lando/code-sign-action + certificate-data: ${{ secrets.APPLE_CERT_DATA }} + certificate-password: ${{ secrets.APPLE_CERT_PASSWORD }} + apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }} + apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }} + apple-team-id: 466VYKC9T5 + apple-product-id: dev.lando.code-sign-action + options: --options runtime --entitlements entitlements.xml + - name: Test output + shell: bash + run: | + echo "${{ steps.code-sign-action.outputs.file }}" + stat "${{ steps.code-sign-action.outputs.file }}" + - name: Execute file + shell: bash + run: chmod +x "${{ steps.code-sign-action.outputs.file }}" && "${{ steps.code-sign-action.outputs.file }}" diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml deleted file mode 100644 index 31b4182..0000000 --- a/.github/workflows/pr-tests.yml +++ /dev/null @@ -1,185 +0,0 @@ -name: PR Tests - -on: - pull_request: - -jobs: - default-signtool-tests: - if: '!startsWith(github.head_ref, ''dependabot/'')' - runs-on: ${{ matrix.os }} - env: - term: xterm - strategy: - fail-fast: false - matrix: - include: - - os: macos-14 - file: dist/@lando/code-sign-action - node-version: '20' - - os: macos-14 - file: /Users/runner/work/code-sign-action/code-sign-action/dist/@lando/code-sign-action - node-version: '20' - - os: windows-2022 - file: dist\@lando\code-sign-action.exe - node-version: '20' - - os: windows-2022 - file: D:\a\code-sign-action\code-sign-action/dist\@lando\code-sign-action.exe - node-version: '20' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install node ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - name: Install dependencies - shell: bash - run: npm clean-install --prefer-offline --frozen-lockfile - - name: Package node binary - shell: bash - run: npm run build - - name: Trust test certs - if: runner.os == 'macOS' - shell: bash - run: | - # dump - echo "${{ secrets.DEFAULT_CERT_DATA }}" | base64 --decode > /tmp/LandoCodeSigningTest.p12 - - # extract - openssl pkcs12 -in /tmp/LandoCodeSigningTest.p12 -clcerts -nokeys -out /tmp/LandoCodeSigningTest.pem -password pass:${{ secrets.DEFAULT_CERT_PASSWORD }} - - # trust - sudo security authorizationdb write com.apple.trust-settings.user allow - sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/LandoCodeSigningTest.pem - sudo security find-certificate -a -c "Lando System Code Signing Test Certificate" -p /Library/Keychains/System.keychain - - name: Trust test certs - if: runner.os == 'Windows' - shell: powershell - run: | - # vars - $temp_dir = "$env:TMP" - $cert_data = "${{ secrets.DEFAULT_CERT_DATA }}" - $cert_path = "$temp_dir\LandoCodeSigningTest.p12" - $cert_password = "${{ secrets.DEFAULT_CERT_PASSWORD }}" - $cert_secure_password = ConvertTo-SecureString -String $cert_password -Force -AsPlainText - $cert_store = "Cert:\LocalMachine\Root" - - # dump - If (!(Test-Path $cert_path)) { - Write-Output "Dumping cert to $cert_path..." - $bytes = [Convert]::FromBase64String($cert_data) - [IO.File]::WriteAllBytes($cert_path, $bytes) - } - - # trust - Import-PfxCertificate -FilePath $cert_path -CertStoreLocation $cert_store -Password $cert_secure_password - - name: Codesign - id: code-sign-action - uses: ./ - with: - file: ${{ matrix.file }} - certificate-data: ${{ secrets.DEFAULT_CERT_DATA }} - certificate-id: LSL337X6 - certificate-password: ${{ secrets.DEFAULT_CERT_PASSWORD }} - - name: Test output - shell: bash - run: | - echo "${{ steps.code-sign-action.outputs.file }}" - stat "${{ steps.code-sign-action.outputs.file }}" - - name: Execute file - shell: bash - run: chmod +x "${{ steps.code-sign-action.outputs.file }}" && "${{ steps.code-sign-action.outputs.file }}" - - keylocker-tests: - if: '!startsWith(github.head_ref, ''dependabot/'')' - runs-on: windows-2022 - env: - term: xterm - strategy: - fail-fast: false - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install node ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - name: Install dependencies - shell: bash - run: npm clean-install --prefer-offline --frozen-lockfile - - name: Package node binary - shell: bash - run: npm run build - - name: Codesign - id: code-sign-action - uses: ./ - with: - file: dist/@lando/code-sign-action.exe - certificate-data: ${{ secrets.KEYLOCKER_CLIENT_CERT }} - certificate-password: ${{ secrets.KEYLOCKER_CLIENT_CERT_PASSWORD }} - keylocker-host: https://clientauth.one.digicert.com - keylocker-api-key: ${{ secrets.KEYLOCKER_API_KEY }} - keylocker-cert-sha1-hash: ${{ secrets.KEYLOCKER_CERT_SHA1_HASH }} - keylocker-keypair-alias: ${{ secrets.KEYLOCKER_KEYPAIR_ALIAS }} - - name: Test output - shell: bash - run: | - echo "${{ steps.code-sign-action.outputs.file }}" - stat "${{ steps.code-sign-action.outputs.file }}" - - name: Execute file - shell: bash - run: chmod +x "${{ steps.code-sign-action.outputs.file }}" && "${{ steps.code-sign-action.outputs.file }}" - - notarize-tests: - if: '!startsWith(github.head_ref, ''dependabot/'')' - runs-on: macos-14 - env: - term: xterm - strategy: - fail-fast: false - matrix: - node-version: - - '20' - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Install node ${{ matrix.node-version }} - uses: actions/setup-node@v4 - with: - node-version: ${{ matrix.node-version }} - cache: npm - - name: Install dependencies - shell: bash - run: npm clean-install --prefer-offline --frozen-lockfile - - name: Package node binary - shell: bash - run: npm run build - - name: Codesign - id: code-sign-action - uses: ./ - with: - file: dist/@lando/code-sign-action - certificate-data: ${{ secrets.APPLE_CERT_DATA }} - certificate-password: ${{ secrets.APPLE_CERT_PASSWORD }} - apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }} - apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }} - apple-team-id: FY8GAUX282 - apple-product-id: dev.lando.code-sign-action - options: --options runtime --entitlements entitlements.xml - - name: Test output - shell: bash - run: | - echo "${{ steps.code-sign-action.outputs.file }}" - stat "${{ steps.code-sign-action.outputs.file }}" - - name: Execute file - shell: bash - run: chmod +x "${{ steps.code-sign-action.outputs.file }}" && "${{ steps.code-sign-action.outputs.file }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0bfb60..56a0538 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,21 +15,21 @@ jobs: matrix: os: - ubuntu-24.04 - node-version: - - '20' steps: - name: Checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v7 + with: + fetch-depth: 0 - name: Export formatted release date run: echo "RELEASE_DATE=$(date -d "${{ github.event.release.published_at }}" "+%B %e, %Y" | tr -s ' ')" >> $GITHUB_ENV - - name: Install node ${{ matrix.node-version }} - uses: actions/setup-node@v4 + - name: Install node + uses: actions/setup-node@v6 with: - node-version: ${{ matrix.node-version }} + node-version-file: .node-version cache: npm - name: Install dependencies - run: npm clean-install + run: npm ci - name: Prepare release uses: lando/prepare-release-action@v3 with: diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..a45fd52 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +24 diff --git a/README.md b/README.md index 9ceec5a..2b51fc7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ It will automatically set the `signtool` based on `runner.os` and the inputs tha Note that signing is **not supported** on `linux` because you cannot sign binary files on Linux and it is not required for binaries to be signed anyway. -## Required Inputs +## Usage ### Signtool @@ -25,7 +25,7 @@ These keys must be set correctly on a `windows` runner. ```yaml jobs: sign: - runs-on: windows-2022 + runs-on: windows-2025 steps: - name: Sign binary uses: lando/code-sign-action@v3 @@ -37,7 +37,7 @@ jobs: ### Codesign -[Codesign](https://ss64.com/mac/codesign.htmll) is the default (and currently only) `signtool` when using `macos` runners. +[Codesign](https://ss64.com/mac/codesign.html) is the default (and currently only) `signtool` when using `macos` runners. These keys must be set correctly on a `macos` runner. @@ -51,7 +51,7 @@ These keys must be set correctly on a `macos` runner. ```yaml jobs: sign: - runs-on: macos-14 + runs-on: macos-15 steps: - name: Sign binary uses: lando/code-sign-action@v3 @@ -86,26 +86,71 @@ These keys must be set correctly on a `macos` runner. ```yaml jobs: package: - runs-on: macos-11 - steps: - name: Sign binary - uses: lando/code-sign-action@v2 - with: - file: path/to/binary - certificate-data: ${{ secrets.APPLE_CERT_DATA }} - certificate-password: ${{ secrets.APPLE_CERT_PASSWORD }} - apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }} - apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }} - apple-notary-tool: altool - apple-team-id: FY8GAUX282 - apple-product-id: dev.lando.code-sign-action - options: --options runtime --entitlements entitlements.xml + runs-on: macos-15 + steps: + - name: Sign binary + uses: lando/code-sign-action@v3 + with: + file: path/to/binary + certificate-data: ${{ secrets.APPLE_CERT_DATA }} + certificate-password: ${{ secrets.APPLE_CERT_PASSWORD }} + apple-notary-user: ${{ secrets.APPLE_NOTARY_USER }} + apple-notary-password: ${{ secrets.APPLE_NOTARY_PASSWORD }} + apple-team-id: FY8GAUX282 + apple-product-id: dev.lando.code-sign-action + options: --options runtime --entitlements entitlements.xml ``` Note that it's only possible to `codesign` and `notarize` using an Apple Developer certificate. Also note that you _probably_ need to set the `options` as above. You can look [here](https://github.com/lando/code-sign-action/blob/main/entitlements.xml) for an example `entitlements.xml` but you will want to configure it to your needs. +### Azure Artifact Signing + +You can also sign on `windows` runners using [Azure Artifact Signing](https://learn.microsoft.com/en-us/azure/trusted-signing/). + +When the Azure inputs are present, `signtool: auto` selects Azure Artifact Signing. You can also set `signtool: azure` explicitly. + +The workflow job must grant OIDC permissions so this action can authenticate with Azure. + +```yaml +permissions: + contents: read + id-token: write +``` + +These keys must be set correctly on a `windows` runner. + +| Name | Description | Example Value | +|---|---|---| +| `file` | The file to sign. | `bin/test.exe` | +| `certificate-id` | The Azure Artifact Signing certificate profile name. | `lando` | +| `azure-client-id` | The Azure application client ID. | `${{ secrets.AZURE_CLIENT_ID }}` | +| `azure-tenant-id` | The Azure tenant ID. | `${{ secrets.AZURE_TENANT_ID }}` | +| `azure-subscription-id` | The Azure subscription ID. | `${{ secrets.AZURE_SUBSCRIPTION_ID }}` | +| `azure-signing-account-name` | The Azure Artifact Signing account name. | `lando` | +| `azure-signing-endpoint` | The Azure Artifact Signing endpoint. | `https://eus.codesigning.azure.net/` | + +```yaml +jobs: + sign: + runs-on: windows-2025 + permissions: + contents: read + id-token: write + steps: + - name: Sign binary + uses: lando/code-sign-action@v3 + with: + file: path/to/binary.exe + certificate-id: lando + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + azure-subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + azure-signing-account-name: lando + azure-signing-endpoint: https://eus.codesigning.azure.net/ +``` + ### KeyLocker You can also sign on `windows` runners using [KeyLocker](https://docs.digicert.com/zh/digicert-keylocker.html) by setting the additional `keylocker` inputs as below: @@ -125,7 +170,7 @@ These keys must be set correctly on a `windows` runner. ```yaml jobs: sign: - runs-on: windows-2022 + runs-on: windows-2025 steps: - name: Sign binary uses: lando/code-sign-action@v3 @@ -176,4 +221,3 @@ Made with [contrib.rocks](https://contrib.rocks). * [TERMS OF USE](https://docs.lando.dev/terms) * [PRIVACY POLICY](https://docs.lando.dev/privacy) * [CODE OF CONDUCT](https://docs.lando.dev/coc) - diff --git a/action.yml b/action.yml index 1004eaa..c103b98 100644 --- a/action.yml +++ b/action.yml @@ -1,19 +1,19 @@ name: "Code Sign Action" -description: "A GitHub Action for code signing files. Particularly those generated with vercel/pkg." +description: "A GitHub Action for code signing files. Particularly those generated with @yao-pkg/pkg." branding: color: purple icon: lock inputs: - # Required + # Common file: description: "The file to sign" required: true certificate-data: - description: "A `base64` encoded string of your `p12` or `pfx` cert contents. Note: if you use KeyLocker, this will be the base64 encoded client certificate (`SM_CLIENT_CERT_FILE`)" - required: true + description: "A `base64` encoded string of your `p12` or `pfx` cert contents. Note: if you use KeyLocker, this will be the base64 encoded client certificate (`SM_CLIENT_CERT_FILE`). Not required for Azure Artifact Signing." + required: false certificate-password: - description: "The password to unlock the certificate-data" - required: true + description: "The password to unlock the certificate-data. Not required for Azure Artifact Signing." + required: false # Optional apple-notary-password: @@ -34,7 +34,22 @@ inputs: required: false certificate-id: description: "A string to identify the correct signing cert" - required: true + required: false + azure-client-id: + description: "The Azure application client ID to use for Azure Artifact Signing" + required: false + azure-signing-account-name: + description: "The Azure Artifact Signing account name" + required: false + azure-signing-endpoint: + description: "The Azure Artifact Signing endpoint" + required: false + azure-subscription-id: + description: "The Azure subscription ID to use for Azure Artifact Signing" + required: false + azure-tenant-id: + description: "The Azure tenant ID to use for Azure Artifact Signing" + required: false keylocker-api-key: description: "The API key to use for KeyLocker" required: false @@ -73,20 +88,6 @@ runs: fi echo "::endgroup::" - echo "::group::Ensure cert data is set" - if [ "${{ inputs.certificate-data }}" == "" ]; then - echo "::error title=Cert data is not set!::You must specify the cert you want to sign with!" - exit 2 - fi - echo "::endgroup::" - - echo "::group::Ensure certificate password is set" - if [ "${{ inputs.certificate-password }}" == "" ]; then - echo "::error title=Cert password is not set!::You must specify the password to unlock the cert with!" - exit 2 - fi - echo "::endgroup::" - - name: Set generic internal inputs id: code-sign-action-generic-internal shell: bash @@ -108,9 +109,24 @@ runs: fi # signtool discovery if needed - if [ "${{ inputs.signtool }}" == "auto" ]; then + if [ "${{ inputs.signtool }}" != "auto" ]; then + echo "signtool=${{ inputs.signtool }}" >> $GITHUB_OUTPUT + else + azure_inputs_set=false + if [[ -n "${{ inputs.azure-client-id }}" ]] \ + || [[ -n "${{ inputs.azure-signing-account-name }}" ]] \ + || [[ -n "${{ inputs.azure-signing-endpoint }}" ]] \ + || [[ -n "${{ inputs.azure-subscription-id }}" ]] \ + || [[ -n "${{ inputs.azure-tenant-id }}" ]]; then + azure_inputs_set=true + fi + + # azure + if [ "${{ runner.os }}" == "Windows" ] && [ "$azure_inputs_set" == "true" ]; then + echo "signtool=azure" >> $GITHUB_OUTPUT + # keylocker - if [ "${{ runner.os }}" != "macOS" ] \ + elif [ "${{ runner.os }}" != "macOS" ] \ && [[ -n "${{ inputs.keylocker-api-key }}" ]] \ && [[ -n "${{ inputs.keylocker-cert-sha1-hash }}" ]] \ && [[ -n "${{ inputs.keylocker-host }}" ]] \ @@ -237,6 +253,24 @@ runs: echo "signtool=${{ steps.code-sign-action-internal.outputs.signtool }}" echo "::endgroup::" + - name: Validate certificate inputs + if: steps.code-sign-action-internal.outputs.os != 'linux' && steps.code-sign-action-internal.outputs.signtool != 'azure' + shell: bash + run: | + echo "::group::Ensure cert data is set" + if [ "${{ inputs.certificate-data }}" == "" ]; then + echo "::error title=Cert data is not set!::You must specify the cert you want to sign with!" + exit 2 + fi + echo "::endgroup::" + + echo "::group::Ensure certificate password is set" + if [ "${{ inputs.certificate-password }}" == "" ]; then + echo "::error title=Cert password is not set!::You must specify the password to unlock the cert with!" + exit 2 + fi + echo "::endgroup::" + - name: Validate linux specific inputs if: steps.code-sign-action-internal.outputs.os == 'linux' shell: bash @@ -249,7 +283,7 @@ runs: run: | # supported codesigner if [ "${{ steps.code-sign-action-internal.outputs.signtool }}" != "codesign" ]; then - echo "::error::You must specify osslsigncode, keylocker or smctl as your signtool! You specified ${{ steps.code-sign-action-internal.outputs.signtool }}." + echo "::error::You must specify codesign as your signtool on macOS! You specified ${{ steps.code-sign-action-internal.outputs.signtool }}." exit 1 fi @@ -265,11 +299,50 @@ runs: # supported codesigner if [ "${{ steps.code-sign-action-internal.outputs.signtool }}" != "signtool" ] \ && [ "${{ steps.code-sign-action-internal.outputs.signtool }}" != "keylocker" ] \ - && [ "${{ steps.code-sign-action-internal.outputs.signtool }}" != "smctl" ]; then - echo "::error::You must specify osslsigncode, keylocker or smctl as your signtool! You specified ${{ steps.code-sign-action-internal.outputs.signtool }}." + && [ "${{ steps.code-sign-action-internal.outputs.signtool }}" != "smctl" ] \ + && [ "${{ steps.code-sign-action-internal.outputs.signtool }}" != "azure" ]; then + echo "::error::You must specify signtool, keylocker, smctl or azure as your signtool on Windows! You specified ${{ steps.code-sign-action-internal.outputs.signtool }}." + exit 1 + fi + - name: Validate azure specific inputs + if: steps.code-sign-action-internal.outputs.signtool == 'azure' + shell: bash + run: | + if [ "${{ steps.code-sign-action-internal.outputs.os }}" != "win" ]; then + echo "::error::Azure Artifact Signing is only supported on Windows runners." exit 1 fi + if [ "${{ inputs.azure-client-id }}" == "" ]; then + echo "::error title=Azure client ID is not set!::You must specify azure-client-id to sign with Azure Artifact Signing." + exit 2 + fi + + if [ "${{ inputs.azure-signing-account-name }}" == "" ]; then + echo "::error title=Azure signing account name is not set!::You must specify azure-signing-account-name to sign with Azure Artifact Signing." + exit 2 + fi + + if [ "${{ inputs.azure-signing-endpoint }}" == "" ]; then + echo "::error title=Azure signing endpoint is not set!::You must specify azure-signing-endpoint to sign with Azure Artifact Signing." + exit 2 + fi + + if [ "${{ inputs.azure-subscription-id }}" == "" ]; then + echo "::error title=Azure subscription ID is not set!::You must specify azure-subscription-id to sign with Azure Artifact Signing." + exit 2 + fi + + if [ "${{ inputs.azure-tenant-id }}" == "" ]; then + echo "::error title=Azure tenant ID is not set!::You must specify azure-tenant-id to sign with Azure Artifact Signing." + exit 2 + fi + + if [ "${{ steps.code-sign-action-internal.outputs.signid }}" == "" ]; then + echo "::error title=Azure certificate profile name is not set!::You must specify certificate-id to sign with Azure Artifact Signing." + exit 2 + fi + - name: Dump certs if: steps.code-sign-action-internal.outputs.os == 'macos' shell: bash @@ -279,7 +352,7 @@ runs: echo "${{ inputs.certificate-data }}" | base64 --decode > "${{ steps.code-sign-action-internal.outputs.signcert }}" echo "::endgroup::" - name: Dump certs - if: steps.code-sign-action-internal.outputs.os == 'win' + if: steps.code-sign-action-internal.outputs.os == 'win' && steps.code-sign-action-internal.outputs.signtool != 'azure' shell: powershell run: | echo "::group::Dumping inputs.certificate-data" @@ -376,9 +449,45 @@ runs: Write-Output "Verifying $file has been signed with the signtool..." & $signtool verify -pa -v "${{ steps.code-sign-action-internal.outputs.file }}" + - name: Azure login + if: steps.code-sign-action-internal.outputs.signtool == 'azure' + uses: azure/login@v3 + with: + client-id: ${{ inputs.azure-client-id }} + tenant-id: ${{ inputs.azure-tenant-id }} + subscription-id: ${{ inputs.azure-subscription-id }} + - name: Signing with ${{ steps.code-sign-action-internal.outputs.signtool }} (${{ steps.code-sign-action-internal.outputs.os }}) + if: steps.code-sign-action-internal.outputs.signtool == 'azure' + uses: azure/artifact-signing-action@v2 + with: + endpoint: ${{ inputs.azure-signing-endpoint }} + signing-account-name: ${{ inputs.azure-signing-account-name }} + certificate-profile-name: ${{ steps.code-sign-action-internal.outputs.signid }} + files: ${{ steps.code-sign-action-internal.outputs.file }} + file-digest: SHA256 + timestamp-rfc3161: http://timestamp.acs.microsoft.com + timestamp-digest: SHA256 + - name: Verifying Azure signature + if: steps.code-sign-action-internal.outputs.signtool == 'azure' + shell: powershell + run: | + $ErrorActionPreference = "Stop" + + $signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" -ErrorAction SilentlyContinue | + Sort-Object { [version]$_.Directory.Parent.Name } -Descending | + Select-Object -First 1 -ExpandProperty FullName + if ([string]::IsNullOrEmpty($signtool)) { + throw "Could not find signtool.exe" + } + + & $signtool verify -pa -v "${{ steps.code-sign-action-internal.outputs.file }}" + if ($LASTEXITCODE -ne 0) { + throw "signtool verify failed with exit code $LASTEXITCODE" + } + - name: Signing with ${{ steps.code-sign-action-internal.outputs.signtool }} (${{ steps.code-sign-action-internal.outputs.os }}) if: steps.code-sign-action-internal.outputs.signtool == 'keylocker' || steps.code-sign-action-internal.outputs.signtool == 'smctl' - uses: cognitedata/code-sign-action/@v3 + uses: cognitedata/code-sign-action@v3 with: path-to-binary: ${{ steps.code-sign-action-internal.outputs.ghwrfile }} env: diff --git a/package-lock.json b/package-lock.json index 59316cd..8b709e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@yao-pkg/pkg": "^6.1.1" }, "engines": { - "node": ">=18.0.0" + "node": ">=24.0.0" } }, "node_modules/@babel/generator": { diff --git a/package.json b/package.json index a5e7c68..655abff 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "Github Actions" ], "engines": { - "node": ">=18.0.0" + "node": ">=24.0.0" }, "pkg": { "outputPath": "dist", @@ -26,12 +26,11 @@ "bin/*.js" ], "assets": [ - "package.json", - "yarn.lock" + "package.json" ] }, "scripts": { - "build": "yarn pkg -c package.json -t node14 bin/test" + "build": "pkg -c package.json -t node20 bin/test" }, "devDependencies": { "@yao-pkg/pkg": "^6.1.1" diff --git a/scripts/trust-apple-signer.ps1 b/scripts/trust-apple-signer.ps1 new file mode 100644 index 0000000..3cbdb76 --- /dev/null +++ b/scripts/trust-apple-signer.ps1 @@ -0,0 +1,188 @@ +<# +.SYNOPSIS +Trusts an Apple signing certificate as a Windows test anchor. + +.DESCRIPTION +This script imports a base64-encoded Apple signing PFX, exports the public +signing certificate, and imports that public certificate into Windows trust +stores. It is intended for GitHub Actions smoke tests that need signtool verify +to accept a shared Apple Developer ID certificate on Windows. + +The script intentionally trusts the leaf signing certificate directly. That +keeps signtool verification enabled while avoiding the Apple Developer ID +intermediate chain that Windows cannot validate because of Apple's critical +extension. + +Environment Variables: +APPLE_CERT_DATA Base64-encoded PFX data. Used when -CertificateData is omitted. +APPLE_CERT_PASSWORD PFX password. Used when -CertificatePassword is omitted. +CODE_SIGN_ACTION_DEBUG Enables debug output when set. +RUNNER_DEBUG Enables debug output in GitHub Actions when set. + +.EXAMPLE +.\scripts\trust-apple-signer.ps1 -CertificateData $env:APPLE_CERT_DATA -CertificatePassword $env:APPLE_CERT_PASSWORD + +.EXAMPLE +.\scripts\trust-apple-signer.ps1 -ExpectedSubject "*466VYKC9T5*" -Debug +#> +param( + # Base64-encoded Apple signing PFX data. + [string]$CertificateData = $env:APPLE_CERT_DATA, + # Password for the Apple signing PFX. + [string]$CertificatePassword = $env:APPLE_CERT_PASSWORD, + # Subject pattern used to select the expected signer from the imported PFX. + [string]$ExpectedSubject = "*", + # Certificate stores that should directly trust the signer certificate. + [string[]]$Stores = @( + "Cert:\LocalMachine\TrustedPeople", + "Cert:\LocalMachine\TrustedPublisher", + "Cert:\LocalMachine\Root" + ), + # Directory for temporary PFX and CER files. + [string]$TempDir = $env:TEMP, + # Leaves temporary files in place for runner debugging. + [switch]$KeepTempFiles, + # Shows debug messages. + [switch]$Debug, + # Displays this help message. + [switch]$Help +) + +Set-StrictMode -Version 3 +$ErrorActionPreference = "Stop" + +function Test-Truthy { + param([AllowNull()][object]$Value) + + $normalized = "" + if (-not [string]::IsNullOrWhiteSpace([string]$Value)) { + $normalized = ([string]$Value).Trim().ToLowerInvariant() + } + + switch ($normalized) { + "" { return $false } + "0" { return $false } + "false" { return $false } + "no" { return $false } + "off" { return $false } + default { return $true } + } +} + +$Debug = $Debug.IsPresent -or (Test-Truthy $env:RUNNER_DEBUG) -or (Test-Truthy $env:CODE_SIGN_ACTION_DEBUG) +$DebugPreference = if ($Debug) { "Continue" } else { $DebugPreference } +if ($DebugPreference -eq "Inquire" -or $DebugPreference -eq "Continue") { + $Debug = $true +} + +try { + $Host.PrivateData.DebugForegroundColor = "DarkGray" + $Host.PrivateData.DebugBackgroundColor = $Host.UI.RawUI.BackgroundColor +} catch { +} + +if ($Help) { + Get-Help $MyInvocation.MyCommand.Path -Detailed + return +} + +function Confirm-Value { + param( + [Parameter(Mandatory)] + [AllowNull()] + [string]$Value, + [Parameter(Mandatory)] + [string]$Name + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + throw "$Name is required." + } +} + +function Confirm-Environment { + Write-Debug "OS is $env:OS" + if ($env:OS -ne "Windows_NT") { + throw "This script is only supported on Windows." + } + + Write-Debug "PowerShell version: $($PSVersionTable.PSVersion.ToString())" + Write-Debug "Expected subject: $ExpectedSubject" + Write-Debug "Trust stores: $($Stores -join ', ')" +} + +function Import-SignerCertificate { + param( + [Parameter(Mandatory)] + [string]$PfxPath, + [Parameter(Mandatory)] + [securestring]$Password + ) + + Write-Host "Importing Apple signing certificate from APPLE_CERT_DATA..." + Import-PfxCertificate -FilePath $PfxPath -Password $Password -CertStoreLocation "Cert:\LocalMachine\My" | Out-Null + + $signer = Get-ChildItem "Cert:\LocalMachine\My" | + Where-Object { $_.HasPrivateKey -and $_.Subject -like $ExpectedSubject } | + Sort-Object NotAfter -Descending | + Select-Object -First 1 + + if ($null -eq $signer) { + throw "Could not find a private-key certificate matching '$ExpectedSubject' in Cert:\LocalMachine\My." + } + + Write-Host "Using signer certificate $($signer.Thumbprint): $($signer.Subject)" + Write-Debug "Signer issuer: $($signer.Issuer)" + Write-Debug "Signer expires: $($signer.NotAfter)" + + return $signer +} + +function Add-SignerToTrustStores { + param( + [Parameter(Mandatory)] + [System.Security.Cryptography.X509Certificates.X509Certificate2]$Signer, + [Parameter(Mandatory)] + [string]$CertPath + ) + + Export-Certificate -Cert $Signer -FilePath $CertPath | Out-Null + Write-Debug "Exported public signer certificate to $CertPath" + + foreach ($store in $Stores) { + Import-Certificate -FilePath $CertPath -CertStoreLocation $store | Out-Null + + $trusted = Get-ChildItem $store | + Where-Object { $_.Thumbprint -eq $Signer.Thumbprint } | + Select-Object -First 1 + + if ($null -eq $trusted) { + throw "Signer certificate $($Signer.Thumbprint) was not found in $store after import." + } + + Write-Host "Trusted signer certificate in ${store}: $($trusted.Thumbprint)" + } +} + +Confirm-Value -Value $CertificateData -Name "CertificateData" +Confirm-Value -Value $CertificatePassword -Name "CertificatePassword" +Confirm-Value -Value $TempDir -Name "TempDir" +Confirm-Environment + +$pfxPath = Join-Path $TempDir "apple-signer.p12" +$certPath = Join-Path $TempDir "apple-signer.cer" +$password = ConvertTo-SecureString $CertificatePassword -AsPlainText -Force + +try { + Write-Debug "Writing temporary PFX to $pfxPath" + [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($CertificateData)) + + $signer = Import-SignerCertificate -PfxPath $pfxPath -Password $password + Add-SignerToTrustStores -Signer $signer -CertPath $certPath +} finally { + if ($KeepTempFiles) { + Write-Debug "Keeping temporary certificate files in $TempDir" + } else { + Remove-Item -Path $pfxPath, $certPath -Force -ErrorAction SilentlyContinue + } +}