From 3b2546ad593fe05db868a2f3248872e8ab3e685f Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Thu, 15 Feb 2024 18:41:39 +0000 Subject: [PATCH] WIP Signed-off-by: Joaquim Rocha --- .github/workflows/app-artifacts-mac.yml | 142 ++++++++++++++++---- Makefile | 2 +- app/mac/scripts/esrp-notarize.js | 168 ++++++++++++++++++++++++ app/windows/codesign.js | 2 +- 4 files changed, 287 insertions(+), 27 deletions(-) create mode 100644 app/mac/scripts/esrp-notarize.js diff --git a/.github/workflows/app-artifacts-mac.yml b/.github/workflows/app-artifacts-mac.yml index 5322e4cf5bf..93396f8ffd8 100644 --- a/.github/workflows/app-artifacts-mac.yml +++ b/.github/workflows/app-artifacts-mac.yml @@ -12,8 +12,64 @@ on: default: false type: boolean jobs: - build-mac: - runs-on: macos-latest + # build-mac: + # runs-on: macos-latest + # steps: + # - uses: actions/checkout@v4 + # with: + # ref: ${{ github.event.inputs.buildBranch }} + # - name: Setup nodejs + # uses: actions/setup-node@v4 + # with: + # node-version: 18.x + # cache: 'npm' + # cache-dependency-path: | + # app/package-lock.json + # frontend/package-lock.json + # - uses: actions/setup-go@v5 + # with: + # go-version: '1.20.*' + # cache-dependency-path: | + # backend/go.sum + # - name: Dependencies + # run: brew install make + # - name: Build Backend and Frontend + # run: | + # make + # - name: Add MacOS certs + # run: cd ./app/mac/scripts/ && sh ./setup-certificate.sh + # env: + # APPLE_CERTIFICATE: ${{ secrets.TEST_APPLE_DEV_CERT }} + # APPLE_CERTIFICATE_PASSWORD: ${{ secrets.TEST_APPLE_DEV_CERT_PASS }} + # - name: Build Notarized App Mac + # if: ${{ inputs.signBinaries }} + # run: | + # make app-build + # # env: + # # APPLEID: ${{ secrets.APPLEID }} + # # APPLEIDPASS: ${{ secrets.APPLEIDPASS }} + # # APPLETEAMID: ${{ secrets.APPLETEAMID }} + # # - name: Build App Mac + # # if: ${{ ! inputs.signBinaries }} + # # run: | + # # make app-mac + # - name: CodeSign + # run: | + # cd ./app/dist/mac && codesign -s ${{ secrets.TEST_APPLE_TEAM_ID }} --deep --force --options runtime --entitlements ../../mac/entitlements.mac.plist ./Headlamp.app + # - name: Zip Artifact + # run: | + # cd ./app/dist/mac && zip -r -X -y ./Headlamp.zip Headlamp.app + # - name: Upload artifact + # uses: actions/upload-artifact@v4 + # with: + # name: zipbuild + # path: ./app/dist/mac/Headlamp.zip + # if-no-files-found: error + # retention-days: 1 + notarize: + runs-on: windows-latest + # needs: build-mac + if: ${{ inputs.signBinaries }} steps: - uses: actions/checkout@v4 with: @@ -22,35 +78,71 @@ jobs: uses: actions/setup-node@v4 with: node-version: 18.x - - uses: actions/setup-go@v5 + cache: 'npm' + cache-dependency-path: | + app/package-lock.json + frontend/package-lock.json + # - name: Download Direct (test/temp) + # shell: pwsh + # run: | + # $currentPath = (Get-Location).Path + # Invoke-WebRequest -Uri 'https://productionresultssa11.blob.core.windows.net/actions-results/e4420c55-2c75-424d-bd2a-6d68d68ae923/workflow-job-run-6f8d8423-5c14-5ef4-cc1e-ef33e16df4c9/artifacts/700818bb65e893a1e8bc92c8d61be9906c54ffaec4cedca1e84eb903f43dd9de.zip?rscd=attachment%3B+filename%3D%22zipbuild.zip%22&se=2024-02-16T13%3A20%3A21Z&sig=KdKO7YoEyQWSgbqqFtIYrnHhvXDR%2F1WfIUpnTTWipdU%3D&sp=r&spr=https&sr=b&st=2024-02-16T13%3A10%3A21Z&sv=2021-12-02' -OutFile $currentPath\app\dist + - name: Download artifact + id: download-artifact + uses: dawidd6/action-download-artifact@v3 with: - go-version: '1.20.*' - - name: Dependencies - run: brew install make - - name: Build Backend and Frontend + # Optional, GitHub token, a Personal Access Token with `public_repo` scope if needed + # Required, if the artifact is from a different repo + # Required, if the repo is private a Personal Access Token with `repo` scope is needed or GitHub token in a job where the permissions `action` scope set to `read` + github_token: ${{secrets.GITHUB_TOKEN}} + run_id: 7929503220 + + # - name: Download artifact + # uses: actions/download-artifact@v2 + # with: + # name: zipbuild + # path: ./app/dist + - name: Fetch certificates + if: ${{ inputs.signBinaries }} + shell: pwsh run: | - make - - name: Add MacOS certs - run: cd ./app/mac/scripts/ && sh ./setup-certificate.sh - env: - APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }} - APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} - - name: Build Notarized App Mac + az login --service-principal -u ${{ secrets.WINDOWS_CLIENT_ID }} -p ${{ secrets.AZ_LOGIN_PASS }} --tenant 72f988bf-86f1-41af-91ab-2d7cd011db47 + az keyvault secret download --subscription ${{ secrets.AZ_SUBSCRIPTION_ID }} --vault-name headlamp --name HeadlampAuthCert --file c:\HeadlampAuthCert.pfx --encoding base64 + az keyvault secret download --subscription ${{ secrets.AZ_SUBSCRIPTION_ID }} --vault-name headlamp --name ESRPHeadlampReqCert --file c:\HeadlampReqCert.pfx --encoding base64 + - name: Set up certificates if: ${{ inputs.signBinaries }} + shell: pwsh run: | - make app-mac - env: - APPLEID: ${{ secrets.APPLEID }} - APPLEIDPASS: ${{ secrets.APPLEIDPASS }} - APPLETEAMID: ${{ secrets.APPLETEAMID }} - - name: Build App Mac - if: ${{ ! inputs.signBinaries }} + Import-PfxCertificate -FilePath c:\HeadlampAuthCert.pfx -CertStoreLocation Cert:\LocalMachine\My -Exportable + Import-PfxCertificate -FilePath c:\HeadlampReqCert.pfx -CertStoreLocation Cert:\LocalMachine\My -Exportable + + - name: Download and Set up ESRPClient + if: ${{ inputs.signBinaries }} + shell: pwsh run: | - make app-mac - - name: Upload artifact + nuget.exe sources add -name esrp -source ${{ secrets.ESRP_NUGET_INDEX_URL }} -username headlamp -password ${{ secrets.AZ_DEVOPS_TOKEN }} + nuget.exe install Microsoft.EsrpClient -Version 1.2.80 -source ${{ secrets.ESRP_NUGET_INDEX_URL }} | out-null + + - name: App Windows + shell: pwsh + run: | + ls app/mac/scripts + ls + if ("${{ inputs.signBinaries }}" -eq "true") { + $env:ESRP_PATH="$(Get-Location)\Microsoft.EsrpClient.1.2.80\tools\EsrpClient.exe" + $env:HEADLAMP_WINDOWS_CLIENT_ID="${{ secrets.WINDOWS_CLIENT_ID }}" + $env:HEADLAMP_WINDOWS_SIGN_EMAIL="${{ secrets.WINDOWS_SIGN_EMAIL }}" + } else { + echo "Not signing binaries" + } + cd ./app/mac/scripts + ls ../../../zipbuild + node ./esrp-notarize.js ../../../zipbuild/Headlamp.zip + + - name: Upload Notarized uses: actions/upload-artifact@v4 with: - name: DMGs + name: Win exes path: ./app/dist/Headlamp*.* if-no-files-found: error - retention-days: 1 + retention-days: 2 diff --git a/Makefile b/Makefile index 901cb85bbb7..fa3b19c75a1 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ app-win-msi: app-build app-linux: app-build cd app && npm run package -- --linux app-mac: app-build - cd app && npm run package -- --mac + cd app && npm run build .PHONY: backend backend: diff --git a/app/mac/scripts/esrp-notarize.js b/app/mac/scripts/esrp-notarize.js new file mode 100644 index 00000000000..7505aa95e9a --- /dev/null +++ b/app/mac/scripts/esrp-notarize.js @@ -0,0 +1,168 @@ +const { execSync } = require('child_process'); +const path = require('path'); +const os = require('os'); +const fs = require('fs'); + +const SIGN_JSON_TEMPLATE = { + Version: '1.0.0', + DriEmail: [`${process.env.HEADLAMP_WINDOWS_SIGN_EMAIL}`], + GroupId: null, + CorrelationVector: null, + SignBatches: [], +}; + +const POLICY_JSON = { + Version: '1.0.0', + Intent: '', + ContentType: '', + ContentOrigin: '', + ProductState: '', + Audience: '', +}; + +const AUTH_JSON = { + Version: '1.0.0', + AuthenticationType: 'AAD_CERT', + ClientId: `${process.env.HEADLAMP_WINDOWS_CLIENT_ID}`, + AuthCert: { + SubjectName: `CN=${process.env.HEADLAMP_WINDOWS_CLIENT_ID}.microsoft.com`, + StoreLocation: 'LocalMachine', + StoreName: 'My', + SendX5c: 'true', + }, + RequestSigningCert: { + SubjectName: `CN=${process.env.HEADLAMP_WINDOWS_CLIENT_ID}`, + StoreLocation: 'LocalMachine', + StoreName: 'My', + }, +}; + +function getFileList(rootDir) { + let files = {}; + let dirs = ['.']; + while (dirs.length > 0) { + const dirName = dirs.shift(); + const curDir = path.join(rootDir, dirName); + + fs.readdirSync(curDir).forEach(file => { + if (['node_modules', '.git'].includes(file)) { + return; + } + const filepath = path.resolve(rootDir, dirName, file); + const stat = fs.statSync(filepath); + if (stat.isDirectory() && !files[file]) { + dirs.push(path.join(dirName, file)); + files[file] = []; + } else { + if (!files[dirName]) { + files[dirName] = []; + } + + files[dirName].push(file); + } + }); + } + return files; +} + +function createJson(pathToSign, fileName = 'test_SignInput.json') { + let rootDir = pathToSign; + let files = {}; + + // Check if we are signing one single file or all files in a directory + const stat = fs.statSync(pathToSign); + if (stat.isFile()) { + rootDir = path.dirname(pathToSign); + files = { '.': [path.basename(pathToSign)] }; + } else { + files = getFileList(pathToSign); + } + + console.log('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>', pathToSign, stat, rootDir, files); + + const filesJson = (dir, files) => { + return { + SourceLocationType: 'UNC', + SourceRootDirectory: path.resolve(rootDir, dir), + SignRequestFiles: files.map(f => ({ + SourceLocation: f, + SourceHash: '', + HashType: null, + Name: f, + })), + SigningInfo: { + Operations: [ + { + KeyCode: 'CP-401337-Apple', + + OperationCode: 'MacAppNotarize', + + Parameters: { + BundleId: 'com.microsoft.Headlamp', // this parameter enables the hardening flag during signing. + }, + + ToolName: 'sign', + + ToolVersion: '1.0', + }, + ], + }, + }; + }; + + SIGN_JSON_TEMPLATE.SignBatches = Object.keys(files) + .map(dir => filesJson(dir, files[dir])) + .filter(f => f.SignRequestFiles.length > 0); + + console.log('>>>>>>>>SIGN', JSON.stringify(SIGN_JSON_TEMPLATE, undefined, 2)); + + const filePath = path.join(os.tmpdir(), fileName); + fs.writeFileSync(filePath, JSON.stringify(SIGN_JSON_TEMPLATE, undefined, 2)); + + return filePath; +} + +/** + * Signs the given file, or all files in a given directory if that's what's passed to it. + * @param esrpTool - The path to the ESRP tool. + * @param pathToSign - A path to a file or directory. + */ +function sign(esrpTool, pathToSign) { + const absPathToSign = path.resolve(pathToSign); + const signJsonBase = path.basename(absPathToSign).split('.')[0]; + const signInputJson = createJson(absPathToSign, `${signJsonBase}-SignInput.json`); + console.log('>>>>>>>>>>>>>>>>>>>>>>>||', absPathToSign); + + const policyJson = path.resolve(os.tmpdir(), 'Policy.json'); + fs.writeFileSync(policyJson, JSON.stringify(POLICY_JSON, undefined, 2)); + const authJson = path.resolve(os.tmpdir(), 'Auth.json'); + fs.writeFileSync(authJson, JSON.stringify(AUTH_JSON, undefined, 2)); + console.log('>>>>', `${esrpTool} Sign -a ${authJson} -p ${policyJson} -i ${signInputJson}`); + console.log( + 'EXEC', + execSync( + `${esrpTool} Sign -l Verbose -a ${authJson} -p ${policyJson} -i ${signInputJson}`, + (error, stdout, stderr) => { + if (error) { + console.log(`error: ${error.message}`); + return; + } + if (stderr) { + console.log(`stderr: ${stderr}`); + return; + } + console.log(`stdout: ${stdout}`); + } + ) + ); +} + +// module.exports = { +// sign, +// }; + +if (require.main === module) { + console.log('>>>>>>>>>>', process.argv.slice(2)); + sign(process.env.ESRP_PATH, process.argv[2]); + process.exit(0); +} diff --git a/app/windows/codesign.js b/app/windows/codesign.js index 7f87a657d47..b2fe489cfb7 100644 --- a/app/windows/codesign.js +++ b/app/windows/codesign.js @@ -119,7 +119,7 @@ function createJson(pathToSign, fileName = 'test_SignInput.json') { SIGN_JSON_TEMPLATE.SignBatches = Object.keys(files) .map(dir => filesJson(dir, files[dir])) .filter(f => f.SignRequestFiles.length > 0); - + console.log('>>>>>TEST', JSON.stringify(SIGN_JSON_TEMPLATE, undefined, 2)); const filePath = path.join(os.tmpdir(), fileName); fs.writeFileSync(filePath, JSON.stringify(SIGN_JSON_TEMPLATE, undefined, 2));