Sep 1, 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
Download Windows 10 SDK from Microsoft.
Open the downloaded file and follow the wizard for the installation.
Install the Python
Install the required package for Python: pip install boto3 cryptography
Configure the AWS account with AWS CLI:
aws configure
Create an asymmetric key for signing:
aws kms create-key --description "KMS key for CSR signing" --key-usage SIGN_VERIFY --key-spec RSA_4096 --origin AWS_KMS
aws kms create-alias --alias-name alias/your-key-alias --target-key-id KeyID
openssl req -new -newkey rsa:2048 -nodes -keyout dummy.key -out dummy.csr
#!/usr/bin/env python3
"""
python script to re-sign an existing CSR with an asymmetric keypair held in AWS KMS
"""
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-----'
def sign_certification_request_info(kms, key_id, csr, digest_algorithm, signing_algorithm):
certificationRequestInfo = csr['certificationRequestInfo']
der_bytes = encoder.encode(certificationRequestInfo)
digest = hashlib.new(digest_algorithm)
digest.update(der_bytes)
digest = digest.digest()
response = kms.sign(KeyId=key_id, Message=digest, MessageType='DIGEST', SigningAlgorithm=signing_algorithm)
return response['Signature']
def output_csr(csr):
print(start_marker)
b64 = base64.b64encode(encoder.encode(csr)).decode('ascii')
for line in textwrap.wrap(b64, width=64):
print(line)
print(end_marker)
def signing_algorithm(hashalgo, signalgo):
# Signature Algorithm OIDs retrieved from
# https://www.ibm.com/docs/en/linux-on-systems?topic=linuxonibm/com.ibm.linux.z.wskc.doc/wskc_pka_pim_restrictions.html
if hashalgo == 'sha512' and signalgo == 'ECDSA':
return 'ECDSA_SHA_512', '1.2.840.10045.4.3.4'
elif hashalgo == 'sha384' and signalgo == 'ECDSA':
return 'ECDSA_SHA_384', '1.2.840.10045.4.3.3'
elif hashalgo == 'sha256' and signalgo == 'ECDSA':
return 'ECDSA_SHA_256', '1.2.840.10045.4.3.2'
elif hashalgo == 'sha224' and signalgo == 'ECDSA':
return 'ECDSA_SHA_224', '1.2.840.10045.4.3.1'
elif hashalgo == 'sha512' and signalgo == 'RSA':
return 'RSASSA_PKCS1_V1_5_SHA_512', '1.2.840.113549.1.1.13'
elif hashalgo == 'sha384' and signalgo == 'RSA':
return 'RSASSA_PKCS1_V1_5_SHA_384', '1.2.840.113549.1.1.12'
elif hashalgo == 'sha256' and signalgo == 'RSA':
return 'RSASSA_PKCS1_V1_5_SHA_256', '1.2.840.113549.1.1.11'
else:
raise Exception('unknown hash algorithm, please specify one of sha224, sha256, sha384, or sha512')
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())[0]
if not csr:
raise Exception('file does not look like a CSR')
# now get the key
if not args.region:
args.region = boto3.session.Session().region_name
if args.profile:
boto3.setup_default_session(profile_name=args.profile)
kms = boto3.client('kms', region_name=args.region)
response = kms.get_public_key(KeyId=args.keyid)
pubkey_der = response['PublicKey']
csr['certificationRequestInfo']['subjectPKInfo'] = \
decoder.decode(pubkey_der, pyasn1_modules.rfc2314.SubjectPublicKeyInfo())[0]
signatureBytes = sign_certification_request_info(kms, args.keyid, csr, args.hashalgo,
signing_algorithm(args.hashalgo, args.signalgo)[0])
csr.setComponentByName('signature', univ.BitString.fromOctetString(signatureBytes))
sigAlgIdentifier = pyasn1_modules.rfc2314.SignatureAlgorithmIdentifier()
sigAlgIdentifier.setComponentByName('algorithm',
univ.ObjectIdentifier(signing_algorithm(args.hashalgo, args.signalgo)[1]))
csr.setComponentByName('signatureAlgorithm', sigAlgIdentifier)
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', action='store', dest='keyid', help='key ID in AWS KMS')
parser.add_argument('--region', action='store', dest='region', help='AWS region')
parser.add_argument('--profile', action='store', dest='profile', help='AWS profile')
parser.add_argument('--hashalgo', choices=['sha224', 'sha256', 'sha512', 'sha384'], default="sha256",
help='hash algorithm to choose')
parser.add_argument('--signalgo', choices=['ECDSA', 'RSA'], default="RSA", help='signing algorithm to choose')
args = parser.parse_args()
main(args)
After creating the Python file, use this script in the below command for signing the CSR:
openssl req -new -newkey rsa:2048 -nodes -keyout dummy.key -out dummy.csr
Log in to GlobalSign Certificate Center (GCC).
Order a new Code Signing Certificate and choose HSM-based.
Once approved, submit the CSR and download and install the issued certificate (.cer file).
Use the below commands to install the certificates into the local machine:
Import-Certificate -FilePath "C:\Users\Administrator\Desktop\cert.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\"
Place the Python signing script below on the Windows server. Make sure place the script over the Windows server for the execution.
This script performs:
• Generating a digest with SignTool
• Signing with AWS KMS
• Incorporating signed digest into executable
• Adding timestamp (http://timestamp.globalsign.com/tsa/advanced)
• Verifying the signature
#!/usr/bin/env python3
"""
Hybrid SignTool + AWS KMS Signing (Alternative Approach)
========================================================
This tries a different approach to the signing process.
"""
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('<I', data[60:64])[0]
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.digicert.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.digicert.com', '/td', 'sha256', exe_path],
# Method 3: Just thumbprint (let SignTool find it)
[signtool, 'sign', '/sha1', cert_thumbprint, '/fd', 'sha256', '/tr', 'http://timestamp.digicert.com', '/td', 'sha256', exe_path],
# Method 4: Auto-detect certificate store
[signtool, 'sign', '/a', '/sha1', cert_thumbprint, '/fd', 'sha256', '/tr', 'http://timestamp.digicert.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()
After setting up the things, use the Jenkins pipeline job to automate the code signing.
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\\""
"""
}
}
}
}
}
Check your certificate installation for SSL issues and vulnerabilities.