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.
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. Public keys
Fetch the public key for the kid of the header from the keys endpoint. It returns the key as a JSON Web Key, together with its status. The signing key is rotated regularly, so look up the key by the kid of every signature, cache it by its ID and reject keys with the status REVOKED; the keys endpoint describes the key rotation in detail.
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")));