Skip to content

Key custody

A seal proves that a key signed a record. Key custody says whose hand was on that key. Every key on a business’s register has a statement, signed by MasterDB, saying one of two things:

Custody Who holds the private half What a seal by the key proves
hosted MasterDB, for the business. This is the key behind standard publishing: a person of the business signs in with an email link and confirms each publish with a one-time code sent to their verified address. MasterDB signed, on a request a person with a grant on the business confirmed. Each use of the key is in the public transparency log, with a hash of the session that used it.
self The business: a person’s passkey, or an integration key held by the business’s own system. A key the business holds signed it.

A key with no statement is reported unstated. A verifier never assumes self.

GET /v1/certificates/{uuid}/key-custody on verify.masterdb.ai (and api.masterdb.ai), beside the ADL Certificate and the business’s public keys (GET /v1/certificates/{uuid}/keys). It is public, needs no account, and lists every statement ever issued for the business, newest first:

{
"uuid": "0b7d6c3e-2f4a-4c1b-9d8e-7f6a5b4c3d2e",
"statements": [
{
"key_id": "7Hs2pQ9vLm4xZk1Nt8Rc5Yw3Bf6Ju0Ea2Di7Go9Ks1M",
"custody": "hosted",
"effective_from": "2026-10-03T09:00:00.000Z",
"issued_at": "2026-10-03T09:00:00.000Z",
"statement_id": "sha256:…",
"statement": { "payloadType": "application/vnd.masterdb.key-custody.v1+json", "payload": "…", "signatures": ["…", "…"] }
}
]
}

Only statement counts. The other members repeat what it says, so a person can read the answer without decoding it.

statement is a DSSE envelope of type application/vnd.masterdb.key-custody.v1+json over this payload, in RFC 8785 form:

Member Meaning
v 1, the format. Any other value is refused (version_unknown).
business_uuid The business the key belongs to.
key_id The key’s RFC 7638 thumbprint, as the seal and the published keys name it.
custody hosted or self. Anything else is refused (payload_malformed).
issued_at When MasterDB issued the statement.
effective_from From when it holds. A first statement takes effect from the key’s own start; it is never later than issued_at.
sandbox true on a sandbox statement only, trusted only under the sandbox anchors.

No other member is allowed. The statement names no person.

It is signed the way the ADL Certificate is signed: by MasterDB’s issuance key, with both halves of it, the P-256 signature and the ML-DSA-65 signature, each by an issuance key valid at issued_at in the signed key set. Both are required. A statement carrying only one is refused with post_quantum_required. The issuance key, not a working key, signs custody because custody changes what a seal proves, so it carries the certificate’s own trust.

  • A hosted key: when MasterDB issues it, before it is ever used. No hosted seal exists without a hosted statement before it.
  • Every other key: when the certificate is issued or re-issued, and when the key first seals, if it has no statement yet.
  • A change: if a key’s custody changes, a new statement is issued with a new issued_at and effective_from. The old one is never edited and stays listed. The statement in force at an instant is the one with the latest effective_from at or before it; a tie goes to the later issuance.

Every issuance is a key_event leaf in the transparency log, over the SHA-256 of the envelope.

TypeScript / Python What it does
verifyKeyCustody / verify_key_custody One statement: the payload type, the format and its exact members, both issuance signatures, and sandbox against the anchors. With expectUuid / expect_uuid, a statement about another business is refused (key_unknown).
verifyKeyCustodyStatements / verify_key_custody_statements The whole answer of the route (or a list of envelopes): every statement checked, all about one business.
keyCustodyInForce / key_custody_in_force The statement in force for a key at an instant.
verifySeal / verify_seal with keyCustody / key_custody The seal as usual, and custody in the result: hosted, self or unstated for the seal’s key at sealed_at.
import { productionKeySet, verifySeal } from '@masterdb/verifier';
const keySet = await productionKeySet();
const base = `https://verify.masterdb.ai/v1/certificates/${uuid}`;
const [certificate, keys, keyCustody] = await Promise.all([base, `${base}/keys`, `${base}/key-custody`].map(async (u) => (await fetch(u)).json()));
const result = verifySeal(recordBytes, seal, { publishedKeys: keys, keySet, certificates: [certificate.certificate], keyCustody });
console.log(result.custody); // 'hosted', 'self' or 'unstated'
from masterdb_verifier import SealContext, production_key_set, verify_seal
result = verify_seal(record_bytes, seal, SealContext(published_keys=keys, key_set=production_key_set(), certificates=[certificate["certificate"]], key_custody=key_custody))
print(result.custody) # "hosted", "self" or "unstated"

The cases both libraries are held to, accepted and refused, are key-custody.json in the test vectors.

Version 1 of the ADL Certificate admits no member it does not name, and the verifier libraries refuse one they do not know, so custody is published beside the certificate rather than inside it.