Select programming language for code examples

linkSignatures

You may utilize your account's public key to verify that API responses and webhook requests all originated from Keygen, as well as to verify a license key is "authentic" i.e. that it was signed using your Keygen account's private key.

You can find your account's public key within your dashboard settings page, which you can use to verify response payloads and license keys. Private keys are kept securely encrypted on our servers and are never, under any circumstances, shared with you or any other third-party.

linkLicense Signatures

License signature verification is useful for checking if a given license key is authentic, especially in offline environments, where access to Keygen's API to fully validate the license is not available. For more information on license key cryptography and signatures, see the info below, view the cryptography section and review the various policy schemes available.

Here are a few examples of cryptographically verifying a license's authenticity:

The signed or encrypted contents of the key are base64url encoded using RFC 4648, a URL-safe version of base64 which is supported in most programming languages. This base64url encoding scheme is different than normal base64 encoding. Most programming languages will have a separate function for decoding URL base64 encoded values, but if not, you can simply replace all "-" chars with "+", and replace "_" with "/", e.g. tr '-_' '+/' to convert the encoded string from base64url encoding to standard base64 encoding.

Example RSA_2048_PKCS1_SIGN_V2 verification using Python

from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
import base64
 
# This should be replaced with your Keygen account's public key (note: all newlines and whitespace must be *exact*)
PUBLIC_KEY = \
"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzPAseDYupK78ZUaSbGw7
YyUCCeKo/1XqTACOcmTTHHGgeHacLK2j9UrbTlhW5h8Vyo0iUEHrY1Kgf4wwiGgF
h0Yc+oDWDhq1bIertI03AE420LbpUf6OTioX+nY0EInxXF3J7aAdx/R/nYgRJrLZ
9ATWaQVSgf3vtxCtCwUeKxKZI41GA/9KHTcCmd3BryAQ1piYPr+qrEGf2NDJgr3W
vVrMtnjeoordAaCTyYKtfm56WGXeXr43dfdejBuIkI5kqSzwVyoxhnjE/Rj6xks8
ffH+dkAPNwm0IpxXJerybjmPWyv7iyXEUN8CKG+6430D7NoYHp/c991ZHQBUs59g
vwIDAQAB
-----END PUBLIC KEY-----"""
 
# This should be the license key that you're cryptographically verifying
LICENSE_KEY = \
"""key/eyJhY2NvdW50Ijp7ImlkIjoiYmY5YjUyM2YtZGQ2NS00OGEyLTk1MTItZmI2NmJhNmMzNzE0In0sInByb2R1Y3QiOnsiaWQiOiI5NTYxYzdkMC1mYzczLTRjOTQtYTZlZC0xY2M3MmEzZTAzNzYifSwicG9saWN5Ijp7ImlkIjoiOTRiYTA5YjYtMzI5ZC00NWNjLTg4ZjctNzM5N2Y2YzgwNDg4In0sInVzZXIiOnsiaWQiOiIxYmEwYzk4Yy0wMGRhLTQ2MjEtOTljNS1jMWU1ZjVkZjk1NzUiLCJlbWFpbCI6InVzZXJAYXBwLmV4YW1wbGUifSwibGljZW5zZSI6eyJpZCI6IjE2YmI3YmM2LTBlNDItNDYyZS04OWE1LWVmNzdjNDYwNzJjYiIsImNyZWF0ZWQiOiIyMDIxLTAzLTE2VDEzOjE4OjE3LjU1M1oiLCJleHBpcnkiOiIyMDIyLTAzLTE2VDEzOjE4OjE3LjU1M1oifX0=.K0VwYlN3LVdRniqL5lpDDqticp1-LhmW-WEyRmK7rhGHFhBMThHMp1qjzpdSHaV4bKpTFQgnmj5p-Bw7n9nKnYZ35kmc_2bnid-69q0G6sA2iXEtS3I5717cLArN7HL5l-lBTYZwSTbq_C_NnnpvxQ2k8aMSOwP9ZkV8SECee9VOEniDD7URZIYMf06JVzSsfprAaEcXEGaSK_LXw5y0cy0o9hcmPC50JdWPPxQtj13DKt0Yc_0CkyHvVfZO8h_hdRf5JSCW9UZumUrHk_wqQf3j-DBfmOytKTidmsIaQqE2KyoJh_-U1T1z4fx44wYDFTLEncr4uWX3zzUTf_cVIA=="""
 
# Split license key to obtain signing data and signature, then parse data
# and decode base64url encoded values
signing_data, enc_sig = LICENSE_KEY.split(".")
signing_prefix, enc_key = signing_data.split("/")
 
key = base64.urlsafe_b64decode(enc_key)
sig = base64.urlsafe_b64decode(enc_sig)
 
# Verify the key's signature
pub_key = RSA.importKey(PUBLIC_KEY)
verifier = PKCS1_v1_5.new(pub_key)
digest = SHA256.new(data="key/" + enc_key)
 
print(
verifier.verify(digest, sig)
)

linkResponse Signatures

Response signature verification is useful for a variety of scenarios where verifying that a response came from Keygen's servers is vital, such as:

  1. Preventing man-in-the-middle attacks. Verifying responses using your public keys will ensure that only responses signed using your private key are accepted. We are the only ones in possession of your account's private keys, so you can rest assured the response was from us and that it has not been altered.
  2. Preventing spoofing attacks. For example, a bad actor could redirect requests to a local licensing server under their control, which defaults to sending "valid" responses. Much like with preventing man-in-the-middle attacks, cryptographic signatures give confidence that the response was from Keygen's servers.
  3. Preventing replay attacks. For example, a bad actor could "record" web traffic between Keygen and your software, and "replay" valid responses, such as replaying responses that occurred before their trial license expires, in order to use your software with an expired license. If the signature is valid, but the response date is older than 5 minutes, we recommend rejecting the response (granted the local time is sychronized using NTP).
  4. Verifying the authenticity of cached data in offline environments. For example, when you perform a license validation request and cache the response for later offline-use, you would want to verify the response signature to ensure that the cache data has not been tampered with.
  5. Verifying the authenticity of webhook events sent to your endpoints. Use request signatures to check if a webhook event was sent from us before processessing. This is especially critical when webhooks are used to automate things like billing.
We do not sign certain error payloads - please keep this in mind during implementation. We will always sign successful responses (2xx–3xx) and errors that occur while authenticated. Certain error responses, such as a bad request error due to a malformed request, or an internal server error, or a request to an invalid account, will not include the Keygen-Signature header.

Relevant response headers

Header Description
Keygen-Signature The signature result for the response. Depending on the Keygen-Accept-Signature algorithm, this may be signed using Ed25519, RSA-PSS-SHA256 or RSA-SHA256. Default is Ed25519.
Digest The base64 encoded SHA-256 digest of the response body. This header is used in the signing data.
Date The date of the response. This header is used in the signing data.

The header format

Below is the format for the Keygen-Signature header, according to the draft RFC. We have added newlines for readability.

Host: api.keygen.sh
Date: Wed, 09 Jun 2021 16:08:15 GMT
Digest: sha-256=827Op2un8OT9KJuN1siRs5h6mxjrUh4LJag66dQjnIM=
Keygen-Signature: keyid="bf9b523f-dd65-48a2-9512-fb66ba6c3714",
algorithm="ed25519",
signature="KhgcM+Ywv+DnQj4gE+DqWfNTM2TG5wfRuFQZ/zW48ValZuCHEu1h95Uyldqe7I85sS/QliCiRAF5QfW8ZN2vAw==",
headers="(request-target) host date digest"

The signature header is made up of 4 components:

Description
keyid The ID of the private key used to sign the response. For the time being, this is your account ID.
algorithm The algorithm used to sign the response. Supported algorithms are: ed25519, rsa-pss-sha256, and rsa-sha256.
signature The base64 encoded signature of the signing data.
headers The headers used to build the signing data.

Verifying response signatures

To verify the response signature, the signing data must be reconstructed. The signing data consists of the following 4 components:

Request target

The first component is what is known as the (request-target). This is formed using the lowercased HTTP method used for the request as well as the request path, and any accompanying query parameters.

For example, it may look something like:

post /v1/accounts/keygen/licenses/actions/validate-key

Or if you include any query parameters in the request, it would look like:

get /v1/accounts/keygen/licenses?page[size]=10&page[number]=1

Host

The Host that the request was sent to. For requests sent to us, this will always be api.keygen.sh. For example, for an API request to Keygen:

POST https://api.keygen.sh/v1/accounts/keygen/licenses/actions/validate-key
...
Host: api.keygen.sh

For a webhook event request sent to you, this will be your webhook endpoint's host.

POST https://webhooks.some-app.example/keygen
...
Host: webhooks.some-app.example

Date

The datetime at which the server sent the response. This will conform to the W3C’s Date header format. The full header from us will look something like:

Date: Wed, 09 Jun 2021 16:08:15 GMT

Digest

To obtain a digest for a response from our API, take the response body and run it through SHA-256. Then take this value and get its base64 value. For webhooks, this will be a digest of the request body.

The full Digest header from us will look something like:

Digest: sha-256=827Op2un8OT9KJuN1siRs5h6mxjrUh4LJag66dQjnIM=

When reconstructing the signing data, do not use the digest header we send. You should calculate your own SHA-256 digest of the request or response body, and then compare your encoded hash digest to the digest header we send.

When the request or response body is empty, such as with a 204 No Content response, you should still hash the body as if it were an empty string.

Remember to prefix the encoded digest with sha-256=.

Reconstructing the signing data

The first step is to construct a signature string based on the following template using all of the components you have already determined:

(request-target): get /v1/accounts/keygen/licenses?limit=1\nhost: api.keygen.sh\ndate: Wed, 09 Jun 2021 16:08:15 GMT\ndigest: sha-256=827Op2un8OT9KJuN1siRs5h6mxjrUh4LJag66dQjnIM=

For this particular example, the signing data was as follows, formatted for readability:

(request-target): get /v1/accounts/keygen/licenses?limit=1\n
host: api.keygen.sh\n
date: Wed, 09 Jun 2021 16:08:15 GMT\n
digest: sha-256=827Op2un8OT9KJuN1siRs5h6mxjrUh4LJag66dQjnIM=

Some things to pay special attention to:

  • The order of components must be (request-target) host date digest
  • Each component must be delimited by a newline character (\n)
  • Each component name is lowercased i.e. host: not Host:
  • The (request-target) HTTP method is lowercased
  • The (request-target) URI path must match exactly what was sent
  • The encoded digest is prefixed with sha-256=
  • There is no trailing newline character

Verifying the signing data

Once you've reconstructed the signing data, you can now verify it using your chosen algorithm. In this Python example, we'll use the default algorithm, Ed25519.

import ed25519
import hashlib
import base64
 
# Example of a typical "response" object
response = {
'status': 200,
'body': """{"data":[{"id":"63ac9241-0bff-4a64-83bb-df6aec781b0e","type":"licenses","attributes":{"name":"Ed25519 License","key":"key/eyJhY2NvdW50Ijp7ImlkIjoiYmY5YjUyM2YtZGQ2NS00OGEyLTk1MTItZmI2NmJhNmMzNzE0In0sInByb2R1Y3QiOnsiaWQiOiI5NTYxYzdkMC1mYzczLTRjOTQtYTZlZC0xY2M3MmEzZTAzNzYifSwicG9saWN5Ijp7ImlkIjoiNTQ2ZTc0OGUtZjhmYS00ODBjLWJjMDItNjYzMjdjOGZkMGZmIiwiZHVyYXRpb24iOm51bGx9LCJ1c2VyIjpudWxsLCJsaWNlbnNlIjp7ImlkIjoiNjNhYzkyNDEtMGJmZi00YTY0LTgzYmItZGY2YWVjNzgxYjBlIiwiY3JlYXRlZCI6IjIwMjEtMDYtMDFUMTU6MTM6NTMuMjUzWiIsImV4cGlyeSI6bnVsbH19.4ctbpwScfuuxkcynfPbmDrfwJojEHBc7ixgdSy9OKZtIRWEatzbWez3P1UwMhf7fMHXffIdUg5Nb41zqqjRqAA==","expiry":null,"uses":0,"suspended":false,"scheme":"ED25519_SIGN","encrypted":false,"strict":false,"floating":false,"concurrent":false,"protected":false,"maxMachines":1,"maxCores":null,"maxUses":null,"requireCheckIn":false,"lastValidated":"2021-06-04T17:00:58.680Z","lastCheckIn":null,"nextCheckIn":null,"metadata":{},"created":"2021-06-01T15:13:53.253Z","updated":"2021-06-04T17:00:58.680Z"},"relationships":{"account":{"links":{"related":"/v1/accounts/bf9b523f-dd65-48a2-9512-fb66ba6c3714"},"data":{"type":"accounts","id":"bf9b523f-dd65-48a2-9512-fb66ba6c3714"}},"product":{"links":{"related":"/v1/accounts/bf9b523f-dd65-48a2-9512-fb66ba6c3714/licenses/63ac9241-0bff-4a64-83bb-df6aec781b0e/product"},"data":{"type":"products","id":"9561c7d0-fc73-4c94-a6ed-1cc72a3e0376"}},"policy":{"links":{"related":"/v1/accounts/bf9b523f-dd65-48a2-9512-fb66ba6c3714/licenses/63ac9241-0bff-4a64-83bb-df6aec781b0e/policy"},"data":{"type":"policies","id":"546e748e-f8fa-480c-bc02-66327c8fd0ff"}},"user":{"links":{"related":"/v1/accounts/bf9b523f-dd65-48a2-9512-fb66ba6c3714/licenses/63ac9241-0bff-4a64-83bb-df6aec781b0e/user"},"data":null},"machines":{"links":{"related":"/v1/accounts/bf9b523f-dd65-48a2-9512-fb66ba6c3714/licenses/63ac9241-0bff-4a64-83bb-df6aec781b0e/machines"},"meta":{"cores":0,"count":0}},"tokens":{"links":{"related":"/v1/accounts/bf9b523f-dd65-48a2-9512-fb66ba6c3714/licenses/63ac9241-0bff-4a64-83bb-df6aec781b0e/tokens"}},"entitlements":{"links":{"related":"/v1/accounts/bf9b523f-dd65-48a2-9512-fb66ba6c3714/licenses/63ac9241-0bff-4a64-83bb-df6aec781b0e/entitlements"}}},"links":{"self":"/v1/accounts/bf9b523f-dd65-48a2-9512-fb66ba6c3714/licenses/63ac9241-0bff-4a64-83bb-df6aec781b0e"}}]}""",
'headers': {
'keygen-signature': 'keyid="bf9b523f-dd65-48a2-9512-fb66ba6c3714", algorithm="ed25519", signature="KhgcM+Ywv+DnQj4gE+DqWfNTM2TG5wfRuFQZ/zW48ValZuCHEu1h95Uyldqe7I85sS/QliCiRAF5QfW8ZN2vAw==", headers="(request-target) host date digest"',
'digest': 'sha-256=827Op2un8OT9KJuN1siRs5h6mxjrUh4LJag66dQjnIM=',
'date': 'Wed, 09 Jun 2021 16:08:15 GMT',
}
}
 
# In a real scenario, we would parse the signature param from
# the `keygen-signature` header. But for brevity...
response_sig = 'KhgcM+Ywv+DnQj4gE+DqWfNTM2TG5wfRuFQZ/zW48ValZuCHEu1h95Uyldqe7I85sS/QliCiRAF5QfW8ZN2vAw=='
response_body = response['body'].encode()
 
# Sign the response body using SHA-256
digest_bytes = hashlib.sha256(response_body).digest()
enc_digest = base64.b64encode(digest_bytes).decode()
 
# Reconstruct the signing data
signing_data = \
'(request-target): get /v1/accounts/keygen/licenses?limit=1\n' \
'host: api.keygen.sh\n' \
'date: ' + response['headers']['date'] + '\n' \
'digest: sha-256=' + enc_digest
 
# Verify the response signature
hex_verify_key = '799efc7752286e6c3815b13358d98fc0f0b566764458adcb48f1be2c10a55906'
verify_key = ed25519.VerifyingKey(hex_verify_key.encode(), encoding='hex')
try:
verify_key.verify(response_sig, signing_data.encode(), encoding='base64')
 
print('signature is good')
except ed25519.BadSignatureError:
print('signature is bad')

Changing the signing algorithm

We understand that not all programming languages have good support for our preferred signing algorithm, Ed25519. In these cases, you may provide a Keygen-Accept-Signature header to specify one of the following signing algorithms:

Description
ed25519 Sign using 128-bit Ed25519. This is the default signing algorithm.
rsa-pss-sha256 Sign using 2048-bit RSA PKCS1 with a SHA256 digest and PSS padding, a SHA256 MGF1 function and max salt length.
rsa-sha256 Sign using 2048-bit RSA PKCS1 with a SHA256 digest.

For example, to use rsa-sha256, you would provide the following header:

Keygen-Accept-Signature: algorithm="rsa-sha256"

This would sign the response using your account's 2048-bit RSA private key.

Webhook signatures

A signature header will always be included with webhook events delivered to your webhook endpoints. Use this to verify that the webhook is from us.

Code examples

To see an example of signature verification, check out our example on GitHub. It will show you how to utilize your account's public key to verify that the response was signed using your account's private key.

Other code examples for verifying response signatures:

Example response signature

Host: api.keygen.sh
Date: Wed, 09 Jun 2021 16:08:15 GMT
Digest: sha-256=827Op2un8OT9KJuN1siRs5h6mxjrUh4LJag66dQjnIM=
Keygen-Signature: keyid="bf9b523f-dd65-48a2-9512-fb66ba6c3714",
algorithm="ed25519",
signature="KhgcM+Ywv+DnQj4gE+DqWfNTM2TG5wfRuFQZ/zW48ValZuCHEu1h95Uyldqe7I85sS/QliCiRAF5QfW8ZN2vAw==",
headers="(request-target) host date digest"
{
"data": [
]
}