Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .azure-pipelines/esrp/sign.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Reusable step template for ESRP code signing via EsrpCodeSigning@6.
#
# For macOS, ESRP requires files to be submitted as a zip archive.
# Set 'useArchive: true' to automatically handle the
# copy → zip → sign → extract cycle. For Windows/Linux where ESRP
# can sign files directly in a folder, leave it as false (default).
#
parameters:
- name: displayName
type: string
- name: folderPath
type: string
- name: pattern
type: string
- name: inlineOperation
type: string
# When true, matching files are copied to a staging dir, zipped,
# signed, and extracted back to folderPath.
- name: useArchive
type: boolean
default: false
# ESRP connection parameters (defaults use pipeline variables)
- name: connectedServiceName
type: string
default: $(esrpAppConnectionName)
- name: appRegistrationClientId
type: string
default: $(esrpClientId)
- name: appRegistrationTenantId
type: string
default: $(esrpTenantId)
- name: authAkvName
type: string
default: $(esrpKeyVaultName)
- name: authSignCertName
type: string
default: $(esrpSignReqCertName)
- name: serviceEndpointUrl
type: string
default: $(esrpEndpointUrl)

steps:
- ${{ if eq(parameters.useArchive, true) }}:
- task: DeleteFiles@1
displayName: 'Clean staging dir for ${{ parameters.displayName }}'
inputs:
SourceFolder: '$(Agent.TempDirectory)/esrp-staging'
Contents: '*'
RemoveSourceFolder: true
- task: CopyFiles@2
displayName: 'Collect files for ${{ parameters.displayName }}'
inputs:
SourceFolder: '${{ parameters.folderPath }}'
Contents: '${{ parameters.pattern }}'
TargetFolder: '$(Agent.TempDirectory)/esrp-staging/contents'
- task: ArchiveFiles@2
displayName: 'Archive files for ${{ parameters.displayName }}'
inputs:
rootFolderOrFile: '$(Agent.TempDirectory)/esrp-staging/contents'
includeRootFolder: false
archiveType: zip
archiveFile: '$(Agent.TempDirectory)/esrp-staging/archive.zip'
- task: EsrpCodeSigning@6
displayName: '${{ parameters.displayName }}'
inputs:
connectedServiceName: '${{ parameters.connectedServiceName }}'
useMSIAuthentication: true
appRegistrationClientId: '${{ parameters.appRegistrationClientId }}'
appRegistrationTenantId: '${{ parameters.appRegistrationTenantId }}'
authAkvName: '${{ parameters.authAkvName }}'
authSignCertName: '${{ parameters.authSignCertName }}'
serviceEndpointUrl: '${{ parameters.serviceEndpointUrl }}'
folderPath: '$(Agent.TempDirectory)/esrp-staging'
pattern: 'archive.zip'
useMinimatch: true
signConfigType: inlineSignParams
inlineOperation: ${{ parameters.inlineOperation }}
- task: ExtractFiles@1
displayName: 'Extract signed files for ${{ parameters.displayName }}'
inputs:
archiveFilePatterns: '$(Agent.TempDirectory)/esrp-staging/archive.zip'
destinationFolder: '${{ parameters.folderPath }}'
overwriteExistingFiles: true
- task: DeleteFiles@1
displayName: 'Clean up staging dir for ${{ parameters.displayName }}'
condition: always()
inputs:
SourceFolder: '$(Agent.TempDirectory)/esrp-staging'
Contents: '*'
RemoveSourceFolder: true
- ${{ else }}:
- task: EsrpCodeSigning@6
displayName: '${{ parameters.displayName }}'
inputs:
connectedServiceName: '${{ parameters.connectedServiceName }}'
useMSIAuthentication: true
appRegistrationClientId: '${{ parameters.appRegistrationClientId }}'
appRegistrationTenantId: '${{ parameters.appRegistrationTenantId }}'
authAkvName: '${{ parameters.authAkvName }}'
authSignCertName: '${{ parameters.authSignCertName }}'
serviceEndpointUrl: '${{ parameters.serviceEndpointUrl }}'
folderPath: '${{ parameters.folderPath }}'
pattern: '${{ parameters.pattern }}'
useMinimatch: true
signConfigType: inlineSignParams
inlineOperation: ${{ parameters.inlineOperation }}
198 changes: 198 additions & 0 deletions .azure-pipelines/esrp/windows/esrpsign.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
#!/bin/bash
#
# Sign Windows files using the ESRP client (Authenticode).
# Usage: esrpsign.sh <file1> [file2 ...]
#
# Required environment variables:
# ESRP_TOOL - Path to ESRPClient.exe
# ESRP_AUTH - Path to the ESRP auth JSON file
# SYSTEM_ACCESSTOKEN - ADO system access token (OAuth bearer)
#
# Optional environment variables:
# ESRP_KEYCODE - Signing key code (default: CP-231522)
#
# The script generates the auth and input JSON files and sets the
# following ESRP client environment variables automatically:
# ESRP_AUTH_CONFIG - Path to the auth JSON file
# ESRP_POLICY_CONFIG - Path to the policy JSON file
# ESRP_SESSION_CONFIG - Not set; ESRP client defaults are used
#
set -euo pipefail

