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:
| Option | Monthly 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:
- Fetches the public key from KMS
- Builds a PKCS#10 CSR structure with your organization details
- Computes a hash of the CSR
- Sends that hash to KMS for signing
- 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 AWStells JSign to use AWS KMS--keystore us-east-1specifies the AWS region--aliasis your KMS key alias or ID--certfileis the certificate you got from the CA--tsaurlis 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:Signkms:GetPublicKeykms: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:
- kmscsr — CLI tool for generating KMS-signed CSRs
- JSign — Authenticode signing tool with AWS KMS support
- AWS KMS key concepts