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.

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. 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​

  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"]))