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.
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
kidof the signature header, never rely oncurrentfor 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
statusdoes, 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
- Keep the raw body bytes as received and read the
x-dilisense-signatureheader. - Parse
kidandsigfrom the header. - Fetch the key document for the
kidfrom the keys endpoint, unless you have it cached, and check that itsstatusis not REVOKED. - Build an EC public key on the P-256 curve from the
xandycoordinates of the JWK. - Base64url-decode
sigand 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.
- Python
- Java
- Javascript
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"]))
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.math.BigInteger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.AlgorithmParameters;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class SignatureVerification {
static final String API_KEY = "<api_key>";
static final HttpClient client = HttpClient.newHttpClient();
static final ObjectMapper mapper = new ObjectMapper();
static Map<String, String> parseSignatureHeader(String header) {
var parts = new HashMap<String, String>();
for (var part : header.split(",")) {
int eq = part.indexOf('=');
if (eq > 0) {
parts.put(part.substring(0, eq), part.substring(eq + 1));
}
}
return parts;
}
static PublicKey publicKeyFor(String kid) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.dilisense.com/v1/keys/" + kid))
.header("x-api-key", API_KEY)
.build();
JsonNode keyDoc = mapper.readTree(client.send(request, HttpResponse.BodyHandlers.ofString()).body());
if ("REVOKED".equals(keyDoc.get("status").asText())) {
throw new IllegalStateException("signing key " + kid + " is revoked");
}
JsonNode jwk = keyDoc.get("keys").get(0);
byte[] x = Base64.getUrlDecoder().decode(jwk.get("x").asText());
byte[] y = Base64.getUrlDecoder().decode(jwk.get("y").asText());
AlgorithmParameters params = AlgorithmParameters.getInstance("EC");
params.init(new ECGenParameterSpec("secp256r1"));
ECParameterSpec spec = params.getParameterSpec(ECParameterSpec.class);
return KeyFactory.getInstance("EC")
.generatePublic(new ECPublicKeySpec(new ECPoint(new BigInteger(1, x), new BigInteger(1, y)), spec));
}
static boolean verify(byte[] body, String signatureHeader) throws Exception {
var parts = parseSignatureHeader(signatureHeader);
Signature verifier = Signature.getInstance("SHA256withECDSA");
verifier.initVerify(publicKeyFor(parts.get("kid")));
verifier.update(body);
return verifier.verify(Base64.getUrlDecoder().decode(parts.get("sig")));
}
public static void main(String[] args) throws Exception {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.dilisense.com/v1/getSourceList"))
.header("x-api-key", API_KEY)
.build();
// Read the body as bytes, do not verify a re-serialized version of it
HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
String signatureHeader = response.headers().firstValue("x-dilisense-signature").orElseThrow();
System.out.println(verify(response.body(), signatureHeader));
}
}
import crypto from "node:crypto";
const headers = { "x-api-key": "<api_key>" };
function parseSignatureHeader(header) {
const parts = {};
for (const part of header.split(",")) {
const eq = part.indexOf("=");
if (eq > 0) {
parts[part.slice(0, eq)] = part.slice(eq + 1);
}
}
return parts;
}
async function publicKeyFor(kid) {
const response = await fetch(`https://api.dilisense.com/v1/keys/${kid}`, { headers });
const keyDoc = await response.json();
if (keyDoc.status === "REVOKED") {
throw new Error(`signing key ${kid} is revoked`);
}
return crypto.createPublicKey({ key: keyDoc.keys[0], format: "jwk" });
}
async function verify(body, signatureHeader) {
const { kid, sig } = parseSignatureHeader(signatureHeader);
const publicKey = await publicKeyFor(kid);
return crypto.verify("sha256", body, { key: publicKey, dsaEncoding: "der" }, Buffer.from(sig, "base64url"));
}
const response = await fetch("https://api.dilisense.com/v1/getSourceList", { headers });
// Read the body as bytes, do not verify a re-serialized response.json()
const body = Buffer.from(await response.arrayBuffer());
console.log(await verify(body, response.headers.get("x-dilisense-signature")));