Sep 4, 2025
OVERVIEW: This page walks you through the process of automating code signing in a CI/CD pipeline using Jenkins, AWS Key Management Service (KMS), and Microsoft SignTool, with a GlobalSign Code Signing Certificate. At the completion of this procedure, GlobalSign Code Signing Certificate will be securely stored in AWS KMS, integrated with Jenkins, and automatically applied to your Windows executables during builds. Learn more about Code Signing Certificate management and other frequently asked questions here. |
AWS CLI installed on a Windows Server 2019 EC2 instance
IAM user with KMS permissions
Microsoft SignTool (via Windows 10 SDK)
OpenSSL for Windows
Python
GlobalSign Code Signing Certificate
AWS KMS asymmetric key created
A CSR signed with the key stored in the AWS KMS
Launch the AWS Windows ec2 instance with Windows server 2019
Install AWS CLI over the launched ec2 instance. Use msiexec to run the MSI installer:
msiexec.exe /i https://awscli.amazonaws.com/AWSCLIV2.msi /qn
Install Signtool.
• Download Windows 10 SDK from Microsoft.
• Open the downloaded file and follow the wizard for the installation
Install the Python and boto3 library.
• Install the required package for Python
• pip install boto3 cryptography
IAM user with KMS permissions.
• Login to AWS console and browse to the IAM console
• Go to IAM > Policies
• Click Create policy
• Go to the JSON tab and paste the below script:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "kms:*",
"Resource": "*"
}
]
}
• Now that Admin permissions has been given to the user, you can add the permissions as per your need.
Configure the AWS account with AWS CLI:
aws configure
• Enter the required AWS credentials like AWS Client Key and Secret for Configuration.
Create an Asymmetric KMS Key using the following commands
• Open CMD and run:
aws kms create-key --description "KMS key for CSR signing" --key-usage SIGN_VERIFY --key-spec RSA_4096 --origin AWS_KMS
#After creating the AWS KMS resource create the alias for it
aws kms create-alias --alias-name alias/your-key-alias --target-key-id KeyID
Use OpenSSL in your Windows Instance, generate a CSR template in the following way:
openssl req -new -newkey rsa:4096 -nodes -keyout dummy.key -out dummy.csr
Sign the above generated CSR with AWS KMS Key
• Use this python Script in your Windows Instance to sign the CSR with key stored in AWS KMS.
To do that, create a Python file, aws-kms-sign-csr.py and add the provided code into it.
#!/usr/bin/env python3
from pyasn1.codec.der import decoder, encoder
from pyasn1.type import univ
import pyasn1_modules.pem
import pyasn1_modules.rfc2986
import pyasn1_modules.rfc2314
import hashlib
import base64
import textwrap
import argparse
import boto3
START_MARKER = '-----BEGIN CERTIFICATE REQUEST-----'
END_MARKER = '-----END CERTIFICATE REQUEST-----'
SIGN_ALGO = 'RSASSA_PKCS1_V1_5_SHA_256'
SIGN_OID = '1.2.840.113549.1.1.11'
def sign_certification_request_info(kms, key_id, csr):
der_bytes = encoder.encode(csr['certificationRequestInfo'])
digest = hashlib.sha256(der_bytes).digest()
response = kms.sign(KeyId=key_id, Message=digest, MessageType='DIGEST', SigningAlgorithm=SIGN_ALGO)
return response['Signature']
def output_csr(csr):
print(START_MARKER)
b64 = base64.b64encode(encoder.encode(csr)).decode('ascii')
print('\n'.join(textwrap.wrap(b64, 64)))
print(END_MARKER)
def main(args):
with open(args.csr, 'r') as f:
substrate = pyasn1_modules.pem.readPemFromFile(f, startMarker=START_MARKER, endMarker=END_MARKER)
csr, _ = decoder.decode(substrate, asn1Spec=pyasn1_modules.rfc2986.CertificationRequest())
if not csr:
raise ValueError('File does not look like a CSR')
region = args.region or boto3.session.Session().region_name
if args.profile:
boto3.setup_default_session(profile_name=args.profile)
kms = boto3.client('kms', region_name=region)
pubkey_der = kms.get_public_key(KeyId=args.keyid)['PublicKey']
csr['certificationRequestInfo']['subjectPKInfo'] = decoder.decode(pubkey_der, pyasn1_modules.rfc2314.SubjectPublicKeyInfo())[0]
signature_bytes = sign_certification_request_info(kms, args.keyid, csr)
csr.setComponentByName('signature', univ.BitString.fromOctetString(signature_bytes))
sig_alg_identifier = pyasn1_modules.rfc2314.SignatureAlgorithmIdentifier()
sig_alg_identifier.setComponentByName('algorithm', univ.ObjectIdentifier(SIGN_OID))
csr.setComponentByName('signatureAlgorithm', sig_alg_identifier)
output_csr(csr)
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('csr', help="Source CSR (can be signed with any key)")
parser.add_argument('--keyid', required=True, help='key ID in AWS KMS')
parser.add_argument('--region', help='AWS region')
parser.add_argument('--profile', help='AWS profile')
args
• After creating the Python file use this below command to sign the CSR.
python aws-kms-sign-csr.py --region
• Then, you would get the signed CSR from AWS KMS.
Get GlobalSign’s HSM based Code Signing Certificate by providing this signed CSR.
Note: Make sure to follow the right steps to get Code Signing Certificate from GlobalSign.
• Log in to GlobalSign Certificate Center (GCC).
• Order a new Code Signing Certificate and choose HSM-based.
• Once your Business Information & Identity Vetting is done, you will receive an email from GlobalSign to submit CSR for generating Certificate.
• Submit the CSR and download and install the issued certificate (.cer file).
• You can download the certificate and place it on the desktop of your Windows Instance.
Install Certificate in Certificate Store of your Windows Instance
• Use the below commands to install the certificate into the Certificate Store from the desktop.
Import-Certificate -FilePath "C:\Users\Administrator\Desktop\OS20250704014833.cer" -CertStoreLocation "Cert:\LocalMachine\My\"
Import-Certificate -FilePath "C:\Users\Administrator\Desktop\intermediate1.cer" -CertStoreLocation "Cert:\LocalMachine\CA\"
Import-Certificate -FilePath "C:\Users\Administrator\Desktop\intermediate2.cer" -CertStoreLocation "Cert:\LocalMachine\CA\"
Use the Python script below for signing the .exe file over the Windows Instance Server through the Jenkins pipeline. Place this file on the desktop of your Windows Instance.
This python script will do the following:
• Generating digest of your Executable file with SignTool
• Signing digest with AWS KMS
• Attestation of Signature into your executable file
• Adding timestamp from GlobalSign’s Timestamp Server
• VERIFICATION of Signatures
#!/usr/bin/env python3
import subprocess
import sys
import os
import json
import base64
import argparse
import tempfile
import hashlib
def find_signtool():
"""Find SignTool.exe on the system"""
possible_paths = [
r"C:\Program Files (x86)\Windows Kits\10\bin\x64\signtool.exe",
r"C:\Program Files (x86)\Windows Kits\10\bin\x86\signtool.exe",
r"C:\Program Files\Microsoft SDKs\Windows\v7.1\Bin\signtool.exe",
r"C:\Program Files (x86)\Microsoft SDKs\Windows\v7.1A\Bin\signtool.exe"
]
for path in possible_paths:
if os.path.exists(path):
return path
# Try to find it in PATH
try:
result = subprocess.run(['where', 'signtool'], capture_output=True, text=True)
if result.returncode == 0:
return result.stdout.strip().split('\n')[0]
except:
pass
return None
def calculate_pe_hash(file_path: str) -> bytes:
"""Calculate the Authenticode hash manually"""
print("Calculating PE Authenticode hash manually...")
with open(file_path, 'rb') as f:
data = f.read()
# Very basic PE hash calculation (simplified)
# This is not the full Authenticode algorithm, but might work for testing
import struct
if len(data) < 64 or data[0:2] != b'MZ':
raise Exception("Invalid PE file")
# Get NT headers offset
nt_offset = struct.unpack('
if data[nt_offset:nt_offset+4] != b'PE\x00\x00':
raise Exception("Invalid PE file")
# Simple hash of the file (excluding areas that will change)
hasher = hashlib.sha256()
# Hash the file data (this is simplified - real Authenticode is more complex)
hasher.update(data)
file_hash = hasher.digest()
print(f"Calculated hash: {file_hash.hex()}")
return file_hash
def try_direct_certificate_store_signing(exe_path: str, cert_thumbprint: str) -> bool:
"""Try signing directly with certificate store (no external digest)"""
print("Attempting direct certificate store signing...")
signtool = find_signtool()
if not signtool:
print("SignTool not found")
return False
# Try different combinations of SignTool parameters
signing_methods = [
# Method 1: Certificate store with machine store
[signtool, 'sign', '/sha1', cert_thumbprint, '/s', 'My', '/sm', '/fd', 'sha256', '/tr', 'http://timestamp.globalsign.com', '/td', 'sha256', exe_path],
# Method 2: Certificate store with user store
[signtool, 'sign', '/sha1', cert_thumbprint, '/s', 'My', '/su', '/fd', 'sha256', '/tr', 'http://timestamp.globalsign.com', '/td', 'sha256', exe_path],
# Method 3: Just thumbprint (let SignTool find it)
[signtool, 'sign', '/sha1', cert_thumbprint, '/fd', 'sha256', '/tr', 'http://timestamp.globalsign.com', '/td', 'sha256', exe_path],
# Method 4: Auto-detect certificate store
[signtool, 'sign', '/a', '/sha1', cert_thumbprint, '/fd', 'sha256', '/tr', 'http://timestamp.globalsign.com', '/td', 'sha256', exe_path],
]
for i, cmd in enumerate(signing_methods, 1):
print(f"\n Method {i}: {' '.join(cmd)}")
try:
result = subprocess.run(cmd, capture_output=True, text=True)
print("STDOUT:")
print(result.stdout)
if result.stderr:
print("STDERR:")
print(result.stderr)
if result.returncode == 0:
print(f"Method {i} succeeded!")
# Verify the signature
verify_cmd = [signtool, 'verify', '/pa', '/v', exe_path]
verify_result = subprocess.run(verify_cmd, capture_output=True, text=True)
if verify_result.returncode == 0:
print("Signature verification passed!")
return True
else:
print("Signature verification failed")
else:
print(f"Method {i} failed with exit code: {result.returncode}")
except Exception as e:
print(f"Method {i} exception: {e}")
return False
def manual_digest_approach(exe_path: str, cert_thumbprint: str, kms_key_id: str, aws_region: str = 'us-east-1') -> bool:
"""Try manual digest calculation and signing"""
print("Attempting manual digest approach...")
# Calculate PE hash manually
try:
pe_hash = calculate_pe_hash(exe_path)
except Exception as e:
print(f"Failed to calculate PE hash: {e}")
return False
# Sign with AWS KMS
print("Signing with AWS KMS...")
digest_b64 = base64.b64encode(pe_hash).decode('ascii')
aws_cmd = [
'aws', 'kms', 'sign',
'--message', digest_b64,
'--message-type', 'DIGEST',
'--signing-algorithm', 'RSASSA_PKCS1_V1_5_SHA_256',
'--key-id', kms_key_id,
'--region', aws_region,
'--output', 'json'
]
result = subprocess.run(aws_cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"AWS KMS signing failed: {result.stderr}")
return False
try:
kms_response = json.loads(result.stdout)
signature_b64 = kms_response['Signature']
print("AWS KMS signing successful")
except (json.JSONDecodeError, KeyError) as e:
print(f"Failed to parse AWS KMS response: {e}")
return False
# TODO: Manually embed signature into PE file
# This would require implementing the full PE signature embedding logic
print("Manual PE signature embedding not implemented yet")
return False
def test_certificate_accessibility(cert_thumbprint: str) -> bool:
"""Test if certificate is accessible and has private key"""
print("Testing certificate accessibility...")
ps_cmd = f'''
$cert = Get-ChildItem -Path Cert:\\CurrentUser\\My, Cert:\\LocalMachine\\My -Recurse |
Where-Object {{$_.Thumbprint -eq '{cert_thumbprint}'}} |
Select-Object -First 1
if ($cert) {{
Write-Host "Certificate found!"
Write-Host "Subject: $($cert.Subject)"
Write-Host "Store: $($cert.PSParentPath)"
Write-Host "Has Private Key: $($cert.HasPrivateKey)"
Write-Host "Provider: $($cert.Provider)"
# Test if we can access the private key
try {{
$rsa = $cert.PrivateKey
if ($rsa) {{
Write-Host "Private key accessible via PrivateKey property"
}} else {{
Write-Host "Private key not accessible via PrivateKey property"
}}
}} catch {{
Write-Host "Error accessing private key: $($_.Exception.Message)"
}}
# Check if certificate is valid for code signing
$eku = $cert.Extensions | Where-Object {{$_.Oid.FriendlyName -eq "Enhanced Key Usage"}}
if ($eku) {{
$ekuText = $eku.Format(0)
if ($ekuText -match "Code Signing") {{
Write-Host "Code Signing EKU present"
}} else {{
Write-Host "Code Signing EKU missing"
}}
}}
$true
}} else {{
Write-Host "Certificate not found!"
$false
}}
'''
try:
result = subprocess.run(
['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps_cmd],
capture_output=True,
text=True
)
print(result.stdout)
return "Certificate found!" in result.stdout and "Has Private Key: True" in result.stdout
except Exception as e:
print(f"Error testing certificate: {e}")
return False
def alternative_signing_approach(exe_path: str, cert_thumbprint: str, kms_key_id: str, aws_region: str = 'us-east-1') -> bool:
"""Try alternative signing approaches"""
print("Starting Alternative Signing Approaches...")
print(f"File: {exe_path}")
print(f"Certificate: {cert_thumbprint}")
print(f"KMS Key: {kms_key_id}")
print()
# Test certificate first
if not test_certificate_accessibility(cert_thumbprint):
print("Certificate accessibility test failed")
return False
# Approach 1: Try direct certificate store signing
print("\n" + "="*60)
print("APPROACH 1: Direct Certificate Store Signing")
print("="*60)
if try_direct_certificate_store_signing(exe_path, cert_thumbprint):
print("Approach 1 succeeded!")
return True
print("Approach 1 failed")
# Approach 2: Manual digest approach
print("\n" + "="*60)
print("APPROACH 2: Manual Digest Calculation")
print("="*60)
if manual_digest_approach(exe_path, cert_thumbprint, kms_key_id, aws_region):
print("Approach 2 succeeded!")
return True
print("Approach 2 failed")
return False
def main():
"""Main entry point"""
parser = argparse.ArgumentParser(description='Alternative AWS KMS + SignTool Approaches')
parser.add_argument('--sign', required=True, help='File to sign')
parser.add_argument('--cert-thumbprint', required=True, help='Certificate thumbprint')
parser.add_argument('--kms-key-id', required=True, help='AWS KMS Key ID')
parser.add_argument('--aws-region', default='us-east-1', help='AWS region')
args = parser.parse_args()
if not os.path.exists(args.sign):
print(f"File not found: {args.sign}")
sys.exit(1)
success = alternative_signing_approach(args.sign, args.cert_thumbprint, args.kms_key_id, args.aws_region)
if success:
print("\n ALTERNATIVE APPROACH SUCCEEDED!")
# Test final result
print("\n Final verification...")
ps_test = f'''
$sig = Get-AuthenticodeSignature -FilePath '{args.sign}'
Write-Host "Status: $($sig.Status)"
if ($sig.SignerCertificate) {{
Write-Host "Windows recognizes the signature!"
Write-Host " Subject: $($sig.SignerCertificate.Subject)"
}} else {{
Write-Host "Windows does not recognize the signature"
}}
'''
try:
result = subprocess.run(
['powershell', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command', ps_test],
capture_output=True,
text=True
)
print(result.stdout)
except:
pass
else:
print("\n ALL APPROACHES FAILED")
print("The certificate might not have an accessible private key for SignTool")
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()
sudo apt update
sudo apt install openjdk-11-jre
wget -q -O - https://pkg.jenkins.io/debian-stable/jenkins.io.key | sudo apt-key add -
sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
sudo apt update
sudo apt install jenkins
sudo systemctl start jenkins
sudo systemctl enable jenkins
After the execution of the above commands, the Jenkins server would be accessible over http://
Below are the steps to connect with the Windows server from Jenkins pipeline:
• Add SSH Credentials to Jenkins
1. Go to Jenkins > Manage Jenkins > Credentials > (Global) > Add Credentials.
2. Select SSH Username with private key.
3. Enter:
Username: administrator
Private Key: Paste the contents of your id_rsa
file.
ID: CREDS_ID
(as used in your pipeline)
• Ensure Windows Server SSH Access
1. On the Windows server, ensure the OpenSSH service is running:
Get-Service sshd
2. Test the connect manually. Transfer Files from Jenkins to Windows Server.
Start-Service sshd
3. Use scp
in your Jenkins pipeline to copy files:
scp /path/to/file administrator@
4. Login to the Windows server and you would get the file that just got copied through scp.
After setting up the Jenkins Server, run the pipeline job to automate the Signing process.
pipeline {
agent any
environment {
DOTNET_ROOT = "${tool 'dotnet-sdk'}"
PATH = "${env.DOTNET_ROOT}/bin:${env.PATH}"
WIN_SERVER_IP = "172.31.91.18"
CREDS_ID = "9ffb7f24-acd9-412b-b1d0-ad76babf8297"
}
triggers {
githubPush()
}
stages {
stage('Checkout') {
steps {
checkout([$class: 'GitSCM',
branches: [[name: '*/main']],
userRemoteConfigs: [[
url: 'https://github.com/PrashantGSIN/CodeSigningAutomation.git',
credentialsId: 'CodeSigningAutomation'
]]
])
}
}
stage('Restore') {
steps {
sh 'dotnet restore'
}
}
stage('Build') {
steps {
sh 'dotnet build -c Release'
}
}
stage('Publish') {
steps {
sh 'dotnet publish -c Release -r win-x64 --self-contained true /p:PublishSingleFile=true'
}
}
stage('Archive Executable') {
steps {
archiveArtifacts artifacts: '**/*.exe', fingerprint: true
}
}
stage('Transfer Executable to Windows Server') {
steps {
sshagent(credentials: [CREDS_ID]) {
sh """
scp -o StrictHostKeyChecking=no \
/var/lib/jenkins/workspace/automationWithSignTool/bin/Release/net9.0/win-x64/publish/HelloWorldApp.exe \
administrator@${WIN_SERVER_IP}:"C:/Users/Administrator/Desktop/AutoSigning/input"
"""
}
}
}
stage('Sign Executable on Windows Server') {
steps {
sshagent(credentials: [CREDS_ID]) {
sh """
ssh -o StrictHostKeyChecking=no administrator@${WIN_SERVER_IP} "python C:/Users/Administrator/Desktop/AutoSigning/Scripts/authenticode_signer.py --sign \\"C:/Users/Administrator/Desktop/AutoSigning/input/HelloWorldApp.exe\\" --cert-thumbprint \\"1C9CB68272967E1DDC75607B1A79184985E56FDF\\" --kms-key-id \\"arn:aws:kms:us-east-1:800548176231:key/382d7ec7-e476-4fdc-8cce-513af1c00bf2\\""
"""
}
}
}
}
}
Once the build success you would be able to get the signed executable.
Check your certificate installation for SSL issues and vulnerabilities.