Skip to main content

Signature Verification

A signed HTTP message from dilisense carries an x-dilisense-signature header next to its unchanged JSON body. The signature lets you verify that the body originates from dilisense and was not altered on its way to you, for example by a proxy, and it lets you prove later, for example in an audit, what dilisense sent. Verification is optional, clients that ignore the header keep working unchanged.

The public keys needed for verification are served by the keys endpoint described in this chapter.

1. The x-dilisense-signature header

The header value consists of comma-separated parts:

x-dilisense-signature: v1,kid=<key id>,sig=<signature>
  • v1: the version of the signature scheme described in this chapter.
  • kid: the ID of the key that created the signature. Use it to fetch the public key from the keys endpoint.
  • sig: the signature, base64url-encoded without padding (RFC 4648 section 5).

Parse the header by splitting it at the commas and reading every part that contains an = as a key/value pair. Ignore parts you do not know, so that your implementation keeps working if further parts are added.


2. Algorithm and signed data

Signatures are created with ES256, i.e. ECDSA on the NIST P-256 curve (also known as secp256r1 or prime256v1) with SHA-256 as the hash function.

The signed data are the exact UTF-8 bytes of the HTTP body as transmitted.

The decoded sig value is an ASN.1 DER-encoded ECDSA signature (a SEQUENCE of the two integers r and s, 70 to 72 bytes long). This is the format that OpenSSL, Java (SHA256withECDSA), the Python cryptography package and the Node.js crypto module produce and verify by default.

warning

Verify the body bytes exactly as received. Do not parse and re-serialize the JSON before verifying, since a different key order or whitespace breaks the signature. If your HTTP client transparently decompresses a gzip-encoded body, the decompressed body is the signed data.


3. keys (GET)

The keys API method returns the public key for a key ID as a JSON Web Key.

Endpoint

https://api.dilisense.com/v1/keys/{kid}

Like all other API calls, the request has to be authenticated with the x-api-key header.

Path parameter

kid

The key ID taken from the x-dilisense-signature header, e.g. 6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f. Two aliases are available in addition:

  • current: the key that is currently used for signing.
  • previous: the key that was replaced by the last rotation.

For verifying a signature always use the kid of the header. The aliases are meant for monitoring and for fetching the keys ahead of time.

Response

The response provided back is formatted as JSON with the following attributes.

statusenumeration

  • ACTIVE: The key is in use for signing.
  • RETIRED: The key was replaced by a newer key in a scheduled rotation. Signatures created with it remain valid.
  • REVOKED: The key must not be trusted anymore.

not_beforestring

The start of the period in which the key is used for signing (ISO 8601 timestamp).

not_afterstring

The end of the period in which the key is used for signing (ISO 8601 timestamp).

keysobject array

Array with one JWK, the public key with the requested ID.

ktystring

The key type, always EC.

crvstring

The curve, always P-256.

algstring

The signature algorithm, always ES256.

kidstring

The key ID.

xstring

The x coordinate of the public key point, a base64url-encoded 32-byte value without padding.

ystring

The y coordinate of the public key point, a base64url-encoded 32-byte value without padding.

Next to the regular error codes the keys endpoint returns a 404 HTTP error code for an unknown key ID and a 400 HTTP error code for a key ID that contains characters other than letters, digits, ., _, : and -.

Key rotation

The signing key is rotated regularly, every key is used for signing for a limited period and is then retired. Keys stay retrievable by their ID after their retirement, so that you can verify a stored body at any later point in time. Follow these rules in your implementation:

  • Look up the key by the kid of the signature header, never rely on current for verification. The current key can change between the moment a body is signed and the moment you fetch the key.
  • Do not hardcode a key. Fetch a key the first time you see its ID and cache it by ID. The key material of a key document never changes, only its status does, so refresh cached keys from time to time to notice a revocation.
  • Accept the statuses ACTIVE and RETIRED, reject REVOKED.

Example

curl --location --request GET 'https://api.dilisense.com/v1/keys/6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f' \
--header 'x-api-key: <api_key>'

The response of the request above looks as follows:

{
"status": "ACTIVE",
"not_before": "2026-09-07T03:00:12.482Z",
"not_after": "2026-09-21T03:00:12.482Z",
"keys": [
{
"kty": "EC",
"crv": "P-256",
"alg": "ES256",
"kid": "6f1c2d3e-4a5b-4c6d-8e9f-0a1b2c3d4e5f",
"x": "bKTx6DOR16BCmP_2pCIzfjjIuegFlS6Y3wsWH12Wr8o",
"y": "stY6VDFgVicJQnpBn763CwilIJBbDoy4sLg7Hsq1vP0"
}
]
}

4. Verifying a signature

  1. Keep the raw body bytes as received and read the x-dilisense-signature header.
  2. Parse kid and sig from the header.
  3. Fetch the key document for the kid from the keys endpoint, unless you have it cached, and check that its status is not REVOKED.
  4. Build an EC public key on the P-256 curve from the x and y coordinates of the JWK.
  5. Base64url-decode sig and verify the DER-encoded ECDSA signature over the body bytes with SHA-256.

If the verification fails, treat the body as untrusted and do not process it. Please contact us at support@dilisense.com if a body from dilisense does not verify.

Example

The examples request the getSourceList endpoint and verify its signed response.

import base64
import requests
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec

headers = {
'x-api-key': '<api_key>'
}

def b64url_decode(value):
return base64.urlsafe_b64decode(value + "=" * (-len(value) % 4))

def parse_signature_header(header):
parts = dict(part.split("=", 1) for part in header.split(",") if "=" in part)
return parts["kid"], parts["sig"]

def public_key_for(kid):
key_doc = requests.get(f"https://api.dilisense.com/v1/keys/{kid}", headers=headers).json()
if key_doc["status"] == "REVOKED":
raise ValueError(f"signing key {kid} is revoked")
jwk = key_doc["keys"][0]
return ec.EllipticCurvePublicNumbers(
int.from_bytes(b64url_decode(jwk["x"]), "big"),
int.from_bytes(b64url_decode(jwk["y"]), "big"),
ec.SECP256R1(),
).public_key()

def verify(body, signature_header):
kid, sig = parse_signature_header(signature_header)
try:
public_key_for(kid).verify(b64url_decode(sig), body, ec.ECDSA(hashes.SHA256()))
return True
except InvalidSignature:
return False

response = requests.get("https://api.dilisense.com/v1/getSourceList", headers=headers)

# response.content are the raw bytes, do not verify a re-serialized response.json()
print(verify(response.content, response.headers["x-dilisense-signature"]))