Create your first context and signing key
By the end of this guide, your VTA has a context set aside for one application, with a signing key inside it, and you will have used that key to sign a real payload.
Prerequisites
- A running VTA with
pnmconnected. See Deploy VTA appliance or Self-hosted (open source) if you haven’t deployed one yet. - Your VTA’s slug to hand.
- Affinidi-hosted: the
VTA slugprinted bypnm bootstrap connect. - Self-hosted: the
slugvalue printed bypnm setup, which ismy-vtain that guide.
- Affinidi-hosted: the
pnm --vta <vta-slug> healthshows ✓ on every check.- Admin access to the VTA.
Steps
Create a context
A context is a named namespace: everything you create inside it, keys included, stays isolated from every other context on the same VTA.
pnm --vta <vta-slug> contexts create \
--id my-app \
--name "My App"Replace <vta-slug> with your VTA’s slug, from the Prerequisites above.
Expected output:
VTA: <vta-slug>
DID: did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:your-appliance.vta.affinidi.io
Context created:
ID: my-app
Name: My App
Base Path: m/26'/2'/2'Mint a signing key
Every context starts empty. Mint an Ed25519 key inside it so there’s something to sign with.
pnm --vta <vta-slug> keys create \
--key-type ed25519 \
--context my-app \
--label "my-app-key-1"Expected output:
VTA: <vta-slug>
DID: did:webvh:QmVE1TQeCtg3aavpTqasqencJpagRr8JKdGRyoZ5Qx6kRp:your-appliance.vta.affinidi.io
Key created:
Key ID: m/26'/2'/2'/0'
Key Type: ed25519
Derivation Path: m/26'/2'/2'/0'
Public Key: z6MkvZqY2CbpWQmbjuuNshtAptH8qbFaCGk3w6Jt72xBXiGi
Status: active
Label: my-app-key-1
Created At: 2026-09-11 19:42:44 +08:00The Key ID is the same value as the Derivation Path: a BIP-32 path, not a short opaque ID. That matters in the next step.
Sign your first payload
pnm has no built-in sign command, but you don’t need one to try signing right now: pnm auth show-token prints your current bearer token, and the VTA’s REST API takes it from there.
A Key ID like m/26'/2'/2'/0' contains / characters, so it cannot go into a URL path unencoded: each / would be read as a path separator, and the request would 404. Percent-encode it first:
KEY_ID="m/26'/2'/2'/0'"
KEY_ID_ENCODED=$(printf '%s' "$KEY_ID" | sed "s#/#%2F#g")
TOKEN=$(pnm --vta <vta-slug> auth show-token)
curl -X POST "https://<vta-url>/keys/${KEY_ID_ENCODED}/sign" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"payload": "aGVsbG8", "algorithm": "EdDSA"}'Set KEY_ID to the Key ID from the previous step. For <vta-url>, use your VTA’s REST endpoint:
- Affinidi-hosted: the VTA URL shown on the configuration page in Affinidi Portal.
- Self-hosted: the
public_urlyou set insetup.toml. Where that is anhttp://address such ashttp://localhost:8100, change thehttps://in the snippet to match.
payload is your message, base64url-encoded. aGVsbG8 decodes to hello. The signing key never leaves the VTA: only the signature comes back.
This uses your own admin session token, which is fine for a quick check but not how an application should sign in production. See Sign application payloads without exposing your keys for provisioning a least-privilege application credential instead.
Confirm
The curl call above returns a JSON body containing a base64url-encoded signature. That signature is proof your key works, without ever exposing the private key itself.
You can also confirm the key exists independently of signing:
pnm --vta <vta-slug> keys list --context my-app --status activeYour new key appears in the list, with Status: active and the my-app context.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
context already exists: my-app | A context with this ID already exists on the VTA. | Pick a different --id, or use the existing context instead of creating a new one. |
unauthorized on pnm contexts create | Your session does not have admin access. | Run pnm auth status to check your session, then contact a VTA admin if you’re not one. |
pnm keys list returns no keys | The key was minted in a different context, or --context doesn’t match. | Re-run pnm --vta <vta-slug> keys list --context my-app without --status to see every key, active or not. |
curl returns 401 Unauthorized | The token from pnm auth show-token expired, or wasn’t captured correctly. | Re-run TOKEN=$(pnm --vta <vta-slug> auth show-token) immediately before the curl call; tokens are short-lived. |
curl returns 404 Not Found on the key | KEY_ID_ENCODED wasn’t percent-encoded, so the / characters split the URL into the wrong path, or KEY_ID doesn’t match the Key ID printed when you minted the key. | Confirm KEY_ID_ENCODED has no raw / characters left in it, and that KEY_ID matches Step 2’s output exactly. |
curl returns {"error":"unauthorized: invalid token: InvalidSignature"} | The <vta-url> in the curl command belongs to a different VTA than the one --vta <vta-slug> fetched the token from. Each VTA signs its own tokens; one VTA’s token never verifies against another. | Confirm --vta <vta-slug> on the pnm auth show-token call and <vta-url> in the curl command point at the same VTA. |
Next steps
- Sign application payloads without exposing your keys: Provision an application credential for this context and start signing with the key you just minted.
- Keys and contexts: Understand how context isolation and key derivation work underneath this guide.
- Mint, inspect, and retire signing keys: list what a context holds, relabel a key, and take one out of service.
- Glossary: decode the terms you see in
pnmoutput, fromVTA slugtoPCR0.
Glad to hear it! Please tell us how we can improve more.
Sorry to hear that. Please tell us how we can improve.
Thank you for sharing your feedback so we can improve your experience.