Contents

PACT 2 · currentrevised 2026-09-30

The MUSTs, indexed

Every normative sentence of the current text, one row each, so that an implementation, a review or a test can name the rule it holds.

88 sentences of the current text carry MUST, MUST NOT or REQUIRED in the obligatory sense of RFC 2119 — 106 keywords in all. They are listed here in document order, under the section each sits in, with the id the record of § 12 lists it under: the section number and a count within the section. The sentence links to its section of the text.

§ 2Identity, certificates and mTLS

IdThe sentence
2.#1When both proofs are present their leaf keys MUST match, else envelope_invalid.

§ 2.1Deriving the root from a passkey

IdThe sentence
2.1#1A wallet that can use a WebAuthn credential MUST derive the root's private key from one rather than generate and store it; a wallet that cannot — a command-line tool — generates the key and keeps it as §9 says.
2.1#2A wallet MUST use exactly these values.
2.1#3A wallet MUST NOT present this as a guarantee: whether a given provider carries the PRF secret across its own sync is that provider's property and not the protocol's, and a wallet that has not verified it SHOULD say so rather than imply otherwise.
2.1#4A wallet holding more than one credential for its origin MUST name the intended credential when it knows which one that is, and MUST prove the derived root before signing (§2.2).
2.1#5A wallet that derives its root MUST still be able to export it (§9).

§ 2.2Proving a root before using it

IdThe sentence
2.2#1Before issuing any certificate, a wallet MUST establish that the root it is about to sign with is the root the identity already has.
2.2#2A wallet MUST refuse to sign unless all three hold:
2.2#3The challenge MUST be domain-separated from certificate bytes — the ASCII PACT root proof v1 followed by a newline and at least 32 random bytes — so that proving possession can never be made to sign a certificate.
2.2#4A wallet MUST validate a chain it has assembled (§14.2) against the expected root and endpoint before returning it.
2.2#5A root certificate is issued once. A wallet MUST NOT rebuild a root certificate for an identity that already has one.

§ 3Contact cards (vCard)

IdThe sentence
3.#1A receiving implementation MUST NOT treat FN as identifying, and SHOULD NOT present it as a contact's whole identity: where two pinned contacts render alike, show the fingerprint alongside.
3.#2A receiver MUST reject a card without X-PACT-CERT, one whose certificate does not parse as §14.1 describes — no issuer key identifier or one that is not the 32 bytes a key identifier is, no endpoint or several, a validity longer than 398 days — and a card whose X-PACT-VERSION names a major version it does not implement, each with bad_request.
3.#3A receiver MUST also refuse, at intake and again before every dial, an endpoint whose host resolves to a loopback, link-local or private address — the resolve-and-vet guard §6.2 applies to media URLs — unless the owner has configured that network on purpose, and a guest's endpoint that names the receiver's own address, which no honest card carries.
3.#4A writer MUST NOT put a control character into a card — in FN, in X-PACT-SEAL, or in a property it adds: a card is lines, a line break writes a property of the writer's choosing, and a reader takes the first of a name, so a name of x, a line break and X-PACT-SEAL:none made a card that requires sealing into one that does not.

§ 4Invites

IdThe sentence
4.#1The same URL serves two audiences by content negotiation: a browser gets the human landing page; a client sending Accept: application/pact-invite+json (or appending ?format=json) gets {"card","card_sig","chain"} — the signed card and the issuer's chain (§2), whose leaf MUST byte-equal the card's X-PACT-CERT and which the redeemer MUST validate (§14.2) before use, so it can seal its very first call.

§ 5.2Manual flow (vCard shared over existing channels)

IdThe sentence
5.2#1"pending"}` any stranger gets, while nothing is recorded and the owner is never bothered: blocked MUST be indistinguishable from never-met (§12).

§ 6.1Tiers

IdThe sentence
6.1#1A caller at the pending tier MAY list its tools: its tools/list MUST answer at the pending tier, naming contact_accepted and contact_rejected, and every other call from it MUST answer pending_approval until the owner decides.

§ 6.2Core tools

IdThe sentence
6.2#1A msg_id MUST be a non-empty string — idempotency keyed on nothing protects nothing.
6.2#2ok — a card refresh, or status: pending from a new address under ask (§5.3). The caller's chain is the authority: the card's certificate MUST equal the chain's leaf, and a card that names another root or carries a certificate that is not that leaf MUST be refused bad_request, the card-intake code of §3
6.2#3get_status answers from that fixed four-value vocabulary; an implementation whose upstream presence source knows richer states MUST map any state not listed to busy.

§ 9Hosting

IdThe sentence
9.#1A leaf's key does not outlive its leaf: a host MUST stop using the key of a leaf that has expired and MUST destroy it, keeping the key id so that an envelope still sealed to it is answered certificate_renewed (§14.4) — past its date every verifier refuses the leaf (§14.2 rule 4), so the key can do nothing legitimate, and a renewal has never needed it.
9.#2Moving is the person issuing a leaf to the new host, the data carried across as an archive — the person's contacts and their conversations, with the media in them, and nothing that is the host's own: no settings, no credentials, no invites, no record of the host's leaves; an archive is the export of §9.2, a host that makes one MUST NOT put key material of any kind in it, and a host that imports one MUST refuse any key material in it and MUST refuse, rather than ignore, anything else it does not recognise — and the new host reaching every contact by §5.3, *before* the person tells the old host to leave, so that no contact meets a gap.
9.#3An address an identity has vacated MUST NOT be assigned to another identity until the last leaf issued for it has expired, so a contact that missed the move never reaches a stranger where it expects a friend.
9.#4A host that exports an identity toward a destination that cannot carry its chain MUST say so before the export; the remedy is a destination that can.
9.#5It issues one live leaf per identity at a time — a second endpoint is a move, not a second home, because contacts keep one pin and the newest leaf wins — and MUST NOT issue a second while one is live except as its replacement.
9.#6A wallet MUST NOT write a leaf, a ledger entry or a contact into the file, and writes it once, when the root is made, and again only when the root is re-bound or a hardware key takes it.

§ 9.1Signing requests

IdThe sentence
9.1#1A wallet MUST refuse a request that is not a top-level navigation, as the browser's fetch metadata reports it (Sec-Fetch-Mode: navigate, Sec-Fetch-Dest: document), so that a script on another page cannot probe it.
9.1#2A wallet MUST refuse a request whose Origin is absent, null, or different from the origin of redirect: the host that asks is the host that collects.
9.1#3A wallet MUST refuse a request with a field the table above does not list, a field that is not a string, or a field that breaks the table, and a request that has expired or expires more than ten minutes ahead.
9.1#4A wallet MUST refuse a request whose CSR fails the checks of §9 — its own signature, and a key that is not a root — or names an endpoint that is not in the normal form of §14.1 or fails the address guard of §3.
9.1#5A wallet MUST prove the root against expect_root (§2.2) before it signs.
9.1#6It MUST show the person the asking origin, the recipient as the host's own claim, the endpoint, the validity and whether the host is new.
9.1#7The wallet MUST NOT keep anything of the request once it has answered, and MUST NOT write its body to a log.
9.1#8A host MUST accept an answer only once, only with the state it minted for a pending request, and only a chain whose leaf carries that request's key and validates at its endpoint (§14.2).

§ 9.2The export

IdThe sentence
9.2#1An exporter MUST NOT leave out a media file it holds for a message it exports: a file it cannot include is a reason to refuse the export, never to omit the file.
9.2#2Spreadsheet formulas. A writer MUST write a CSV cell that begins with =, +, -, @, ', a tab or a carriage return with one ' before it, and a reader strips one leading '. base64url DER cannot begin that way: it starts with M, from its first byte 0x30.
9.2#3Unencrypted. Every surface that writes an export MUST tell the person, before the file is written, that it is not encrypted, that anyone who gets it can read what it holds — their contact list and all their conversations and files for a full export, their contact list for a book — and that it holds no keys, so it cannot be used to speak as them.
9.2#4A host that delivers an export over a network MUST NOT keep it at rest: it builds the file when the signed-in person asks and streams it to them.
9.2#5Validation. An importer MUST check the whole file before it writes anything, and MUST refuse the whole file if any check below fails:
9.2#6An importer MUST refuse any entry whose name is not exactly manifest.json, contacts.csv, threads.csv, messages.jsonl, media/, or media/ followed by 64 lowercase hex digits — so no .., no absolute path, no backslash and no other file — and it MUST read the zip's central directory as the only index
9.2#7An importer MUST refuse a file in which one name appears twice
9.2#8An importer MUST refuse a file that lacks a member; a file MAY omit threads.csv, messages.jsonl and media/ only when its manifest counts them zero, which is what a book does
9.2#9An importer MUST refuse an encrypted entry, a symbolic link (a Unix mode in the external attributes), and any directory but media/
9.2#10An importer MUST count sizes by the bytes it actually decompresses, never by the sizes a header states, and MUST refuse a manifest over 64 KiB, a contacts.csv over 4 MiB or 5000 rows, a threads.csv over 16 MiB, a line of messages.jsonl over 64 KiB, a media file over 5 MiB, and anything over a ceiling of the host's own (below)
9.2#11An importer MUST refuse a text member that manifest.files does not list or whose sha256 differs from it, a listed member the file lacks, a files entry that names anything but a text member, a media member whose name is not the lowercase hex sha256 of its bytes, and counts that differ from what the file holds — counts.media included, which is the number of media members
9.2#12An importer MUST refuse a file whose owner is not the root of the identity importing it, and a contact row whose root is owner
9.2#13An importer MUST refuse a header that is not exactly the one shown, a row or a message that breaks what its column or member holds above, a message with a member not listed or one missing, and a message with more than one attachment, a message that carries an attachment and a body that is not empty, and a time that is not an RFC 3339 instant in UTC ending in Z, or whose fraction follows anything but a .
9.2#14An importer MUST refuse a thread whose contact, a message whose thread, contact or non-null reply_to, or an attachment whose file names nothing in the file, and a media file that nothing names
9.2#15An importer MUST refuse any cell, any string member of the manifest or of a message, and any media file that decodes as a private key — PKCS #8 or SEC1, in DER or PEM — and MUST parse a certificate only as a certificate of §14.1's profile
9.2#16It MUST show the person the contacts, and write nothing until the person agrees.
9.2#17An imported leaf MUST NOT replace a pin the host validated itself, and a row's leaf is pinned only when [leaf, root_cert] validates at the row's endpoint (§14.2).
9.2#18A host MUST NOT send a message it imported, whatever its status: retries belonged to the host that exported it.
9.2#19The import MUST end with a request for a new leaf for the importing endpoint, which the host mints itself with expect_root equal to owner — move for an identity new to the host, renew for one it already serves — and which the person completes in their wallet (§9.1).
9.2#20Once that leaf is installed, the host MUST call update_contact at every imported contact that is not blocked and whose leaf it holds (step 2), since a contact whose leaf it does not hold cannot be sealed to, and MUST report every other contact that is not blocked as unreached, without retrying it; that contact stays pinned by its root.
9.2#21A contact that refuses the call — update_contact is a contact-tier tool, and that contact does not hold the identity as one — MUST then be sent request_contact, which that contact decides under its own policy.
9.2#22A writer MUST write reply_to as null when the message it names is not in the file.
9.2#23A writer MUST drop from their_permissions every name that is not a permission of §8 and every name repeated, since the column is informative.
9.2#24A writer MUST truncate display_name to 200 characters, since it is the contact's own claim.
9.2#25A writer MUST leave out a message whose body, or whose media file, the key-material check above would refuse, with the attachment it carried, and MUST list each message it leaves out, by its id and the reason, in the report it gives the person; it never leaves one out silently.
9.2#26Ceilings. A host MAY set import ceilings of its own, on the whole file and on counts — contacts, threads, lines of messages.jsonl, the characters of an id — and MUST name the ceiling in each refusal it makes for one.
9.2#27A host MUST NOT refuse to write an export because the file would exceed an import ceiling of its own, of any kind, the whole-file ceiling included; it MAY warn the person that the file exceeds them, naming each.
9.2#28The owner's own strings. A writer MUST refuse to write a manifest whose owner_name or tool the key-material check above would refuse, naming the member: those are the owner's and the host's own, not a contact's, so there is nothing to leave out.

§ 13.1Format

IdThe sentence
13.1#1base64url HPKE encapsulated key, of exactly the suite's Npk (RFC 9180 §7.1): 65 bytes for PACT-SEAL-P256, an uncompressed P-256 point, and 32 for PACT-SEAL-X25519. A receiver MUST refuse any other length (envelope_invalid) — sig covers the three members concatenated with nothing between them, so the suite's own length is what fixes the boundary; without it a byte moved from the end of enc to the front of ct leaves the signed bytes identical
13.1#2Each of the four members is base64url (RFC 4648 §5) without padding, in its one canonical spelling, and a receiver MUST refuse (envelope_invalid) a member written any other way: with a character outside that alphabet — padding, whitespace and the standard alphabet's + and / among them — or with a last character whose unused bits are not zero.
13.1#3The suite follows the recipient's key and nothing else: a receiver MUST refuse an envelope whose suite is not the one its key takes (envelope_invalid), so no choice is left on the wire for a sender to make badly.
13.1#4The HPKE info parameter is the ASCII string PACT-SEAL-v2, and an envelope sealed under any other info string MUST NOT open.
13.1#5The HPKE ephemeral MUST be fresh for every envelope — a reused one repeats the key and the nonce, and two ciphertexts under them leak the XOR of their plaintexts — and both sides MUST refuse an all-zero DH output, which a low-order X25519 point produces (RFC 9180 §7.1.4).
13.1#6msg_id is REQUIRED and MUST be non-empty — replay protection keyed on an empty string protects nothing.
13.1#7A protected header carrying a member not listed for its v, or one whose type is not the one listed — v, ts and exp are JSON integers, suite, kid, msg_id and cty JSON strings — MUST be rejected (envelope_invalid): the header is the AAD, and two implementations that disagree about what was signed cannot interoperate.

§ 13.2The sealed_call tool

IdThe sentence
13.2#1The plaintext of a request envelope is one bare JSON object (no JSON-RPC framing) of method, params, and exactly one of chain and leaf; the method MUST be tools/call or tools/list.
13.2#2A sender MUST carry chain on first contact and in its first envelope to each contact after a renewal, and MAY carry it at any time.
13.2#3The result of a sealed request MUST be sealed back to the caller (same format, kid naming the caller's leaf key, the request's msg_id for correlation, cty: application/pact-result+json, and the responder's own chain or leaf in the plaintext beside the result, by the same rule — the chain when the caller has not seen this leaf, the fingerprint after; a result plaintext is one bare JSON object of result, the inner result, or error, an error object of §12, and exactly one of chain and leaf); result envelopes are never dispatched — the receiving caller decodes, opens, validates the chain or finds the named leaf among its pins, verifies the signature and correlates; a caller that cannot verify a result asks with get_card, which always answers with the chain — and the request-side steps of §13.3 (idempotency, tiering) do not apply to them.
13.2#4chain MUST validate (§14.2), sig MUST verify under its leaf key, and its leaf MUST byte-equal the card argument's X-PACT-CERT.
13.2#5Error results follow the sealing rule too: once a request envelope has been successfully opened, an error result MUST be sealed back like any other result — a plaintext error is only for an envelope that could not be opened at all, where there is no proven key to seal toward.

§ 13.3Opening

