Automate Code Signing with Jenkins, AWS KMS, and Signtool

Sep 1, 2025

Automate Code Signing with Jenkins, AWS KMS, and Signtool

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

Prerequisites 

  • 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

Guidelines

Step 1: Set Up AWS CLI

  1. Launch the AWS Windows ec2 instance with Windows server 2019

  2. 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

Step 2: Install Signtool

  1. Download Windows 10 SDK from Microsoft.

  2. Open the downloaded file and follow the wizard for the installation.

Step 3: Install Python

  1. Install the Python

  2. Install the required package for Python: pip install boto3 cryptography

Step 4: Set Up AWS KMS

  1. Configure the AWS account with AWS CLI:

    aws configure

  2. 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 

  3. Assign an alias: 

    aws kms create-alias --alias-name alias/your-key-alias --target-key-id KeyID 

Step 5: Generate and Sign CSR 

  1. Generate a CSR template with OpenSSL: 

    openssl req -new -newkey rsa:2048 -nodes -keyout dummy.key -out dummy.csr 
     
  2. Sign the CSR with key stored in AWS KMS. Below aws-kms-sign-csr.py is the python script used for signing the 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)

  3. 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

Step 6: Submit CSR to GlobalSign (GCC) 

  1. Log in to GlobalSign Certificate Center (GCC). 

  2. Order a new Code Signing Certificate and choose HSM-based. 

  3. Once approved, submit the CSR and download and install the issued certificate (.cer file). 

Step 7: Import Certificate into Windows Certificate Store

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\" 

Step 8: Automate Signing with Jenkins 

  1. 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()

  2. 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\\""
                        """
            }
        }
    }


        }
    }

Step 9: Verify Signed Executable 

  1. Check that the signature is valid and timestamped by GlobalSign TSA. 

Related Articles

GlobalSign System Alerts

View recent system alerts.

View Alerts

Atlas Discovery

Scan your endpoints to locate all of your Certificates.

Sign Up

SSL Configuration Test

Check your certificate installation for SSL issues and vulnerabilities.

Contact Support