Skip to content

Commit

Permalink
WIP
Browse files Browse the repository at this point in the history
Signed-off-by: Joaquim Rocha <joaquim.rocha@microsoft.com>
  • Loading branch information
joaquimrocha committed Feb 23, 2024
1 parent efcc9d4 commit 3a653e1
Show file tree
Hide file tree
Showing 9 changed files with 562 additions and 35 deletions.
110 changes: 101 additions & 9 deletions .github/workflows/app-artifacts-mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,114 @@ jobs:
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
- name: Build Notarized App Mac
- name: Build App Mac
if: ${{ inputs.signBinaries }}
env:
# This will trigger codesign. See app/mac/scripts/codeSign.js
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
make app-mac
env:
APPLEID: ${{ secrets.APPLEID }}
APPLEIDPASS: ${{ secrets.APPLEIDPASS }}
APPLETEAMID: ${{ secrets.APPLETEAMID }}
- name: Build App Mac
- name: Early staple (only if we are not notarizing the app)
if: ${{ ! inputs.signBinaries }}
run: |
make app-mac
xcrun stapler staple ./app/dist/Headlamp*.dmg
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: DMGs
path: ./app/dist/Headlamp*.*
name: dmgs
path: ./app/dist/Headlamp*.dmg
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:
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
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: dmgs
path: ./dmgs
- name: Fetch certificates
if: ${{ inputs.signBinaries }}
shell: pwsh
run: |
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: |
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: |
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.87 -source ${{ secrets.ESRP_NUGET_INDEX_URL }} | out-null
- name: Sign App
shell: pwsh
run: |
if ("${{ inputs.signBinaries }}" -eq "true") {
$env:ESRP_PATH="$(Get-Location)\Microsoft.EsrpClient.1.2.87\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
node ./esrp-notarize.js SIGN ../../../dmgs/
- name: Notarize App
shell: pwsh
run: |
if ("${{ inputs.signBinaries }}" -eq "true") {
$env:ESRP_PATH="$(Get-Location)\Microsoft.EsrpClient.1.2.87\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
node ./esrp-notarize.js NOTARIZE ../../../dmgs/
- name: Upload Notarized
uses: actions/upload-artifact@v4
with:
name: dmgs
path: ./dmgs/Headlamp*.dmg
if-no-files-found: error
overwrite: true
retention-days: 2
stapler:
runs-on: macos-latest
needs: notarize
if: ${{ inputs.signBinaries }}
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: dmgs
path: ./dmgs
- name: Staple
run: |
xcrun stapler staple ./dmgs/Headlamp*.dmg
- name: Upload Stapled
uses: actions/upload-artifact@v4
with:
name: dmgs
path: ./dmgs/Headlamp*.dmg
if-no-files-found: error
overwrite: true
retention-days: 2
6 changes: 6 additions & 0 deletions .github/workflows/app-artifacts-win.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: 18.x
cache: 'npm'
cache-dependency-path: |
headlamp/app/package-lock.json
headlamp/frontend/package-lock.json
- uses: actions/setup-go@v5
with:
go-version: '1.20.*'
cache-dependency-path: |
headlamp/backend/go.sum
- name: Dependencies
uses: crazy-max/ghaction-chocolatey@v1
with:
Expand Down
25 changes: 25 additions & 0 deletions app/mac/scripts/codeSign.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require('dotenv').config();
const { execSync } = require('child_process');
const path = require('path');

exports.default = async function codeSign(config) {
const teamID = process.env.APPLE_TEAM_ID;

if (!teamID) {
console.log('Mac codesign: No Apple Team ID found, skipping codesign');
return;
}

const entitlementsPath = path.resolve(path.join(__dirname, '..', 'entitlements.mac.plist'));

let exitCode = 0;
try {
execSync(
`codesign -s ${teamID} --deep --force --options runtime --entitlements ${entitlementsPath} ${config.app}`
);
} catch (e) {
exitCode = e.status !== null ? e.status : 1;
}

console.log('Mac codesign:', exitCode === 0 ? 'Success' : `Failed (${exitCode})`);
};
198 changes: 198 additions & 0 deletions app/mac/scripts/esrp-notarize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* This script is used to sign and notarize the Headlamp app for MacOS
* using a tool from ESRP (Windows only). It is mainly called from CI.
*
* Usage: node esrp-notarize.js SIGN|NOTARIZE path-to-sign
**/

const crypto = require('crypto');
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({
path: file,
hash: getSHA256(filepath),
});
}
});
}
return files;
}

function getSHA256(filePath) {
const hash = crypto.createHash('sha256');
const data = fs.readFileSync(filePath);

hash.update(data);
return hash.digest('hex');
}

const signOp = {
KeyCode: 'CP-401337-Apple',

OperationCode: 'MacAppDeveloperSign',
Parameters: {
Hardening: '--options=runtime',
},
ToolName: 'sign',

ToolVersion: '1.0',
};
const notarizeOp = {
KeyCode: 'CP-401337-Apple',
OperationCode: 'MacAppNotarize',
Parameters: {
BundleId: 'com.microsoft.Headlamp',
},
ToolName: 'sign',
ToolVersion: '1.0',
};

function createSignJson(pathToSign, fileName = 'test_SignInput.json') {
return createJson(pathToSign, signOp, fileName);
}

function createNotarizeJson(pathToSign, fileName = 'test_SignInput.json') {
return createJson(pathToSign, notarizeOp, fileName);
}

function createJson(pathToSign, op, 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: path.basename(pathToSign),
hash: getSHA256(pathToSign),
},
],
};
} else {
files = getFileList(pathToSign);
}

const filesJson = (dir, files) => {
return {
SourceLocationType: 'UNC',
SourceRootDirectory: path.resolve(rootDir, dir),
SignRequestFiles: files.map(f => ({
SourceLocation: f.path,
SourceHash: f.hash ?? '',
HashType: (f.hash && 'SHA256') || null,
Name: f.path,
})),
SigningInfo: {
Operations: [op],
},
};
};

SIGN_JSON_TEMPLATE.SignBatches = Object.keys(files)
.map(dir => filesJson(dir, files[dir]))
.filter(f => f.SignRequestFiles.length > 0);
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 op - The operation to perform. Either 'SIGN' or 'NOTARIZE'.
* @param pathToSign - A path to a file or directory.
*/
function sign(esrpTool, op, pathToSign) {
const absPathToSign = path.resolve(pathToSign);
const signJsonBase = path.basename(absPathToSign).split('.')[0];
let signInputJson = '';
if (op === 'SIGN') {
signInputJson = createSignJson(absPathToSign, `${signJsonBase}-SignInput.json`);
} else if (op === 'NOTARIZE') {
signInputJson = createNotarizeJson(absPathToSign, `${signJsonBase}-SignInput.json`);
} else {
throw new Error('Invalid operation');
}

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));

try {
execSync(`${esrpTool} Sign -l Verbose -a ${authJson} -p ${policyJson} -i ${signInputJson}`);
} catch (e) {
console.error('Failed to sign:', e);
process.exit(e.status !== null ? e.status ?? 1 : 1);
}
}

if (require.main === module) {
const wantedOp = process.argv[2];
const pathToSign = process.argv[3];
sign(process.env.ESRP_PATH, wantedOp, pathToSign);
process.exit(0);
}
20 changes: 0 additions & 20 deletions app/mac/scripts/notarize.js

This file was deleted.

Loading

0 comments on commit 3a653e1

Please sign in to comment.