Using AWS KMS as Your Code Signing HSM


If you’ve ever needed to sign Windows executables with an Extended Validation certificate (EV cert), you’ve probably run into the Hardware Security Module (HSM) requirement. EV certificates require your private key to be stored in an HSM, a tamper-resistant device that never exposes the key material. The catch? HSMs are expensive and complicated.

I spent a couple of weeks figuring out how to use AWS KMS as a drop-in replacement for a traditional HSM. It’s been running in production for over two years now, signing thousands of installers, and the total cost is under $50/month. Here’s how to set it up yourself.

Quick glossary:

  • HSM: Hardware Security Module - a tamper-resistant device that stores cryptographic keys
  • EV: Extended Validation - the highest level of certificate validation for code signing
  • CSR: Certificate Signing Request - a file containing your organization details and public key
  • CA: Certificate Authority - the company that issues your certificate (like DigiCert, Sectigo)
  • KMS: AWS Key Management Service - AWS’s managed encryption service

Why This Works

When you create a KMS key with key_usage = "SIGN_VERIFY", AWS stores that key in FIPS 140-2 Level 3 validated hardware (the same HSMs that power CloudHSM). The private key never leaves AWS’s infrastructure. You interact with it purely through API calls: “here’s a hash, please sign it.”

This is exactly what an HSM does. The difference is pricing:

OptionMonthly Cost
CloudHSM (2 nodes for HA)~$2,100
Physical HSM appliance$30K-$100K upfront
KMS signing key~$1

The API calls add a bit more ($0.03 per 10,000 signatures), but unless you’re signing millions of files, it’s negligible.

The Challenge: Getting a Certificate

To get an EV certificate, you need to send a Certificate Signing Request (CSR - the file that contains your organization’s details and public key) to a Certificate Authority (CA). The CSR must be signed by your private key — that’s how the CA knows you actually control the key.

But our private key is locked inside KMS. We can’t export it (that’s the whole point). So how do we create a CSR?

The trick is to generate the CSR using KMS itself. You ask KMS for the public key, build the CSR structure, then ask KMS to sign it. The result is a valid CSR that proves control of the KMS key.

We wrote a CLI tool to handle this: kmscsr.

Step 1: Create a KMS Key

First, create an asymmetric signing key in KMS. You can do this with Terraform:

resource "aws_kms_key" "code_signing" {
  description              = "Code signing key"
  key_usage                = "SIGN_VERIFY"
  customer_master_key_spec = "RSA_4096"
}

resource "aws_kms_alias" "code_signing" {
  name          = "alias/code-signing"
  target_key_id = aws_kms_key.code_signing.id
}

Or with the AWS CLI:

aws kms create-key \
  --key-usage SIGN_VERIFY \
  --customer-master-key-spec RSA_4096 \
  --description "Code signing key"

Note the key ARN, you’ll need it for the next steps.

Step 2: Generate a CSR with kmscsr

Install the tool:

go install github.com/cavenine/kmscsr/cmd/kmscsr@latest

Then generate your CSR:

kmscsr \
  --kms-arn "arn:aws:kms:us-east-1:123456789:key/abcd-1234-efgh-5678" \
  --common-name "Your Company Name" \
  --organization "Your Company Inc" \
  --country "US" \
  --state "California" \
  --locality "San Francisco" \
  --output code-signing.csr

This does several things behind the scenes:

  1. Fetches the public key from KMS
  2. Builds a PKCS#10 CSR structure with your organization details
  3. Computes a hash of the CSR
  4. Sends that hash to KMS for signing
  5. Assembles the final CSR with the signature

The output is a standard CSR file that any Certificate Authority will accept.

Step 3: Get Your Certificate

Submit the CSR to your CA through their normal process. For EV certificates, this typically involves some back-and-forth to verify your organization’s identity. Once approved, they’ll issue a certificate (usually a .crt or .pem file).

The certificate ties your organization’s verified identity to the public key that lives in KMS. The private key, of course, never left AWS.

Step 4: Sign Files with JSign

JSign is an open-source tool for creating Authenticode signatures. It has native support for AWS KMS, so you can sign files directly without any custom code.

Install JSign (on macOS with Homebrew):

brew install jsign

Or download the JAR from the releases page.

Now sign your executable:

jsign \
  --storetype AWS \
  --keystore us-east-1 \
  --alias "alias/code-signing" \
  --certfile certificate-chain.pem \
  --tsaurl http://timestamp.digicert.com \
  application.exe

Let’s break down the flags:

  • --storetype AWS tells JSign to use AWS KMS
  • --keystore us-east-1 specifies the AWS region
  • --alias is your KMS key alias or ID
  • --certfile is the certificate you got from the CA
  • --tsaurl is a timestamp server (important — more on this below)

JSign handles the Authenticode format, computes the right hashes, calls KMS to sign them, and embeds everything in the executable.

A Note on Timestamping

Always timestamp your signatures. Without a timestamp, the signature becomes invalid when your certificate expires. With a timestamp, the signature remains valid as long as it was created while the certificate was valid.

DigiCert’s free timestamp server (http://timestamp.digicert.com) works fine. JSign will retry automatically if the server is slow.

AWS Credentials

JSign uses the standard AWS credential chain. If you have the AWS CLI configured, it should just work. Otherwise, set the environment variables:

export AWS_ACCESS_KEY_ID=your-access-key
export AWS_SECRET_ACCESS_KEY=your-secret-key
export AWS_REGION=us-east-1

Or if you’re running in EC2/Lambda with an IAM role, it’ll pick up the credentials automatically.

The IAM permissions needed are:

  • kms:Sign
  • kms:GetPublicKey
  • kms:DescribeKey

Securing Access to the Key

With a physical HSM, controlling who can sign usually means network segmentation, physical access controls, or middleware configuration. With KMS, you just use IAM.

Only principals with kms:Sign permission on the key can create signatures. To lock this down, create a policy that grants signing access only to specific roles:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "kms:Sign",
        "kms:GetPublicKey",
        "kms:DescribeKey"
      ],
      "Resource": "arn:aws:kms:us-east-1:123456789:key/abcd-1234-efgh-5678"
    }
  ]
}

Attach this to your CI/CD role and nothing else. IAM denies by default, so anyone without explicit access can’t sign. CloudTrail logs every signing operation if you need an audit trail.

When to Use This (and When Not To)

This approach works well for:

  • CI/CD pipelines that sign releases automatically during builds
  • On-demand signing services (I’ve used this in Lambda functions that sign installers per-request)
  • Small to medium volume: hundreds or thousands of signatures per day

It might not be the best fit if:

  • You need PKCS#11 compatibility. Some tools only speak PKCS#11, and while there are bridges, they add complexity.
  • You’re signing millions of files daily. At that scale, the API costs might exceed CloudHSM’s flat rate.
  • Regulatory requirements mandate CloudHSM or dedicated hardware.

Production Reality

The setup we’ve described here has been running in production for a couple of years, signing Windows installers on-demand. The total AWS bill for this component is under $50/month, down from $2,000+ when we were running CloudHSM.

For signing purposes, KMS behaves like an HSM: the key never leaves hardware, and you interact with it through sign/verify operations. The difference is the API and the price. The CSR generation is the only tricky part, and kmscsr handles that.

If you’re currently paying for CloudHSM just to satisfy the EV certificate HSM requirement, this approach is worth considering.


Resources: