diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54cdcdbbd..078363e12 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -55,17 +55,19 @@ env: - config: # Human identifier for the job. name: Windows - runs-on: windows-2019 + runs-on: [self-hosted, windows-sign-pc] # The value is a string representing a JSON document. # Setting this to null causes the job to run directly in the runner machine instead of in a container. container: | null # Name of the secret that contains the certificate. - certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX + certificate-secret: INSTALLER_CERT_WINDOWS_CER # Name of the secret that contains the certificate password. - certificate-password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD + certificate-password-secret: INSTALLER_CERT_WINDOWS_PASSWORD # File extension for the certificate. certificate-extension: pfx + # Container for windows cert signing + certificate-container: INSTALLER_CERT_WINDOWS_CONTAINER # Quoting on the value is required here to allow the same comparison expression syntax to be used for this # and the companion needs.select-targets.outputs.merge-channel-files property (output values always have string # type). @@ -270,6 +272,8 @@ jobs: env: # Location of artifacts generated by build. BUILD_ARTIFACTS_PATH: electron-app/dist/build-artifacts + IS_WINDOWS_CONFIG: ${{ matrix.config.name == 'Windows' }} + IS_MACOS_CONFIG: ${{ matrix.config.name == 'macOS x86' }} strategy: matrix: config: ${{ fromJson(needs.select-targets.outputs.build-matrix) }} @@ -293,7 +297,7 @@ jobs: uses: actions/checkout@v3 - name: Install Node.js - if: fromJSON(matrix.config.container) == null + if: fromJSON(matrix.config.container) == null && env.IS_WINDOWS_CONFIG == false uses: actions/setup-node@v4 with: node-version: ${{ env.NODE_VERSION }} @@ -301,26 +305,26 @@ jobs: cache: 'yarn' - name: Install Python 3.x - if: fromJSON(matrix.config.container) == null + if: fromJSON(matrix.config.container) == null && env.IS_WINDOWS_CONFIG == false uses: actions/setup-python@v5 with: python-version: '3.11.x' - name: Install Go - if: fromJSON(matrix.config.container) == null + if: fromJSON(matrix.config.container) == null && env.IS_WINDOWS_CONFIG == false uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - name: Install Go # actions/setup-go@v5 has dependency on a higher version of glibc than available in the Linux container. - if: fromJSON(matrix.config.container) != null + if: fromJSON(matrix.config.container) != null && env.IS_WINDOWS_CONFIG == false uses: actions/setup-go@v4 with: go-version: ${{ env.GO_VERSION }} - name: Install Taskfile - if: fromJSON(matrix.config.container) == null + if: fromJSON(matrix.config.container) == null && env.IS_WINDOWS_CONFIG == false uses: arduino/setup-task@v2 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -328,7 +332,7 @@ jobs: - name: Install Taskfile # actions/setup-task@v2 has dependency on a higher version of glibc than available in the Linux container. - if: fromJSON(matrix.config.container) != null + if: fromJSON(matrix.config.container) != null && env.IS_WINDOWS_CONFIG == false uses: arduino/setup-task@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} @@ -352,7 +356,7 @@ jobs: CREATE_CLIENT_SECRET: ${{ secrets.CREATE_CLIENT_SECRET }} run: | # See: https://www.electron.build/code-signing - if [ $CAN_SIGN = false ]; then + if [ $CAN_SIGN = false ] || [ $IS_WINDOWS_CONFIG = true ]; then echo "Skipping the app signing: certificate not provided." else export CSC_LINK="${{ runner.temp }}/signing_certificate.${{ matrix.config.certificate-extension }}" @@ -372,7 +376,7 @@ jobs: yarn --cwd electron-app rebuild yarn --cwd electron-app build yarn --cwd electron-app package - + # Both macOS jobs generate a "channel update info file" with same path and name. The second job to complete would # overwrite the file generated by the first in the workflow artifact. - name: Stage channel file for merge diff --git a/.github/workflows/check-certificates.yml b/.github/workflows/check-certificates.yml index db5ffc09b..adf4052be 100644 --- a/.github/workflows/check-certificates.yml +++ b/.github/workflows/check-certificates.yml @@ -74,9 +74,11 @@ jobs: - identifier: macOS signing certificate # Text used to identify certificate in notifications. certificate-secret: APPLE_SIGNING_CERTIFICATE_P12 # Name of the secret that contains the certificate. password-secret: KEYCHAIN_PASSWORD # Name of the secret that contains the certificate password. + type: pkcs12 - identifier: Windows signing certificate - certificate-secret: WINDOWS_SIGNING_CERTIFICATE_PFX - password-secret: WINDOWS_SIGNING_CERTIFICATE_PASSWORD + certificate-secret: INSTALLER_CERT_WINDOWS_CER + # The password for the Windows certificate is not needed, because its not a container, but a single certificate. + type: x509 steps: - name: Set certificate path environment variable @@ -95,7 +97,7 @@ jobs: CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }} run: | ( - openssl pkcs12 \ + openssl ${{ matrix.certificate.type }} \ -in "${{ env.CERTIFICATE_PATH }}" \ -legacy \ -noout \ @@ -122,26 +124,43 @@ jobs: CERTIFICATE_PASSWORD: ${{ secrets[matrix.certificate.password-secret] }} id: get-days-before-expiration run: | - EXPIRATION_DATE="$( - ( - openssl pkcs12 \ - -in "${{ env.CERTIFICATE_PATH }}" \ - -clcerts \ - -legacy \ - -nodes \ - -passin env:CERTIFICATE_PASSWORD - ) | ( - openssl x509 \ - -noout \ - -enddate - ) | ( - grep \ - --max-count=1 \ - --only-matching \ - --perl-regexp \ - 'notAfter=(\K.*)' - ) - )" + if [[ ${{ matrix.certificate.type }} == "pkcs12" ]]; then + EXPIRATION_DATE="$( + ( + openssl pkcs12 \ + -in "${{ env.CERTIFICATE_PATH }}" \ + -clcerts \ + -legacy \ + -nodes \ + -passin env:CERTIFICATE_PASSWORD + ) | ( + openssl x509 \ + -noout \ + -enddate + ) | ( + grep \ + --max-count=1 \ + --only-matching \ + --perl-regexp \ + 'notAfter=(\K.*)' + ) + )" + elif [[ ${{ matrix.certificate.type }} == "x509" ]]; then + EXPIRATION_DATE="$( + ( + openssl x509 \ + -in ${{ env.CERTIFICATE_PATH }} \ + -noout \ + -enddate + ) | ( + grep \ + --max-count=1 \ + --only-matching \ + --perl-regexp \ + 'notAfter=(\K.*)' + ) + )" + fi DAYS_BEFORE_EXPIRATION="$((($(date --utc --date="$EXPIRATION_DATE" +%s) - $(date --utc +%s)) / 60 / 60 / 24))" diff --git a/electron-app/package.json b/electron-app/package.json index 3e42fdd0d..0451107f1 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -133,7 +133,8 @@ "msi", "nsis", "zip" - ] + ], + "sign": "./scripts/windowsCustomSign.js" }, "mac": { "darkModeSupport": true, diff --git a/electron-app/scripts/windowsCustomSign.js b/electron-app/scripts/windowsCustomSign.js new file mode 100644 index 000000000..19db2b300 --- /dev/null +++ b/electron-app/scripts/windowsCustomSign.js @@ -0,0 +1,26 @@ +const childProcess = require('child_process'); + +exports.default = async function (configuration) { + const SIGNTOOL_PATH = process.env.SIGNTOOL_PATH; + const INSTALLER_CERT_WINDOWS_CER = process.env.INSTALLER_CERT_WINDOWS_CER; + const CERT_PASSWORD = process.env.CERT_PASSWORD; + const CONTAINER_NAME = process.env.CONTAINER_NAME; + const filePath = configuration.path; + + if ( + SIGNTOOL_PATH && + INSTALLER_CERT_WINDOWS_CER && + CERT_PASSWORD && + CONTAINER_NAME + ) { + childProcess.execSync( + `"${SIGNTOOL_PATH}" sign -d "Arduino IDE" -f "${INSTALLER_CERT_WINDOWS_CER}" -csp "eToken Base Cryptographic Provider" -k "[{{${CERT_PASSWORD}}}]=${CONTAINER_NAME}" -fd sha256 -tr http://timestamp.digicert.com -td SHA256 -v "${filePath}"`, + { stdio: 'inherit' } + ); + } else { + console.warn( + 'Custom windows signing was no performed: SIGNTOOL_PATH, INSTALLER_CERT_WINDOWS_CER, CERT_PASSWORD, and CONTAINER_NAME environment variables were not provided.' + ); + process.exit(1); + } +};