Skip to content

Signing requests

Every request an AI company’s system makes is signed with RFC 9421 HTTP Message Signatures by one of its retrieval keys. The signature replaces a bearer token entirely: there is no credential in the request to steal, and a captured request cannot be replayed. A business’s system signs its pushes the same way, with a different tag.

The TypeScript SDK signs for you (createRetrievalClient, createBusinessClient). In Python, use the file at the end of this page. Nobody should need to write this by hand; this page is for those who must.

Covered components "@method" "@authority" "@path", then "@query" when the request has a query string, then "content-digest" when it has a body
Parameters created, expires, nonce, keyid, tag, in that order
created, expires Unix seconds; expires at most 300 seconds after created
nonce fresh for every request: 32 random bytes, base64url
keyid the key’s id, its RFC 7638 thumbprint
tag mdb-retrieval for the retrieval API; mdb-push for publishing; mdb-business-read for a business reading its own data
Algorithm Ed25519 (or ES256 as raw r ‖ s); it comes from the registered key, never from the request
Label sig1

Content-Digest is sha-256=:<base64 of SHA-256 of the body>: (RFC 9530), over the exact bytes you send. @authority is the host you address, lower case, without a default port — which is what keeps a sandbox request from being accepted by production.

The signature base for a search looks like this (one line per component, then the parameters; no trailing newline):

"@method": POST
"@authority": sandbox.api.masterdb.ai
"@path": /v1/search
"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
"@signature-params": ("@method" "@authority" "@path" "content-digest");created=1790000000;expires=1790000300;nonce="kPq0e3Yc6sJ0mX8tq2f5b1c4d7e9a0b3c6d9e2f5a8b1c4d";keyid="L1BSyf0VsZoYxYTQE2NWgZhhPww06EQJ73k4cJoVnsI";tag="mdb-retrieval"

and the request carries:

Content-Digest: sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
Signature-Input: sig1=("@method" "@authority" "@path" "content-digest");created=1790000000;expires=1790000300;nonce="kPq0…";keyid="L1BS…";tag="mdb-retrieval"
Signature: sig1=:<base64 of the Ed25519 signature over the signature base>:

In this order, in the region that received the request, with nothing fetched from anywhere:

  1. Signature-Input and Signature are present. Otherwise signature_missing.
  2. Exactly one signature carries the tag this API accepts; its parameters are the ones above and nothing else; the nonce is 16 to 128 printable characters; the covered components include every one required; @authority is this deployment’s host. Otherwise signature_invalid.
  3. created is no more than 30 seconds in the future, expires has not passed, and expires is 1 to 300 seconds after created. Otherwise signature_expired. Keep your clock synchronised.
  4. The key is one the region holds, not revoked. Otherwise key_unknown.
  5. Content-Digest is recomputed over the body received. Otherwise digest_mismatch.
  6. The signature verifies over the signature base. Otherwise signature_invalid.
  7. The nonce has not been seen with this key. Otherwise nonce_reused. A nonce is claimed only once the signature has verified, and is held until the signature expires.

A request refused afterwards — for its body, its rate or its allowance — has used its nonce: sign again for a retry.

Each signed request is billed once: billing counts each (keyid, nonce) once.

Your signature is your receipt’s other half

Section titled “Your signature is your receipt’s other half”

Every receipt carries request_hash: the SHA-256 of your request’s signature base. Your signature is your statement that you asked; the receipt is MasterDB’s that it answered; each binds the other. Keep your signature bases if you want to reconcile to the request.

The Python samples on this site import this one file. It is httpx.Auth for RFC 9421, plus the two seals a business’s system makes (Pushes). It runs against the sandbox with every other sample.