if [ $# -lt 1 ]; then
echo "usage: esrpsign.sh <file> [file ...]" >&2
exit 1
fi

if [ -z "${ESRP_TOOL:-}" ]; then
echo "error: ESRP_TOOL environment variable must be set" >&2
exit 1
fi
if [ -z "${ESRP_AUTH:-}" ]; then
echo "error: ESRP_AUTH environment variable must be set" >&2
exit 1
fi
if [ -z "${SYSTEM_ACCESSTOKEN:-}" ]; then
echo "error: SYSTEM_ACCESSTOKEN environment variable must be set" >&2
exit 1
fi

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Check for overriden key code, otherwise use default (Microsoft Third-Party/OSS)
ESRP_KEYCODE="${ESRP_KEYCODE:-CP-231522}"

# Create work dir and resolve its Windows path by cd-ing into it.
WORK_DIR="$(mktemp -d)"
WORK_DIR_WIN="$(cd "$WORK_DIR" && pwd -W | sed 's|/|\\|g')"

echo "==> ESRP signing tool: $ESRP_TOOL"
echo "==> Working directory: $WORK_DIR"

if [ ! -f "$ESRP_TOOL" ]; then
echo "error: ESRPClient.exe not found at $ESRP_TOOL" >&2
exit 1
fi

# Convert an MSYS2 path to Windows format for ESRPClient.exe.
to_windows_path () {
# Prefer cygpath if available (full Git for Windows)
if command -v cygpath >/dev/null 2>&1; then
cygpath -w "$1"
return
fi
case "$1" in
/[a-zA-Z]/*)
# Drive path: /d/path -> D:\path
drive=$(echo "$1" | cut -c2 | tr 'a-z' 'A-Z')
rest=$(echo "$1" | cut -c3-)
echo "${drive}:${rest}" | sed 's|/|\\|g'
;;
/*)
# Absolute path under MSYS2 root
root=$(cd / && pwd -W)
echo "${root}${1}" | sed 's|/|\\|g'
;;
# Relative or already-Windows path: just flip slashes
*)
echo "$1" | sed 's|/|\\|g'
;;
esac
}

# Build the SignRequestFiles JSON array
echo "==> Preparing files for signing ($# file(s))..."
files_json=""
for file in "$@"; do
if [ ! -f "$file" ]; then
echo "error: file not found: $file" >&2
exit 1
fi

abs_path="$(cd "$(dirname "$file")" && pwd)/$(basename "$file")"
win_path="$(to_windows_path "$abs_path")"
# Escape backslashes for JSON
win_path_escaped="${win_path//\\/\\\\}"
echo " - $win_path"

if [ -n "$files_json" ]; then
files_json+=","
fi
files_json+="
{
\"SourceLocation\": \"$win_path_escaped\",
\"DestinationLocation\": \"$win_path_escaped\"
}"
done

# Generate the input JSON
input_json="$WORK_DIR/input.json"
output_json="$WORK_DIR/output.json"

echo "==> Generating input JSON: $input_json"
cat > "$input_json" <<-EOF
{
"Version": "1.0.0",
"SignBatches": [
{
"SourceLocationType": "UNC",
"DestinationLocationType": "UNC",
"SignRequestFiles": [$files_json
],
"SigningInfo": {
"Operations": [
{
"KeyCode": "$ESRP_KEYCODE",
"OperationCode": "SigntoolSign",
"ToolName": "sign",
"ToolVersion": "1.0",
"Parameters": {
"OpusName": "Microsoft",
"OpusInfo": "https://www.microsoft.com",
"FileDigest": "/fd SHA256",
"PageHash": "/NPH",
"TimeStamp": "/tr \"http://rfc3161.gtm.corp.microsoft.com/TSS/HttpTspServer\" /td sha256"
}
},
{
"KeyCode": "$ESRP_KEYCODE",
"OperationCode": "SigntoolVerify",
"ToolName": "sign",
"ToolVersion": "1.0",
"Parameters": {}
}
]
}
}
]
}
EOF

# Generate policy JSON
echo "==> Generating policy JSON..."
policy_json="$WORK_DIR/policy.json"
cat > "$policy_json" <<-EOF
{
"Version": "1.0.0",
"Intent": "ProductRelease",
"ContentType": "Binaries",
"ContentOrigin": "1stParty",
"ProductState": "Current",
"Audience": "ExternalBroad"
}
EOF

# Use auth JSON from ESRP_AUTH
export ESRP_AUTH_CONFIG="$(to_windows_path "$ESRP_AUTH")"
export ESRP_POLICY_CONFIG="$WORK_DIR_WIN\\policy.json"

# The ADO system access token is referenced in the auth JSON via the environment
# variable - export this so the ESRP client can pick it up when it runs.
export SYSTEM_ACCESSTOKEN

# Print generated JSON files for debugging
echo "==> Auth JSON:"
cat "$ESRP_AUTH"
echo ""
echo "==> Policy JSON:"
cat "$policy_json"
echo ""
echo "==> Input JSON:"
cat "$input_json"
echo ""

# Sign the files
esrp_tool_win="$(to_windows_path "$ESRP_TOOL")"
input_json_win="$WORK_DIR_WIN\\input.json"
output_json_win="$WORK_DIR_WIN\\output.json"

echo "==> ESRP_AUTH_CONFIG=$ESRP_AUTH_CONFIG"
echo "==> ESRP_POLICY_CONFIG=$ESRP_POLICY_CONFIG"
echo "==> Running: $esrp_tool_win sign -i $input_json_win -o $output_json_win"
"$esrp_tool_win" sign \
-i "$input_json_win" \
-o "$output_json_win"

echo "==> Signing complete."
echo "==> Output JSON:"
cat "$output_json"
69 changes: 69 additions & 0 deletions .azure-pipelines/esrp/windows/setup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
parameters:
- name: serviceConnectionName
type: string
- name: esrpClientId
type: string
- name: keyVaultName
type: string
- name: signCertName
type: string

steps:
- task: EsrpClientTool@4
name: esrpinstall
displayName: 'Install ESRP client'
- task: AzureCLI@2
displayName: 'Set up ESRP environment'
inputs:
azureSubscription: ${{ parameters.serviceConnectionName }}
addSpnToEnvironment: true
scriptType: ps
scriptLocation: inlineScript
inlineScript: |
# Resolve ESRP client tool path (passed via env to avoid PS subexpression issues)
$esrpTool = "$env:ESRPCLIENT_TOOLPATH\$env:ESRPCLIENT_TOOLNAME"
if (-not (Test-Path $esrpTool)) { Write-Error "ESRPClient.exe not found at $esrpTool"; exit 1 }
Write-Host "Found ESRP client: $esrpTool"
Write-Host "##vso[task.setvariable variable=ESRP_TOOL]$esrpTool"

# Derive the service connection GUID from the ENDPOINT_URL_* env vars
# that the agent emits for the bound connection. Filter out the
# built-in SystemVssConnection which is always present.
$scId = (Get-ChildItem env:ENDPOINT_URL_*).Name `
-replace '^ENDPOINT_URL_','' |
Where-Object { $_ -ne 'SYSTEMVSSCONNECTION' }
if (-not $scId) { Write-Error "Could not derive service connection GUID"; exit 1 }
Write-Host "Resolved service connection GUID: $scId"

# servicePrincipalId and tenantId are provided by addSpnToEnvironment
$authJson = @{
Version = "1.0.0"
AuthenticationType = "AAD_MSI_WIF"
EsrpClientId = "${{ parameters.esrpClientId }}"
ClientId = $env:servicePrincipalId
TenantId = $env:tenantId
AADAuthorityBaseUri = "https://login.microsoftonline.com/"
FederatedTokenData = @{
JobId = "$(System.JobId)"
PlanId = "$(System.PlanId)"
ProjectId = "$(System.TeamProjectId)"
Hub = "$(System.HostType)"
Uri = "$(System.CollectionUri)"
ServiceConnectionId = $scId
SystemAccessToken = "SYSTEM_ACCESSTOKEN"
}
RequestSigningCert = @{
GetCertFromKeyVault = $true
KeyVaultName = "${{ parameters.keyVaultName }}"
KeyVaultCertName = "${{ parameters.signCertName }}"
}
} | ConvertTo-Json -Depth 4

$authPath = "$(Agent.TempDirectory)\esrp-auth.json"
$authJson | Set-Content -Path $authPath -Encoding UTF8
Write-Host "Generated ESRP auth JSON: $authPath"
Write-Host "##vso[task.setvariable variable=ESRP_AUTH]$authPath"
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
ESRPCLIENT_TOOLPATH: $(esrpinstall.esrpclient.toolpath)
ESRPCLIENT_TOOLNAME: $(esrpinstall.esrpclient.toolname)
Loading
Loading