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.
What is signed
Section titled “What is signed”| 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>:What MasterDB checks
Section titled “What MasterDB checks”In this order, in the region that received the request, with nothing fetched from anywhere:
Signature-InputandSignatureare present. Otherwisesignature_missing.- 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;
@authorityis this deployment’s host. Otherwisesignature_invalid. createdis no more than 30 seconds in the future,expireshas not passed, andexpiresis 1 to 300 seconds aftercreated. Otherwisesignature_expired. Keep your clock synchronised.- The key is one the region holds, not revoked. Otherwise
key_unknown. Content-Digestis recomputed over the body received. Otherwisedigest_mismatch.- The signature verifies over the signature base. Otherwise
signature_invalid. - 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.
Python: the signing helper
Section titled “Python: the signing helper”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.
"""RFC 9421 request signing and Path A sealing for MasterDB, in Python.
The Python samples sign with this one file. It does exactly what theTypeScript 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 base64import hashlibimport jsonimport secretsimport timefrom collections.abc import Callable, Generator, Sequencefrom datetime import datetime, timezonefrom typing import Any
import httpxfrom cryptography.hazmat.primitives import serializationfrom cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKeyfrom 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)}