samples/python/masterdb_signing.py
"""RFC 9421 request signing and Path A sealing for MasterDB, in Python.
The Python samples sign with this one file. It does exactly what the
TypeScript SDK does:
* ``MasterDBAuth``, an ``httpx.Auth`` that signs every request with the
components MasterDB requires — ``"@method" "@authority" "@path"``, ``"@query"``
when there is a query, ``"content-digest"`` (RFC 9530, sha-256) when there is
a body — and the parameters ``created``, ``expires`` (five minutes),
``nonce``, ``keyid`` (the key's RFC 7638 thumbprint) and ``tag``.
For the sandbox, ``sandbox_key_grant`` sends the key's grant as the
``MDB-Sandbox-Key`` header, as the TypeScript SDK's ``sandboxKeyGrant``
does.
* ``seal_batch`` and ``seal_action``, the DSSE seals of a Path A push and of a
sealed withdrawal or deletion.
Copy it into your project. Needs ``httpx``, ``cryptography`` and
``masterdb-verifier`` (for PAE, JCS and the Merkle tree).
"""
from __future__ import annotations
import base64
import hashlib
import json
import secrets
import time
from collections.abc import Callable, Generator, Sequence
from datetime import datetime, timezone
from typing import Any
import httpx
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
from masterdb_verifier import jcs_bytes, leaf_hash, merkle_root, pae
SANDBOX_API = "https://sandbox.api.masterdb.ai"
PRODUCTION_API = "https://api.masterdb.ai"
RETRIEVAL = "mdb-retrieval"
PUSH = "mdb-push"
BUSINESS_READ = "mdb-business-read"
SANDBOX_KEY_HEADER = "MDB-Sandbox-Key"
SANDBOX_KEY_GRANT_PREFIX = "mdb_sbxk1"
def _b64url(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b"=").decode("ascii")
def load_key(path: str) -> Ed25519PrivateKey:
"""Your private key from a PKCS #8 PEM file. It never leaves your process."""
with open(path, "rb") as f:
key = serialization.load_pem_private_key(f.read(), password=None)
if not isinstance(key, Ed25519PrivateKey):
raise TypeError("expected an Ed25519 private key")
return key
def key_id(key: Ed25519PrivateKey) -> str:
"""The key's id: the RFC 7638 thumbprint of its public JWK."""
x = key.public_key().public_bytes(serialization.Encoding.Raw, serialization.PublicFormat.Raw)
jwk = json.dumps({"crv": "Ed25519", "kty": "OKP", "x": _b64url(x)}, separators=(",", ":"), sort_keys=True)
return _b64url(hashlib.sha256(jwk.encode("ascii")).digest())
def business_tag(method: str) -> str:
"""A business's system: reading its own data is ``mdb-business-read``; publishing is ``mdb-push``."""
return BUSINESS_READ if method == "GET" else PUSH
class MasterDBAuth(httpx.Auth):
"""Signs each request with RFC 9421 (and RFC 9530 ``Content-Digest``).
``sandbox_key_grant`` is for the sandbox only: the ``sandbox_key_grant`` the portal answered when
the key was taken. It is sent as ``MDB-Sandbox-Key`` on every request, so the
sandbox admits the key on its first request instead of refusing it ``key_unknown``. It holds no secret and is
not part of the signature. Production ignores the header. Unset, the header is not sent.
"""
requires_request_body = True
def __init__(
self, key: Ed25519PrivateKey, tag: str | Callable[[str], str] = RETRIEVAL, *, sandbox_key_grant: str | None = None
) -> None:
if sandbox_key_grant is not None and not sandbox_key_grant.startswith(SANDBOX_KEY_GRANT_PREFIX + "."):
raise ValueError(
f"sandbox_key_grant is the sandbox_key_grant the portal answered ({SANDBOX_KEY_GRANT_PREFIX}.…), not a key or a bearer token"
)
self.sandbox_key_grant = sandbox_key_grant
self.key = key
self.key_id = key_id(key)
self.tag = tag
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
tag = self.tag(request.method) if callable(self.tag) else self.tag
if self.sandbox_key_grant is not None:
request.headers[SANDBOX_KEY_HEADER] = self.sandbox_key_grant
raw = request.url.raw_path.decode("ascii")
path, _, query = raw.partition("?")
authority = request.url.netloc.decode("ascii").lower()
default_port = ":443" if request.url.scheme == "https" else ":80"
if authority.endswith(default_port):
authority = authority[: -len(default_port)]
components = [("@method", request.method), ("@authority", authority), ("@path", path or "/")]
if query:
components.append(("@query", "?" + query))
body = request.content
if body:
digest = "sha-256=:" + base64.b64encode(hashlib.sha256(body).digest()).decode("ascii") + ":"
request.headers["Content-Digest"] = digest
components.append(("content-digest", digest))
created = int(time.time())
params = (
"(" + " ".join(f'"{name}"' for name, _ in components) + ")"
+ f";created={created};expires={created + 300}"
+ f';nonce="{_b64url(secrets.token_bytes(32))}";keyid="{self.key_id}";tag="{tag}"'
)
base = "\n".join(f'"{name}": {value}' for name, value in components) + f'\n"@signature-params": {params}'
signature = self.key.sign(base.encode("utf-8"))
request.headers["Signature-Input"] = f"sig1={params}"
request.headers["Signature"] = "sig1=:" + base64.b64encode(signature).decode("ascii") + ":"
yield request
def _envelope(payload_type: str, payload: dict[str, Any], key: Ed25519PrivateKey) -> dict[str, Any]:
body = jcs_bytes(payload)
sig = key.sign(pae(payload_type, body))
return {
"payloadType": payload_type,
"payload": base64.b64encode(body).decode("ascii"),
"signatures": [{"keyid": key_id(key), "sig": base64.b64encode(sig).decode("ascii")}],
}
def _now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def seal_batch(
records: Sequence[bytes], key: Ed25519PrivateKey, *, cert_id: str, ai_policy_version: int, seq: int, record_type: str = "products"
) -> dict[str, Any]:
"""The body of ``POST /v1/publish/{type}``: one seal over the RFC 6962 Merkle root of the records' exact bytes."""
if not 1 <= len(records) <= 1000:
raise ValueError("a batch holds 1 to 1,000 records")
root = merkle_root([leaf_hash(r) for r in records])
payload = {
"v": 2,
"key_id": key_id(key),
"cert_id": cert_id,
"root": "sha256:" + root.hex(),
"tree_size": len(records),
"seq": seq,
"sealed_at": _now(),
"record_type": record_type,
"ai_policy_version": ai_policy_version,
}
return {
"batch_seal": _envelope("application/vnd.masterdb.batch-seal.v2+json", payload, key),
"records": [base64.b64encode(r).decode("ascii") for r in records],
}
def seal_action(record_id: str, action: str, key: Ed25519PrivateKey, *, cert_id: str, ai_policy_version: int, record_type: str = "products") -> dict[str, Any]:
"""The body of ``POST /v1/publish/{type}/withdraw`` or ``/delete``: the sealed document ``{record_id, action, at}``."""
at = _now()
document = json.dumps({"record_id": record_id, "action": action, "at": at}, separators=(",", ":")).encode("utf-8")
payload = {
"v": 2,
"key_id": key_id(key),
"cert_id": cert_id,
"hash": "sha256:" + hashlib.sha256(document).hexdigest(),
"sealed_at": at,
"record_type": record_type,
"ai_policy_version": ai_policy_version,
}
return {"document_base64": base64.b64encode(document).decode("ascii"), "seal": _envelope("application/vnd.masterdb.seal.v2+json", payload, key)}