diff --git a/.github/actions/akv-secret/index.js b/.github/actions/akv-secret/index.js index 72f624f8fb5645..19a930db983b0c 100644 --- a/.github/actions/akv-secret/index.js +++ b/.github/actions/akv-secret/index.js @@ -1,6 +1,8 @@ const { spawnSync } = require('child_process'); const fs = require('fs'); const os = require('os'); +const path = require('path'); +const { isUtf8 } = require("buffer"); // Note that we are not using the `@actions/core` package as it is not available // without either committing node_modules/ to the repository, or using something @@ -14,6 +16,35 @@ const escapeData = (s) => { .replace(/\n/g, '%0A') } +const stringify = (value) => { + if (typeof value === 'string') return value; + if (Buffer.isBuffer(value) && isUtf8(value)) return value.toString('utf-8'); + return undefined; +} + +const trimEOL = (buf) => { + let l = buf.length + if (l > 0 && buf[l - 1] === 0x0a) { + l -= l > 1 && buf[l - 2] === 0x0d ? 2 : 1 + } + return buf.slice(0, l) +} + +const writeBufToFile = (buf, file) => { + out = fs.createWriteStream(file) + out.write(buf) + out.end() +} + +const logInfo = (message) => { + process.stdout.write(`${message}${os.EOL}`); +} + +const setFailed = (error) => { + process.stdout.write(`::error::${escapeData(error.message)}${os.EOL}`); + process.exitCode = 1; +} + const writeCommand = (file, name, value) => { // Unique delimiter to avoid conflicts with actual values let delim; @@ -28,24 +59,38 @@ const writeCommand = (file, name, value) => { } const setSecret = (value) => { - process.stdout.write(`::add-mask::${escapeData(value)}${os.EOL}`); + value = stringify(value); + + // Masking a secret that is not a valid UTF-8 string or buffer is not useful + if (value === undefined) return; + + process.stdout.write( + value + .split(/\r?\n/g) + .filter(line => line.length > 0) // Cannot mask empty lines + .map( + value => `::add-mask::${escapeData(value)}${os.EOL}` + ) + .join('') + ); } const setOutput = (name, value) => { + value = stringify(value); + if (value === undefined) { + throw new Error(`Output value '${name}' is not a valid UTF-8 string or buffer`); + } + writeCommand(process.env.GITHUB_OUTPUT, name, value); } const exportVariable = (name, value) => { - writeCommand(process.env.GITHUB_ENV, name, value); -} - -const logInfo = (message) => { - process.stdout.write(`${message}${os.EOL}`); -} + value = stringify(value); + if (value === undefined) { + throw new Error(`Environment variable '${name}' is not a valid UTF-8 string or buffer`); + } -const setFailed = (error) => { - process.stdout.write(`::error::${escapeData(error.message)}${os.EOL}`); - process.exitCode = 1; + writeCommand(process.env.GITHUB_ENV, name, value); } (async () => { @@ -67,9 +112,7 @@ const setFailed = (error) => { // Fetch secrets from Azure Key Vault for (const { input: secretName, encoding, output } of secretMappings) { - let secretValue = ''; - - const az = spawnSync('az', + let az = spawnSync('az', [ 'keyvault', 'secret', @@ -92,10 +135,12 @@ const setFailed = (error) => { if (az.error) throw new Error(az.error, { cause: az.error }); if (az.status !== 0) throw new Error(`az failed with status ${az.status}`); - secretValue = az.stdout.toString('utf-8').trim(); + // az keyvault secret show --output tsv returns a buffer with trailing \n + // (or \r\n on Windows), so we need to trim it specifically. + let secretBuf = trimEOL(az.stdout); // Mask the raw secret value in logs - setSecret(secretValue); + setSecret(secretBuf); // Handle encoded values if specified // Sadly we cannot use the `--encoding` parameter of the `az keyvault @@ -105,31 +150,46 @@ const setFailed = (error) => { if (encoding) { switch (encoding.toLowerCase()) { case 'base64': - secretValue = Buffer.from(secretValue, 'base64').toString(); + secretBuf = Buffer.from(secretBuf.toString('utf-8'), 'base64'); + break; + case 'ascii': + case 'utf8': + case 'utf-8': + // No need to decode the existing buffer from the az command break; default: - // No decoding needed - } + throw new Error(`Unsupported encoding: ${encoding}`); + } - setSecret(secretValue); // Mask the decoded value as well + // Mask the decoded value + setSecret(secretBuf); } - if (output.startsWith('$env:')) { - // Environment variable - const envVarName = output.replace('$env:', '').trim(); - exportVariable(envVarName, secretValue); - logInfo(`Secret set as environment variable: ${envVarName}`); - } else if (output.startsWith('$output:')) { - // GitHub Actions output variable - const outputName = output.replace('$output:', '').trim(); - setOutput(outputName, secretValue); - logInfo(`Secret set as output variable: ${outputName}`); - } else { - // File output - const filePath = output.trim(); - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, secretValue); - logInfo(`Secret written to file: ${filePath}`); + const outputType = output.startsWith('$env:') + ? 'env' + : output.startsWith('$output:') + ? 'output' + : 'file'; + + switch (outputType) { + case 'env': + const varName = output.replace('$env:', '').trim(); + exportVariable(varName, secretBuf); + logInfo(`Secret set as environment variable: ${varName}`); + break; + + case 'output': + const outputName = output.replace('$output:', '').trim(); + setOutput(outputName, secretBuf); + logInfo(`Secret set as output variable: ${outputName}`); + break; + + case 'file': + const filePath = output.trim(); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + writeBufToFile(secretBuf, filePath); + logInfo(`Secret written to file: ${filePath}`); + break; } } })().catch(setFailed); diff --git a/.github/workflows/build-git-installers.yml b/.github/workflows/build-git-installers.yml index ddad1453b119a5..d38b8f4a116a6c 100644 --- a/.github/workflows/build-git-installers.yml +++ b/.github/workflows/build-git-installers.yml @@ -132,9 +132,9 @@ jobs: with: vault: ${{ secrets.AZURE_VAULT }} secrets: | - ${{ secrets.WIN_GPG_KEYGRIP_SECRET_NAME }} > $output:keygrip - ${{ secrets.WIN_GPG_PRIVATE_SECRET_NAME }} > $output:private-key - ${{ secrets.WIN_GPG_PASSPHRASE_SECRET_NAME }} > $output:passphrase + ${{ secrets.WIN_GPG_KEYGRIP_SECRET_NAME }} > $output:keygrip + ${{ secrets.WIN_GPG_PRIVATE_SECRET_NAME }} base64> $output:private-key + ${{ secrets.WIN_GPG_PASSPHRASE_SECRET_NAME }} > $output:passphrase - name: Prepare home directory for GPG signing if: ${{ steps.gpg-secrets.outputs.keygrip != '' && steps.gpg-secrets.outputs.private-key != '' }} shell: bash @@ -218,10 +218,22 @@ jobs: shell: bash run: | git clone --filter=blob:none --single-branch -b main https://github.com/git-for-windows/build-extra /usr/src/build-extra + - name: Log in to Azure + uses: azure/login@v2 + if: env.DO_WIN_CODESIGN == 'true' + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Check out repository (for akv-secret Action) + if: env.DO_WIN_CODESIGN == 'true' + uses: actions/checkout@v4 + with: + path: git - name: Download code signing secrets id: codesign-secrets if: env.DO_WIN_CODESIGN == 'true' - uses: ./.github/actions/akv-secret + uses: ./git/.github/actions/akv-secret with: vault: ${{ secrets.AZURE_VAULT }} secrets: | @@ -344,13 +356,22 @@ jobs: fi && openssl dgst -sha256 artifacts/${{matrix.type.fileprefix}}-*.exe | sed "s/.* //" >artifacts/sha-256.txt - name: Verify that .exe files are code-signed - env: - DO_CODE_SIGN: ${{ secrets.WIN_CODESIGN_CERT_SECRET_NAME != '' }} - if: env.DO_CODE_SIGN == 'true' - shell: bash + if: env.DO_WIN_CODESIGN == 'true' + shell: pwsh run: | - PATH=$PATH:"/c/Program Files (x86)/Windows Kits/10/App Certification Kit/" \ - signtool verify //pa artifacts/${{matrix.type.fileprefix}}-*.exe + $ret = 0 + $files = Get-ChildItem -Path artifacts -Filter "${{matrix.type.fileprefix}}-*.exe" + foreach ($file in $files) { + $signature = Get-AuthenticodeSignature -FilePath $file.FullName + if ($signature.Status -eq 'Valid') { + Write-Host "[ VALID ] $($file.FullName)" + } else { + Write-Host "[INVALID] $($file.FullName)" + Write-Host " Message: $($signature.StatusMessage)" + $ret = 1 + } + } + exit $ret - name: Publish ${{matrix.type.name}}-${{matrix.arch.name}} uses: actions/upload-artifact@v4 with: @@ -369,7 +390,7 @@ jobs: - name: Check out repository uses: actions/checkout@v4 with: - path: 'git' + path: git - name: Install Git dependencies run: | @@ -386,9 +407,16 @@ jobs: # Make universal gettext library lipo -create -output libintl.a /usr/local/opt/gettext/lib/libintl.a /opt/homebrew/opt/gettext/lib/libintl.a + - name: Log in to Azure + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Download signing secrets id: signing-secrets - uses: ./.github/actions/akv-secret + uses: ./git/.github/actions/akv-secret with: vault: ${{ secrets.AZURE_VAULT }} secrets: | @@ -399,8 +427,8 @@ jobs: ${{ secrets.APPLE_DEVELOPER_PASSWORD_SECRET_NAME }} > $output:dev-pass ${{ secrets.APPLE_APPCERT_PASS_SECRET_NAME }} > $output:appcert-pass ${{ secrets.APPLE_INSTCERT_PASS_SECRET_NAME }} > $output:instcert-pass - ${{ secrets.APPLE_APPCERT_SECRET_NAME }} base64> appcert.p12 - ${{ secrets.APPLE_INSTCERT_SECRET_NAME }} base64> instcert.p12 + ${{ secrets.APPLE_APPCERT_SECRET_NAME }} base64> ${{ runner.temp }}/appcert.p12 + ${{ secrets.APPLE_INSTCERT_SECRET_NAME }} base64> ${{ runner.temp }}/instcert.p12 - name: Set up signing/notarization infrastructure run: | @@ -411,7 +439,7 @@ jobs: # Prevent re-locking security set-keychain-settings $RUNNER_TEMP/buildagent.keychain - security import appcert.p12 \ + security import $RUNNER_TEMP/appcert.p12 \ -k $RUNNER_TEMP/buildagent.keychain \ -P '${{ steps.signing-secrets.outputs.appcert-pass }}' \ -T /usr/bin/codesign @@ -420,7 +448,7 @@ jobs: -s -k pwd \ $RUNNER_TEMP/buildagent.keychain - security import instcert.p12 \ + security import $RUNNER_TEMP/instcert.p12 \ -k $RUNNER_TEMP/buildagent.keychain \ -P '${{ steps.signing-secrets.outputs.instcert-pass }}' \ -T /usr/bin/pkgbuild @@ -496,7 +524,7 @@ jobs: cp -R stage/git-universal-$VERSION/ \ git/.github/macos-installer/build-artifacts make -C git/.github/macos-installer V=1 codesign \ - APPLE_APP_IDENTITY=${{ steps.signing-secrets.outputs.appsign-id }} || die "Creating signed payload failed" + APPLE_APP_IDENTITY='${{ steps.signing-secrets.outputs.appsign-id }}' || die "Creating signed payload failed" # Build and sign pkg make -C git/.github/macos-installer V=1 pkg \ @@ -636,9 +664,14 @@ jobs: tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Check out repository (for akv-secret Action) + uses: actions/checkout@v4 + with: + path: git + - name: Download GPG secrets id: gpg-secrets - uses: ./.github/actions/akv-secret + uses: ./git/.github/actions/akv-secret with: vault: ${{ secrets.AZURE_VAULT }} secrets: | @@ -809,8 +842,13 @@ jobs: tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + - name: Check out repository (for akv-secret Action) + uses: actions/checkout@v4 + with: + path: git + - name: Download Linux GPG public key signature file - uses: ./.github/actions/akv-secret + uses: ./git/.github/actions/akv-secret with: vault: ${{ secrets.AZURE_VAULT }} secrets: |