diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 54cdcdbb..cd277983 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,11 +61,13 @@ env: 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). @@ -73,10 +75,16 @@ env: artifacts: - path: '*Windows_64bit.exe' name: Windows_X86-64_interactive_installer + - path: '*Windows_64bit_unsigned.exe' + name: Windows_X86-64_interactive_installer_unsigned - path: '*Windows_64bit.msi' name: Windows_X86-64_MSI + - path: '*Windows_64bit_unsigned.msi' + name: Windows_X86-64_MSI_unsigned - path: '*Windows_64bit.zip' name: Windows_X86-64_zip + - path: '*Windows_64bit_unsigned.zip' + name: Windows_X86-64_zip_unsigned - config: name: Linux runs-on: ubuntu-latest @@ -345,6 +353,7 @@ jobs: IS_NIGHTLY: ${{ needs.build-type-determination.outputs.is-nightly }} IS_RELEASE: ${{ needs.build-type-determination.outputs.is-release }} CAN_SIGN: ${{ secrets[matrix.config.certificate-secret] != '' }} + IS_WINDOWS_CONFIG: ${{ matrix.config.name == 'Windows' }} # The CREATE_* environment vars are only used to run tests. These secrets are optional. Dependent tests will # be skipped if not available. CREATE_USERNAME: ${{ secrets.CREATE_USERNAME }} @@ -352,7 +361,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 +381,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 @@ -406,11 +415,76 @@ jobs: name: ${{ env.JOB_TRANSFER_ARTIFACT }} path: ${{ env.BUILD_ARTIFACTS_PATH }} + sign-windows: + runs-on: [self-hosted, windows-sign-pc] + needs: build + + defaults: + run: + shell: bash + + env: + BUILD_ARTIFACTS_PATH: electron-app/dist/build-artifacts + INSTALLER_CERT_WINDOWS_CER: "/tmp/cert.cer" + # We are hardcoding the path for signtool because is not present on the windows PATH env var by default. + # Keep in mind that this path could change when upgrading to a new runner version + SIGNTOOL_PATH: "C:/Program Files (x86)/Windows Kits/10/bin/10.0.19041.0/x86/signtool.exe" + + steps: + - name: Download artifact + uses: actions/download-artifact@v3 + with: + name: ${{ env.JOB_TRANSFER_ARTIFACT }} + path: ${{ env.BUILD_ARTIFACTS_PATH }} + + - name: Find and process exe and msi artifacts + shell: bash + env: + CERT_PASSWORD: ${{ secrets.INSTALLER_CERT_WINDOWS_PASSWORD }} + CONTAINER_NAME: ${{ secrets.INSTALLER_CERT_WINDOWS_CONTAINER }} + # https://stackoverflow.com/questions/17927895/automate-extended-validation-ev-code-signing-with-safenet-etoken + run: | + shopt -s nullglob + for ARTIFACT in "${{ env.BUILD_ARTIFACTS_PATH }}"/*_unsigned.{exe,msi}; do + echo "Processing $ARTIFACT" + FILENAME=$(basename "$ARTIFACT") + BASE_NAME="${FILENAME%.*}" + EXTENSION="${FILENAME##*.}" + # Remove '_unsigned' from the base name + SIGNED_BASE_NAME="${BASE_NAME%_unsigned}" + + # Sign and rename EXE and MSI files + if [[ "$EXTENSION" == "exe" || "$EXTENSION" == "msi" ]]; then + echo "Signing $ARTIFACT" + "${{ env.SIGNTOOL_PATH }}" sign -d "Arduino IDE" -f ${{ env.INSTALLER_CERT_WINDOWS_CER }} -csp "eToken Base Cryptographic Provider" -k "[{{${{ env.CERT_PASSWORD }}}}]=${{ env.CONTAINER_NAME }}" -fd sha256 -tr http://timestamp.digicert.com -td SHA256 -v "$ARTIFACT" + SIGNED_ARTIFACT_PATH="${{ env.BUILD_ARTIFACTS_PATH }}/${SIGNED_BASE_NAME}.${EXTENSION}" + mv "$ARTIFACT" "$SIGNED_ARTIFACT_PATH" + echo "Renamed $ARTIFACT to $SIGNED_ARTIFACT_PATH" + fi + done + + - name: Upload signed EXE + uses: actions/upload-artifact@v3 + with: + name: Windows_X86-64_interactive_installer + path: ${{ env.BUILD_ARTIFACTS_PATH }}/*Windows_64bit.exe + + - name: Upload signed MSI + uses: actions/upload-artifact@v3 + with: + name: Windows_X86-64_MSI + path: ${{ env.BUILD_ARTIFACTS_PATH }}/*Windows_64bit.msi + + # This step is needed because the self hosted runner does not delete files automatically + - name: Clean up artifacts + run: rm -rf ${{ env.BUILD_ARTIFACTS_PATH }} + merge-channel-files: needs: - build-type-determination - select-targets - build + - sign-windows if: needs.select-targets.outputs.merge-channel-files == 'true' runs-on: ubuntu-latest permissions: {} @@ -474,6 +548,7 @@ jobs: needs: - select-targets - build + - sign-windows if: always() && needs.build.result != 'skipped' runs-on: ubuntu-latest @@ -498,6 +573,7 @@ jobs: needs: - build-type-determination - build + - sign-windows runs-on: ubuntu-latest outputs: BODY: ${{ steps.changelog.outputs.BODY }} @@ -547,6 +623,7 @@ jobs: - build-type-determination - merge-channel-files - changelog + - sign-windows if: > always() && needs.build-type-determination.result == 'success' && @@ -580,6 +657,7 @@ jobs: - build-type-determination - merge-channel-files - changelog + - sign-windows if: > always() && needs.build-type-determination.result == 'success' && @@ -631,6 +709,7 @@ jobs: - publish - release - artifacts + - sign-windows if: always() && needs.build.result != 'skipped' runs-on: ubuntu-latest diff --git a/.github/workflows/check-certificates.yml b/.github/workflows/check-certificates.yml index db5ffc09..adf4052b 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/scripts/package.js b/electron-app/scripts/package.js index 87a59709..83fb0b4d 100644 --- a/electron-app/scripts/package.js +++ b/electron-app/scripts/package.js @@ -100,7 +100,7 @@ async function getArtifactName(version) { switch (platform) { case 'win32': { if (arch === 'x64') { - return `${name}_${version}_Windows_64bit.\$\{ext}`; + return `${name}_${version}_Windows_64bit_unsigned.\$\{ext}`; } throw new Error(`Unsupported platform, arch: ${platform}, ${arch}`); }