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.
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 importRSA
from Crypto.Signature importPKCS1_v1_5
from Crypto.Hash importSHA256
import base64
# This should be replaced with your Keygen account's public key (note: all newlines and whitespace must be *exact*)
Response signature verification is useful for a variety of scenarios where verifying that
a response came from Keygen's servers is vital, such as:
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.
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.
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 synchronized using NTP).
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.
Verifying the authenticity of webhook events sent to your endpoints. Use request
signatures to check if a webhook event was sent from us before processing. 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.
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 our API, this will 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 requests sent to a custom domain, this will be the
custom domain:
POST https://licensing.example.com/v1/licenses/actions/validate-key
...
Host: licensing.example.com
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.
When generating a digest for the response body, please hash the raw response body string
(or bytes), before deserializing the JSON payload for later use. Deserializing and then
reserializing the response body may introduce subtle issues such as key sort order, encoding,
and unicode escaping. A change in any of these could cause your signature verification to
unexpectedly fail.
For example, with JavaScript, that would mean using await response.text(), and then later
parsing the JSON manually using JSON.parse(), instead of using await response.json().
You would use the raw text() body for signature verification.
The full Digest header from us will look something like:
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
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.
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.
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: