humanproof format specification
This document is for anyone who wants to check a Hummand receipt without depending on Hummand, including by writing their own verifier. With it, the public key and the test vectors, an independent implementation checks signature, hash, chain and coherence and reaches the same results as the verifier on this site.
Anyone may implement this specification and use the vectors to test their implementation, without asking for permission.
- Files
- Test vectors (.zip)Receipts, batches and checkpoints signed with test keys, the invalid ones, canonicalization, geneses and indice.json with the expected result of each.
- humanproof/1 schemaJSON Schema, draft 2020-12.
- humanproof-acesso/1 schemaJSON Schema, draft 2020-12.
- SHA256SUMS.txtThe hashes of the three files above.
- ICP-Brasil roots (JSON)The roots this site's verifier trusts for the timestamp, downloaded from the ITI's official repository, with each one's SHA-256 to check against another source.
- Hummand's public key (JWKS)The key production receipts are checked with.
1. Conventions
The words MUST, MUST NOT, SHOULD and MAY have the meaning of RFC 2119. A conforming verifier is one that meets every MUST in this specification and gives, for each vector in the index (section 11), the result declared there.
The reference implementation is the humanproof-core library, in Rust, the same one that signs on Hummand’s server, runs in the humanproof-verify command-line verifier and, compiled to WebAssembly, in the public verifier at labs.hummand.com.br/verificar. Where this document and the reference implementation disagree, the disagreement is a defect to fix, and the vectors say which of the two is right.
2. Model
- Issuer: Hummand. Signs each object with a key whose public part is published.
- Tenant: Hummand’s customer. Each tenant has two chains of its own, the acts chain and the accesses chain, and no receipt of one tenant references a receipt of another.
- Act receipt (
humanproof/1): issued when an act ends, whatever the result. Positive, negative, expired, cancelled, technical failure and revocation are all signed. - Access receipt (
humanproof-acesso/1): issued when someone opens the evidence of an act or the shared dossier. A denied attempt is signed too. - Checkpoint (
humanproof-ponto/1and/2): a signed statement of how many receipts a chain had at an instant and which one was the last. It is what makes truncation of the end detectable. - Anchoring proof (
humanproof-ancoragem/1): a checkpoint or a receipt bound to a timestamp from a Time Stamping Authority (TSA, “ACT” in Brazil), a third party that is not Hummand. The checkpoint proof attests that the tenant chain already had those receipts at that instant; the receipt proof, that the receipt already existed, and it is checked with that receipt alone. - Verifier: any person or program that checks the objects. It needs no access to Hummand beyond the public key.
None of the formats carries biometrics, images, templates or the data subject’s data in the clear. What concerns the person goes in as a hash, and each format’s schema rejects unknown fields.
3. Canonicalization
Every hash and every signature in these formats is computed over the JCS canonical form (RFC 8785) of the object, with one restriction: numbers only as safe integers.
- Input MUST be I-JSON (RFC 7493): valid UTF-8, no duplicate member names in the same object. A verifier MUST reject text with a duplicate name, at any depth, before any other check.
- Numbers MUST be integers with |n| ≤ 2^53 − 1, written in the input without a fraction, without an exponent and without
-0. Input with a number outside this MUST be rejected, not normalized:1.0,1e3,1.5,-0and9007199254740992are rejected. - Output has no whitespace outside strings.
- Object members are sorted by name, compared as sequences of UTF-16 code units, not UTF-8 bytes. The two orders differ for characters outside the basic plane:
"😀"(U+1F600, units D83D DE00) comes before"~"(U+FF5E). - Arrays keep their order.
- Strings are written as
JSON.stringifywrites them:"and\escaped with a backslash;\b,\f,\n,\rand\tin short form; the other control characters below U+0020 as\ufollowed by four lowercase hex digits; everything else literal in UTF-8, including/, U+007F and U+2028. Escapes in the input are undone first. - Numbers in decimal, with no leading zeros;
true,falseandnullliteral.
The canonicalizacao.json vector file has each of these rules as an input and output pair, with the hash of the output, and the inputs that MUST be rejected with canonico: null.
4. Hash
hash(x) = "sha256:" + hex(SHA-256(UTF-8(x))), with 64 lowercase hex digits. Every hash field in these formats has this form, and a verifier MUST reject what does not.
- Hash of an object: the hash of the JCS of the whole object, signature included. It is what goes into the
prev_hashof the next object, which is why the chain also binds the signatures. - Acts genesis of a tenant:
hash("humanproof/1:genesis:" + tenant.id). - Accesses genesis of a tenant:
hash("humanproof-acesso/1:genesis:" + tenant.id).
In both geneses the hash is over the concatenated string, without JCS. The genesis.json file has the values for the test tenants.
5. Signature and keys
- Signed payload: the JCS of the object without the
assinaturamember.kidandalgstay inside the payload and are therefore covered by the signature. - Algorithms:
ES256(ECDSA over P-256 with SHA-256) andEdDSA(Ed25519). Hummand’s issuer signs withES256, with the private key in a managed key module it never leaves.EdDSAis part of the format and is exercised in the vectors. - Encoding: unpadded base64url (RFC 4648 §5) of the raw signature bytes. For
ES256,r || swith 32 bytes each, as in JWS (RFC 7518 §3.4), never DER. ForEdDSA, the 64 signature bytes. - Keys: a JWKS (RFC 7517). Each key has
kid,ktyandcrv(EC/P-256orOKP/Ed25519), the public coordinates andalg. Hummand’s issuer JWKS is athttps://gateway.hummand.com.br/.well-known/humanproof/keys.json. - Selection: the verifier MUST pick the key by the object’s
kidand MUST require the object’salgto match the key type. There is no negotiation: analgthat does not match the key is a signature failure, before any computation. So is akidabsent from the JWKS. - A JWKS can hold more than one key, and a chain can cross a change of key and of algorithm: the chain is by hash and does not depend on the key.
6. The act receipt humanproof/1
The exact structure is in the JSON Schema humanproof-1.schema.json (draft 2020-12), which is part of this specification and rejects unknown fields. Field names are in Portuguese, as in the format. The required members:
| Member | Content |
|---|---|
formato |
"humanproof/1" |
recibo_id, ato_id |
opaque identifiers |
tenant |
{ id, nome_publico } |
tipo |
act type, such as presenca.recorrente or mandato.pessoa |
nivel |
integer from 0 to 3: the level actually reached, not the one requested |
resultado |
positivo, negativo, expirado, cancelado, falhou or revogado |
motivo_codigo |
short reason code, or null |
sujeito |
ref_cliente_hash (HMAC-SHA256 with a tenant secret over the reference the customer gave the person, in hash form, or null) and, when there was one, referencia with the hash of the compared reference |
contexto |
hash of the JCS of the context the customer sent and chaves_declaradas, the key names; never the content |
consentimentos |
per layer: kind, versao, texto_hash and ts |
etapas |
nome, resultado (ok or the short failure reason) and ts |
fontes |
source, version and, when there is one, the third-party component used |
garantias |
humano_vivo, mesma_pessoa_da_referencia, identidade_civil, consentiu (true, false or null when the step did not happen) and biometria_retida, always false |
mandato_ref, encadeamento, revisado_por |
mandate reference, position in a group of chained acts and human review mark, each null when not applicable |
politica_versao |
the policy applied |
ts_criado, ts_concluido |
RFC 3339 in UTC, with the Z suffix |
prev_hash |
hash of the previous receipt of the same tenant, or the acts genesis |
kid, alg, assinatura |
section 5 |
Optional members, by act type: backfill; escopo and mandatario in a mandate grant; verificacao_mandato in a mandate use; documento_anexo; assinatura_juridica; recibos_individuais and humanos_distintos in composite acts; gatilho.
Three reading notes:
contexto.hashonly checks against whoever has the context. Whoever has the original context recomputeshash(JCS(contexto))alone; the verifier checks only the form of the hash. An act without context uses the hash of{}. Receipts issued before 2026-09-13 whose context had a non-ASCII character hold the hash of an earlier recipe, which escaped those characters instead of writing them in UTF-8; for ASCII-only context both recipes give the same value.etapas[].tsis the instant the step ended, when the issuer measured it. When it did not, the issuer puts the act’sts_concluidothere, and the receipt remains valid. Nothing in the receipt tells the two cases apart.inconclusivois never a result. An act that goes through human review comes out aspositivoornegativo, withrevisado_porfilled in.
7. Coherence of humanproof/1
Beyond schema and signature, the verifier MUST apply the rules below. They ensure the receipt does not contradict itself, not that what it states happened outside it.
resultadofrom the vocabulary;nivelbetween 0 and 3; non-emptykid; every hash in the form of section 4;garantias.biometria_retidafalse.positivorequires every stepok.positivoat level 2 or above, withoutmandato_ref, requires apresencastep withok,garantias.humano_vivotrue and, when there is asujeito.referencia,garantias.mesma_pessoa_da_referenciatrue.positivoat level 3 requiresgarantias.identidade_civiltrue.revisado_poronly inpositivoornegativo.revogadorequiresmandato_ref, the revoked mandate.- Times in RFC 3339;
ts_concluidodoes not precedets_criado; no step afterts_concluido; steps in non-decreasingtsorder. A step may precedets_criado, because consent is collected before the act is born. - Mandate grant.
escopoandmandatariogo together.escopo.acoesnot empty;escopo.revogavelpresent;escopo.validade_atein RFC 3339 and, in the grant (withoutmandato_ref), later thants_criado: a mandate born expired authorizes nothing.mandatario.tipoispessoaoragente, andmandatario.ref_hashis a hash. - Mandate use.
verificacao_mandatorequiresmandato_refand the four statementsvalido,dentro_escopo,dentro_validadeandrevogado. They authorize when the first three are true and the last is false, and the receipt ispositivoexactly when they authorize.motivo_codigomust be supported by them:mandato_invalidorequiresvalidofalse;mandato_revogado,revogadotrue;mandato_vencido,dentro_validadefalse;fora_de_escopo,dentro_escopofalse. Theverificacao_mandatostep isokexactly when the four authorize.escopoandverificacao_mandatonever appear in the same receipt.
The verifier does not check the referenced mandate against the grant receipt: whoever wants the whole proof verifies both receipts, which are in the same tenant chain.
8. The access receipt humanproof-acesso/1
Same canonicalization, hash, signature and keys as sections 3 to 5. Schema in humanproof-acesso-1.schema.json, also closed.
| Member | Content |
|---|---|
formato |
"humanproof-acesso/1" |
acesso_id, ato_id |
the access and the act whose evidence was opened |
tenant |
{ id, nome_publico } |
ator |
{ papel, id }, with an opaque id, never a name or e-mail |
motivo_hash |
hash of the JCS of the reason string, that is, of the quoted text; the text does not go in |
componentes |
one per record touched: registro, registro_id and desfecho (aberto, apagado, nunca_houve, negado or erro) |
desfecho |
of the access: concedido, negado or erro |
ts_acesso |
RFC 3339 in UTC, with Z |
prev_hash |
hash of the tenant’s previous access, or the accesses genesis |
kid, alg, assinatura |
section 5 |
Roles and ator.id prefixes:
papel |
prefix | who |
|---|---|---|
cliente |
usuario: |
user of the tenant’s dashboard |
operador_hummand |
operador: |
Hummand operations |
revisor_hummand |
revisor: |
Hummand human review |
integracao |
apikey: |
the tenant’s integration |
terceiro |
compartilhamento: |
whoever opened, without an account, a dossier the customer shared by link; the id is the share’s, never the person’s |
Coherence the verifier MUST apply:
concedidorequires at least one component and nonenegado.negadorequires every componentnegado; the empty list is accepted, because the refusal can come before any record is touched.erroaccepts any mix.- Known
ator.papelandator.idwith the role’s prefix, followed by at least one character. ts_acessoin RFC 3339 withZ;prev_hashandmotivo_hashin hash form; non-emptykid.
The access receipt covers opening through the official path, which is the only one the issuer sees.
9. The chain
Each tenant has an acts chain and an accesses chain, independent, each with its own genesis. A batch is verified like this:
- Group the objects by
(formato, tenant.id). Each group is a chain. A verifier that receives a file as a single chain MUST reject one that mixes tenants or formats. - Compute each object’s hash and link each one to the object whose hash is its
prev_hash. Input order does not matter. - The chain is continuous when there is exactly one object whose
prev_hashis not the hash of another object in the batch, and each object has at most one successor. - Read the start: if that first object points to the genesis of the format and tenant, the chain is verified from genesis; if it points to another hash, it is continuous within the batch, of external origin, and MUST be presented as such, never as complete.
- Problems the verifier MUST report: more than one start (a gap in the middle or an object from another history), two objects with the same
prev_hash(fork), cycle and repeated object.
The chain does not promise time order: an expired act concludes at its expiry date, which can be later than that of acts issued after it.
What the chain does not catch. Removing the last objects leaves a chain continuous from genesis, and nothing inside the batch says how many there should be. That is what the checkpoint is for.
10. The checkpoint
humanproof-ponto/1, acts chain only:
{
"formato": "humanproof-ponto/1",
"tenant": { "id": "..." },
"total": 128,
"ultimo_hash": "sha256:...",
"ts": "2026-09-13T21:00:00Z",
"kid": "...", "alg": "ES256", "assinatura": "..."
}
humanproof-ponto/2, both chains, both required:
{
"formato": "humanproof-ponto/2",
"tenant": { "id": "..." },
"cadeias": {
"atos": { "total": 128, "ultimo_hash": "sha256:..." },
"acessos": { "total": 7, "ultimo_hash": "sha256:..." }
},
"ts": "2026-09-24T21:00:00Z",
"kid": "...", "alg": "ES256", "assinatura": "..."
}
total is how many objects the chain had up to ultimo_hash, inclusive. An empty chain has total 0 and ultimo_hash equal to the genesis of that chain. Signature as in section 5. /1 remains accepted and states nothing about accesses: checking accesses against it MUST be refused, never approved by omission.
Checking a batch against a checkpoint, per chain:
| Situation | Reading |
|---|---|
| the checkpoint does not verify (signature) or belongs to another tenant | contradicts |
total 0 and ultimo_hash equal to the genesis |
matches: the checkpoint states there was nothing yet |
the batch contains ultimo_hash and, if it starts at genesis, at position total |
matches |
the batch starts at genesis and contains ultimo_hash at another position |
contradicts |
the batch starts at genesis and does not contain ultimo_hash |
contradicts: truncated, forked or rewritten, or earlier than the checkpoint |
the batch does not start at genesis and does not contain ultimo_hash |
inconclusive: it may be a window earlier than the checkpoint |
The verifier SHOULD show both counts, the batch’s and the checkpoint’s, not just the reading.
A checkpoint is worth the distance in time. A checkpoint requested together with the batch comes from the same version of history and proves nothing; what proves is the checkpoint the auditor kept before and compares later.
11. Full procedure and vectors
For each object, in this order, reporting each step separately:
- Read as I-JSON, rejecting duplicate names, and identify
formato. An unknown format MUST be rejected. - Validate against the format’s schema.
- Select the key by
kidand checkalgagainst it (section 5). - Check the signature over the payload.
- Apply the format’s coherence rules (sections 7 and 8).
An object is valid when it passes all five. For a batch, after that, verify each chain (section 9) and, if there is a kept checkpoint, check each chain against it (section 10).
The vectors come with this document. They were signed with test keys derived from public labels, worthless outside tests: the P-256 scalar is the SHA-256 of the string humanproof/1:chave-de-teste:p256 (the second one, humanproof/1:chave-de-teste:p256:2), the Ed25519 seed is the SHA-256 of humanproof/1:chave-de-teste:ed25519, and the signatures are deterministic (RFC 6979). The public key files are jwks.json, jwks-rotacao.json and jwks-eddsa.json.
indice.jsonlists each vector with the operation to apply and the expected result.recibo: the object alone.cadeia: the array must form one chain of one format and one tenant.lote: the array grouped by format and tenant.ponto: the array checked against the checkpoint given.ancoragem: the proof checked with the JWKS and the trusted root given inraizes.esperado.falhassays at which steps verification fails (leitura, reading, when the text is not I-JSON;schema,assinatura,coerencia,cadeia,ponto; in anchoring, the checkpointassinatura,caminhoandcarimbo); empty means a valid vector.esperado.inicioisgenesisorexterno;esperado.pontoisconfere(matches),inconclusivoorcontradiz(contradicts).canonicalizacao.json: the input and output pairs of section 3.genesis.json: the geneses of the test tenants and a samplemotivo_hash.ancoragem/: the valid proofs, timestamps from a test TSA (RSA with SHA-512 and ECDSA P-256, issued by OpenSSL), a full TSA response and the DER certificates of the roots, the test one and a foreign one.
A conforming implementation gives, for each index entry, exactly the failures, start and reading declared. The same vector can appear with different operations: the batch truncated at the end is a valid chain from genesis and contradicts the checkpoint, and that is what shows what the checkpoint is for.
12. Versions
A format change is a new format (humanproof/2, for example); a published format is not edited. This specification can gain additive values within a format, such as the terceiro role added on 2026-10-19, and the document records each one with its date. In version 1.1 of the document, from the same day as 1.0, rejecting duplicate names went from SHOULD to MUST, with the invalidos/nome-duplicado.json vector. Version 1.2, also from 2026-10-19, adds the humanproof-ancoragem/1 format (section 15). Version 1.3, from 2026-10-20, adds to it the receipt proof (recibo_hash), before the format had any proof issued in production. An older verifier rejects a value it does not know, because the schemas are closed; it never accepts it by omission.
13. What the formats do not prove
- The receipt states what the declared sources support, and nothing beyond. Liveness is the verdict of a third-party component, identified in the receipt, that decides by a probabilistic criterion; the receipt records the verdict and the version, not a score.
- The receipt does not prove the legitimacy of the reference the customer sent: a good match against the wrong reference is high confidence in the wrong person.
- The chain and the checkpoint cover what was issued. An act that never became a receipt appears in no chain.
- The access receipt covers opening through the official path. Access outside it produces no receipt.
- Coherence is the absence of internal contradiction, not external confirmation.
- The timestamp attests when the checkpoint or the receipt existed, not that what the receipts state happened. Receipts issued between one anchoring and the next remain unanchored until it.
14. Security
- Trust in the key. A signature only proves that whoever had the private key signed. The verifier MUST obtain the JWKS from the issuer’s address or keep it beforehand, and MUST NOT trust a key that came in the same package as the receipts: whoever forges a package also forges the key inside it. The package’s key is only for comparison.
- Duplicate names. Two parsers that resolve duplicate names differently (first wins, last wins) would read the same text as different receipts. That is why input MUST be I-JSON, and a verifier MUST reject text with a duplicate name instead of picking one of the values. The
invalidos/nome-duplicado.jsonvector is a genuine negative receipt with a secondresultado, positive, at the start of the object: whoever keeps the last value verifies the signature, and whoever keeps the first reads a positive. - Privacy.
ref_cliente_hashis an HMAC with a tenant secret, because the plain hash of an identity document number is reversed by enumeration.motivo_hash,contexto.hashandtexto_hashcan only be checked by whoever has the original text. - Checkpoint. See section 10: only a checkpoint kept before the check catches truncation of the end, or an anchored checkpoint (section 15).
- Anchoring roots. A timestamp is worth what the root the verifier trusts is worth. The roots MUST come from the official source (for ICP-Brasil, the ITI), never from the same package that carries the proof.
15. The anchoring proof humanproof-ancoragem/1
Periodically the issuer builds a Merkle tree with two kinds of leaf and asks a TSA for an RFC 3161 timestamp over the root:
- the signed
humanproof-ponto/2of each tenant with new receipts, which protects the whole chain against truncation of the end; - each receipt issued since the previous anchoring, act or access, by its hash (section 4), which proves that receipt existed to whoever holds only it.
Each leaf has its own proof. The checkpoint one:
{
"formato": "humanproof-ancoragem/1",
"ponto": { "formato": "humanproof-ponto/2", "...": "the tenant's signed checkpoint" },
"caminho": [
{ "lado": "direita", "hash": "sha256:..." },
{ "lado": "direita", "hash": "sha256:..." }
],
"raiz": "sha256:...",
"carimbo": "<RFC 3161 token, DER, base64 with padding>"
}
The receipt one replaces ponto with recibo_hash, the receipt hash:
{
"formato": "humanproof-ancoragem/1",
"recibo_hash": "sha256:...",
"caminho": [ { "lado": "esquerda", "hash": "sha256:..." } ],
"raiz": "sha256:...",
"carimbo": "<RFC 3161 token, DER, base64 with padding>"
}
formato, caminho, raiz and carimbo are required, with exactly one of ponto and recibo_hash; no other member is accepted, and in the caminho items only lado and hash. The proof carries no Hummand signature: the checkpoint inside it is signed, the receipt is signed and in the hands of whoever checks, and the timestamp is signed by the TSA. The receipt proof carries nothing of the receipt beyond its hash: no tenant, no act.
The tree follows RFC 6962 §2.1, over the 32 bytes of each hash:
- checkpoint leaf:
SHA-256(0x00 || JCS(ponto)), with the checkpoint signature; - receipt leaf:
SHA-256(0x00 || "humanproof-ancoragem/1:recibo:" || recibo_hash), with the hash in thesha256:<hex>form, in UTF-8. The checkpoint leaf preimage starts with{and the receipt one withh: one never passes for the other; - node:
SHA-256(0x01 || left || right); - with n leaves, n ≥ 2, the root is the node of the roots of the first k and of the remaining n − k, k being the largest power of two less than n; with one leaf, the root is the leaf.
caminho lists the leaf’s siblings up to the root, lowest first; lado says which side the sibling is on (esquerda left, direita right). Climbing is: start at the leaf and, for each item, make the node with the sibling on the left or on the right. The empty path is the one-leaf tree.
The timestamp is a TimeStampToken (RFC 3161 §2.4.2): a CMS ContentInfo SignedData (RFC 5652) whose content is a TSTInfo. The messageImprint uses SHA-256, and the hashedMessage is the 32 bytes of the root.
Checking a proof, reporting each step:
- Form: the members above,
raiz,recibo_hashand eachhashin the form of section 4,carimboin base64. - Leaf: in the checkpoint proof, the checkpoint verifies with Hummand’s JWKS, as in section 10. In the receipt proof, whoever checks holds the receipt and verifies that its hash (section 4) is
recibo_hash; the proof holds for that receipt and no other. - Path: the leaf, climbing the path, gives exactly
raiz. - Timestamp, over the bytes of
raiz:- the
TSTInfostamps those 32 bytes, with SHA-256; - there is a single
SignerInfo, and its signed attributes havecontent-typeequal toid-ct-TSTInfoandmessage-digestequal to the digest of theTSTInfo, with theSignerInfodigestAlgorithm(SHA-256, SHA-384 or SHA-512); - the signature, over the DER encoding of the signed attributes with the SET tag, verifies with the key of the TSA certificate, which comes in the timestamp and is identified by the
sid. Algorithms: RSA PKCS #1 v1.5 with SHA-256, SHA-384 or SHA-512, and ECDSA P-256 with SHA-256; - the TSA certificate has the
timeStampingextended key usage; - the chain climbs from the TSA certificate to one of the verifier’s trusted roots, each certificate signed by the one above and valid at the
genTimeinstant, and every intermediate issuer with CAbasicConstraints.
- the
- In the checkpoint proof, the checkpoint
tsis not later thangenTime.
The proof is valid when it passes all of them. The verifier SHOULD show the genTime, the policy, the TSA and the root the chain reached.
What each one proves: the receipt proof, that the receipt existed at genTime, which is enough for whoever receives a dossier with it, without seeing any other receipt of the tenant. The checkpoint proof, that the tenant chain had total receipts ending in ultimo_hash at that instant; the tenant batch is checked against that checkpoint as against a kept checkpoint (section 10), and the auditor need not have kept a checkpoint beforehand.
What offline checking does not do, and the verifier MUST say so when relevant: consult the TSA revocation list, which needs the network; check the timestamp signing-certificate attribute (the signature and the chain already bind the certificate); accept RSASSA-PSS.