Automate Code Signing with Jenkins, AWS KMS, and Signtool

Sep 4, 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: Launch AWS Windows Instance, Configure AWS CLI, Create Keys using AWS KMS and Create CSR to Import GlobalSign’s Certificate

  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

  3. Install Signtool.

    • Download Windows 10 SDK from Microsoft.
    • Open the downloaded file and follow the wizard for the installation

  4. Install the Python and boto3 library.

    • Install the required package for Python
    • pip install boto3 cryptography

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

  6. Configure the AWS account with AWS CLI:
    aws configure

    • Enter the required AWS credentials like AWS Client Key and Secret for Configuration.

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

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

  9. 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 --keyid alias/your-key-alias --hashalgo sha256 dummy.csr > signed.csr

    • Then, you would get the signed CSR from AWS KMS.

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

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

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

     

Step 2: Configure Jenkins Server and Connect it with your Windows Instance

  1. Install Jenkins on Linux.

    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
  2. After the execution of the above commands, the Jenkins server would be accessible over http://:8080. Once you are logged in you can get the dashboard of the Jenkins.

  3. 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
              Start-Service sshd

         
    2. Test the connect manually. Transfer Files from Jenkins to Windows Server. 
         3. Use scp in your Jenkins pipeline to copy files:

             scp /path/to/file administrator@:"C:/path/on/windows"

         
    4. Login to the Windows server and you would get the file that just got copied through scp.

 

Step 3: Run Jenkins Pipeline to Sign your Executable files automatically

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


        }
    }

  2. Once the build success you would be able to get the signed executable.

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