IdThe sentence
13.3#1Receivers MUST validate in this order, rejecting at the first failure: decode protected; check v and suite supported; resolve kid to a leaf key this endpoint holds for the identity served at the path the envelope arrived at — the current one, or a superseded one not yet past its notAfter — and otherwise answer certificate_renewed with the current chain when kid names a key this endpoint once held for that identity, envelope_invalid when it never did or holds it for another identity (§14.4); check that suite is the one the leaf's key takes (§13.1); HPKE-open; require the plaintext to carry exactly method, params and one of chain or leaf; with leaf, find the leaf it names among the pins of active and pending contacts and verify sig under its key, answering chain_required to any failure, and proceed at that pin's tier and endpoint; with chain, validate it (§14.2), verify sig under its leaf key, and resolve the tier (§6.1) — when the chain's root is pinned, a leaf older than the pinned one is a guest, a different endpoint is §5.3, a newer leaf at the pinned endpoint replaces it; when it is not pinned, apply the guest binding of §13.2; enforce time — now < exp, and |now − ts| ≤ 300 s, since every envelope is delivered directly; enforce msg_id idempotency (a replayed envelope is acknowledged with its original result, never re-executed); then dispatch.
13.3#2Idempotency records for seen msg_ids MUST be retained until min(exp, ts + 300 s) — the end of the window in which the envelope could be presented again and accepted.
13.3#3A blocked sender's envelopes MUST be processed exactly as an unknown sender's — the guest card-binding rules of §13.2 apply and a sealed tools/list is rejected envelope_invalid — so sealing never becomes an oracle distinguishing blocked from unknown (§12); a guest envelope whose inner call carries no card argument is likewise rejected envelope_invalid.

§ 13.4Negotiation

IdThe sentence
13.4#1none — the recipient does not accept envelopes (sealed_call absent; senders MUST NOT seal); optional — both accepted; senders MAY seal; required — unsealed substantive calls are refused (plain tools/list still answers with whatever the transport identity earns), and senders MUST seal.

§ 13.5Stated trade-offs

IdThe sentence
13.5#1That key signs exactly four structures — a TLS handshake, a certificate signing request, a card, an envelope — each distinguishable by its first bytes, and an implementation MUST NOT sign anything else with it.

§ 14.1Profile

IdThe sentence
14.1#1A certificate's signatureAlgorithm MUST be its issuer key's own algorithm; a verifier takes the algorithm from the key, never from the certificate, so a mismatch is simply a certificate the key did not sign.
14.1#2The algorithm identifier inside the tbsCertificate and the outer signatureAlgorithm MUST be byte-equal and carry no parameters, as RFC 5280 §4.1.1.2 requires — a certificate that reads one way to a verifier of this profile and another to a TLS stack is exactly what §14.1 exists to exclude.
14.1#3So an ECDSA signature on a certificate MUST be the twin with s ≤ n/2, the *low-S* form: an issuer normalises what it signs, including a signature a hardware token made, and a verifier refuses the other twin as outside the profile, at card intake as much as in a chain.

§ 14.2Chain validation

IdThe sentence
14.2#1When the verifier already holds a fingerprint for the identity in question — from a pin, or from the issuer key identifier of a card's certificate — the two MUST be equal.
14.2#2When the verifier knows which address is in question — the URL it dialed, the endpoint it pinned, the endpoint in the card — the URI MUST equal it byte for byte — both are the normal form of §14.1, so nothing is normalised at comparison time.
14.2#3A dNSName beside the URI MUST equal its host, and the address guard of §3 — no loopback, link-local or private host; never the verifier's own endpoint from a guest — applies before any dial.
14.2#4A verifier therefore MUST NOT refuse a chain on the root's notBefore — including a root whose notBefore is later than the leaf's, which is the ordinary case for a new identity, since §14.1 backdates a first leaf up to an hour for clock skew while the root was made minutes ago.

§ 14.3The newest leaf wins

IdThe sentence
14.3#1A verifier that does confirm a pin, by whatever means and at whatever moment it chooses, MUST NOT treat an unanswered or failed confirmation as a reason to refuse a contact or to un-pin one: an endpoint that is down, slow, or behind a network the verifier cannot reach at this moment is not a compromised endpoint, and a rule that turned unreachability into revocation would hand any carrier the power to disconnect two people by dropping one request.