Skip to main content
With KEY_PROVIDER=aws-kms, Parmana signs inside AWS KMS. The private key is created in KMS and never leaves it. The server sends KMS the bytes to sign and receives a signature back. This page takes you from no key to a verified deployment. Do the steps in order. Every step ends with a Check.

What KMS signs, and what stays a file

So a KMS deployment still needs the gateway key file. Only the default key moves to KMS. See the Environment variable reference. Only KEY_PROVIDER values local and aws-kms work. azure-key-vault, gcp-kms and hsm pass validation but have no implementation, and the server refuses to start with them.

Before you start

Step 1: Create the key

The key must be an asymmetric Ed25519 signing key. The server accepts no other key spec and stops with KmsSigner only supports ECC_NIST_EDWARDS25519 keys if it finds one.
Note the KeyId in the output. Then give the key the alias the server looks for. The server turns the key id default into alias/default:
If you manage AWS with infrastructure as code, the CloudFormation resource AWS::KMS::Key takes the same KeySpec and KeyUsage values, and AWS::KMS::Alias creates the alias. Check:
You should see ECC_NIST_EDWARDS25519 SIGN_VERIFY Enabled. The default region for this project is ap-south-1. Use one region everywhere, because AWS_REGION in step 3 must match the key.

Step 2: Create the role that Vercel assumes

The server never holds an AWS access key. On Vercel it exchanges the project’s OIDC token for short lived credentials by assuming an IAM role. Create a role with a trust policy and a permission policy. Trust policy. It lets your Vercel project, and only it, assume the role. This is the format from the Vercel OIDC documentation for AWS. Replace the four values in square brackets:
The OIDC identity provider oidc.vercel.com/[TEAM SLUG] must exist in your AWS account first. Create it as the Vercel documentation describes. Give the role the production environment only. Do not add preview, because a preview deployment should not sign with the production key. Permission policy. Three actions on one key, and nothing else:
Note the role ARN, which looks like arn:aws:iam::[AWS ACCOUNT ID]:role/[ROLE NAME]. You need it in step 3. Check: in the IAM console, open the role and confirm the trust policy names your team and project, and the permission policy names your key.

Step 3: Set the environment on Vercel

Set these for the Production environment. The full meaning of each is in the Environment variable reference. Keep the gateway key set as before, either files in PARMANA_KEY_DIR or PARMANA_KEY_MATERIAL_JSON. Do not set an AWS access key. The server never reads one. Turn on OIDC federation for the Vercel project, as the Vercel documentation describes, so the function receives an OIDC token. Then deploy again, because variables are read once at startup. Check: vercel env ls production lists all three variables.

Step 4: Deploy and check the key is reachable

Then, with your base URL in URL:
The first call returns the public half of the KMS key:
/ready should answer {"status":"READY","authDisabled":false}. On Vercel the app is built on the first real request, not when the module loads, because the OIDC token only exists while a request is being handled. The first request after a cold start therefore does the setup work, including the call to KMS. Check: /keys/default returns "algorithm": "ed25519". A 500 here means the server could not reach the key. See Troubleshooting.

Step 5: Verify signing

The repository has a script that signs test data through the same KmsSigner the server uses, on both sides of the 4096 byte limit. It changes nothing in AWS. It makes about seven kms:Sign calls, one kms:GetPublicKey and one kms:DescribeKey per run. Sign in with a limited user first (see Sign in with a limited user), then run from the repository root:
On PowerShell:
Expected output:
The last check is a control. It sends 5000 raw bytes straight to KMS and requires KMS to refuse them. It proves the limit is real and that the commitment is what makes larger messages work. The exit code is 0 when every check passes. If AWS_ROLE_ARN is set in your shell, the script ignores it and says so, because that variable selects the Vercel OIDC token, which only exists on Vercel. Check: the last line reads All 7 checks passed.

Step 6: Verify a real record offline

Signing that verifies in a script is not the same as a record that a third party can check. Run one real request as in Integrate Parmana: specification for AI agents. Save the Execution Trust Record it returns, and get the key from the server:
Write the pem value into default.public.pem, then verify with only that public key:
Check: the output has "valid": true and "errors": []. Verifying the same record against a different public key must fail. A full record is usually larger than 4096 bytes, so this also proves the commitment path end to end. The rule a verifier you write yourself must follow is in Verify independently.

How messages over 4096 bytes are signed

KMS refuses a raw Ed25519 message longer than 4096 bytes. A full Execution Trust Record is larger than that. Parmana signs a message over the limit as a fixed 97 byte commitment instead: the bytes PARMANA-ED25519-LARGE-MESSAGE-V1, one NUL byte, then the SHA-512 digest of the message. The result is still an ordinary Ed25519 signature. Messages of 4096 bytes or fewer are signed unchanged. No marker is stored on the record, because the choice depends only on the message length. The decision is recorded in ADR-0010 in docs/adr.

Sign in with a limited user

Do not run checks as the AWS account root user. Use an IAM user that can do only what this page needs. A user with the three actions from step 2, on the one key, is enough to run npm run verify:kms. It does not need iam:*, and it should have no access keys. Sign in with the console based login, which stores short lived credentials on your machine and needs no access key:
At the browser page choose IAM user, and enter the account ID, the user name and the password. Add the AWS managed policy SignInLocalDevelopmentAccess to the user, because aws login needs it. Check:
The Arn should end in user/<your user name> and must not end in :root. Then confirm the user is limited: aws iam list-users --profile <your profile> should fail with AccessDenied.
A second aws login with the same --profile replaces the credentials stored for that profile. If you use one profile for both root and a limited user, the last login wins. Give each its own profile name.

Run the whole server against KMS locally

To reproduce a full request with KMS signing on your machine, start the server with these settings, on top of the minimum from the production runbook: The gateway key is still read from PARMANA_KEY_DIR. A connector that verifies the gateway’s authorization, such as the refund service, must use the same key provider and the same KMS access, or it will report an invalid signature.

Troubleshooting

What has been verified

Verified on 2026-09-20, against a real KMS key (alias/default, ap-south-1, ECC_NIST_EDWARDS25519, enabled), using a limited IAM user with only the three actions above: Not covered by those runs:
  1. The Vercel OIDC role. The runs used the local AWS credential chain. ADR-0010 records the live production verification.
  2. The real parmana-paytm-agent. The full run used the repository’s mock refund service.
  3. Key rotation and other regions. Neither was tested.

Next

Production deployment runbook

The full procedure to deploy and check a server.

Verify independently

Check a signed record without trusting the server, including the large message rule.