Internet-Draft J.J. Brucker draft-foopgp-secret-sheets-00 foopgp Intended status: Informational Mnêmê Expires: 21 March 2027 foopgp 17 September 2026 PGP ID Secret Sheets: OpenPGP Secret Keys on Paper, Shared Out Across QR Codes draft-foopgp-secret-sheets-00 Abstract This document specifies "PGP ID secret sheets": a paper backup of an OpenPGP secret key, printed as a set of QR codes of which a chosen number is enough to rebuild the key. It describes every version of the QR code payload defined so far. Versions 1, 2 and 3 were experimental and are obsolete. Versions 4 and 5 are in use. Version 6 encodes the payload in base45, so that the same key fits in a smaller QR code that is easier to read. Because the size of the key decides the size of the codes, the document also states how a secret key is to be cleaned before it is printed. Status of This Memo This is an Internet-Draft submitted in full conformance with the provisions of BCP 78 and BCP 79. Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). They are subject to change and may be replaced or obsoleted at any time. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/. This Internet-Draft will expire on 21 March 2027. Copyright Notice Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved. This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info). Table of Contents 1. Introduction 1.1. Goals 1.2. Versions at a Glance 2. Conventions and Terminology 3. Overview 4. Preparing the Secret 4.1. Export 4.2. Cleaning 4.3. Secret Key Protection 5. Dividing the Secret 5.1. Consecutive Pieces 5.2. Shamir's Secret Sharing over GF(2^8) 6. Payload Format 6.1. Syntax 6.2. Header Fields 6.3. Obsolete Versions 1, 2 and 3 6.4. Version 4 6.5. Version 5 6.6. Version 6 7. QR Code Symbols 7.1. Error Correction Level 7.2. Encoding Modes 7.3. Symbol Size 8. Printed Sheet (Informative) 9. Reading a Set 10. Writing a Set 11. Security Considerations 12. Privacy Considerations 13. IANA Considerations 14. Implementation Status 15. References 15.1. Normative References 15.2. Informative References Appendix A. Test Vectors Appendix B. History of the Versions Authors' Addresses 1. Introduction A secret key kept in one place is one accident away from being lost. In the PGP ID system [URN-EID], a person's OpenPGP secret keys [RFC9580] normally live on a hardware token (an OpenPGP smartcard); losing the token must not mean losing the identity. The keys are therefore generated off the token, printed on paper, and only then moved onto it. With the paper, a new token can be configured. Paper keeps for decades without power, cannot be read across a network, and is understood by everybody. A single sheet, however, is a single thing to lose or to have taken. So the key is divided across several sheets, each holding one QR code [ISO18004]: by default any 3 of 5 sheets rebuild the key, fewer than 3 reveal nothing about it, and losing 2 costs nothing. 1.1. Goals o Nothing electronic is needed to keep the backup. o A sheet is read back with a webcam or a phone camera. o In the shared schemes, no single sheet, and no group smaller than the threshold, discloses anything about the key. o Each QR code says what it is. A reader can tell a fragment from any other QR code, fragments of two different sets from each other, and a version it knows from one it does not know. o The payload is plain text, so that common QR decoders return it intact. o The smaller the key, the smaller the codes: what is printed is only what is needed to use the key again. 1.2. Versions at a Glance Versions 1 to 3 were experiments that added a symmetric encryption under a weak, partly guessable passphrase on top of the division of the key. Their format was never published before this document, and they only served a small circle of testers. They are OBSOLETE. Version 4 cuts the key into consecutive pieces and version 5 shares it out with Shamir's scheme; both write base64url text. Version 6 does what versions 4 and 5 do, but writes base45 text, which a QR code stores in about 23% fewer bits. +-----+------------+-------------+-------------------+-----------+ | Ver | Status | Division | Extra encryption | Body text | +-----+------------+-------------+-------------------+-----------+ | 1 | obsolete | cut | yes (digits) | base64url | | 2 | obsolete | cut | yes (digits+words)| base64url | | 3 | obsolete | shared | yes (digits) | base64url | | 4 | current | cut | no | base64url | | 5 | current | shared | no | base64url | | 6 | proposed | cut, shared | no | base45 | +-----+------------+-------------+-------------------+-----------+ 2. Conventions and Terminology The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals. secret (S): The octet string that the sheets carry. For versions 4 to 6, it is a binary (not ASCII-armored) OpenPGP Transferable Secret Key (Section 10.2 of [RFC9580]). For versions 1 to 3, it is an OpenPGP message, symmetrically encrypted, whose plaintext is such a key (Section 6.3). set: The sheets printed together, from one secret, in one run. Two printings of the same key are two different sets, and their fragments do not combine. sheet: One printed page. It carries exactly one QR code. fragment, payload: The text carried by the QR code of one sheet. n: The number of sheets in a set: from 3 to 10 in versions 1 to 5, whose header numbers them with a decimal digit, and from 3 to 16 in version 6, which uses a hexadecimal one. t (threshold): The number of sheets needed to rebuild the secret, from 2 to n. A threshold of 1 would print the whole secret on every sheet. cut set: A set in which t = n: the secret is cut into n consecutive pieces (Section 5.1) and every sheet is needed. shared set: A set in which t < n: every sheet carries one share of the secret (Section 5.2) and any t of them rebuild it. share number: The abscissa of a share, an integer from 1 to 255. writer, reader: A program that produces sheets from a secret key, and one that rebuilds the secret key from sheets. base64url: The URL- and filename-safe Base 64 encoding of Section 5 of [RFC4648], WITH its "=" padding characters. base45: The Base45 encoding of [RFC9285]. The syntax in Section 6.1 uses ABNF [RFC5234]. 3. Overview Writing a set goes through the following steps; reading a set goes through them backwards. secret key in a keyring or a key file | | export and clean (Section 4) v secret S: a binary Transferable Secret Key | | [versions 1-3 only: symmetric encryption, Section 6.3] | | cut into n pieces (5.1), or n shares, t needed (5.2) v n fragments of octets | | encode as text (base64url, or base45 for version 6) | and prefix with a header (Section 6) v n payloads --> n QR codes (7) --> n printed sheets (8) What the sheets rebuild is always an OpenPGP key (encrypted once more in versions 1 to 3), never a raw private scalar: any OpenPGP implementation imports it as it stands, and it carries its own self-signatures. 4. Preparing the Secret 4.1. Export The secret key is exported as a binary Transferable Secret Key. A writer MUST NOT use ASCII armor: the division and the text encoding already make the payload text, and armor would add a third of the size for nothing. 4.2. Cleaning In a shared set every share is as long as the whole secret, and the length of the text decides the size of the QR code (Section 7.3). A few hundred octets more can make the difference between a code that a phone reads at a glance and one that nobody manages to read. Everything that the certificate published on keyservers gives back does not need to be on paper. A writer MUST clean the exported key as follows before dividing it: 1. Keep only the most recent self-signature of each User ID and each subkey, and drop all third-party certifications. (With GnuPG: export option "export-minimal".) 2. Drop User IDs that are not usable (revoked, expired, or without a valid self-signature) and signatures that are not usable. (With GnuPG: export option "export-clean".) 3. Drop every User Attribute packet: a backup does not need a photograph, and each kilobyte of picture is another kilobyte on every sheet. (With GnuPG: export option "no-export-attributes".) 4. Keep at most three User IDs, and drop every other User ID packet together with the signatures that follow it: * the identity User ID "UID:urn:eid:..." [URN-EID]; * the name User ID "FN:..."; * one address User ID: the primary User ID when it is an address, otherwise the most recent usable one. These three are enough to know whose key it is and to write back. Removing a User ID together with its own self-signature leaves every other signature valid, so nothing needs to be re-signed and no passphrase is needed for this step. A writer MUST NOT add to the secret anything that is not part of the key. For scale: a key made the way PGP ID tools make them (an Ed25519 primary key, a Cv25519 encryption subkey, an Ed25519 authentication subkey, and the three User IDs above), exported by GnuPG 2.4 and cleaned as above, takes 1112 octets when its secret parts are not protected, and 1250 octets when they are protected by a passphrase. The same key with a photograph, or with a few dozen certifications, takes several times as much. 4.3. Secret Key Protection The secret parts of an OpenPGP key MAY be protected by a passphrase (Sections 3.7 and 5.5.3 of [RFC9580]). Versions 4 to 6 carry the key exactly as it was exported, protected or not, and a reader MUST NOT remove or change that protection: that is the business of whatever uses the key next (for example, the program that moves it onto a token). o In a shared set, fewer than t sheets reveal nothing (Section 11), so the passphrase is a second factor rather than the only one. It costs about 46 octets per secret key packet, 138 octets for the key described in Section 4.2, which is two QR code versions. A writer MAY remove the protection before printing a shared set, and SHOULD say so. o In a cut set, every sheet carries a contiguous part of the key; a piece of a few hundred octets can hold a whole secret key packet. The passphrase is then the only protection, and a writer SHOULD NOT print an unprotected key in a cut set. o Whenever a passphrase is kept, it MUST be kept apart from the sheets. Printing it on the sheets, as some writers can be asked to do, is NOT RECOMMENDED: in a cut set, it makes every sheet disclose the key material it carries, and in a shared set, it turns the passphrase into nothing more than the threshold already is. 5. Dividing the Secret 5.1. Consecutive Pieces A cut set has t = n, and every piece is needed. Versions 1, 2 and 4 cut the text: the whole secret is encoded in base64url, and the resulting text of L characters is cut into n pieces. The pieces 0 to n-2 hold floor(L/n) characters each, and the piece n-1 holds the rest, padding included. Version 6 cuts the octets: the secret of |S| octets is cut into n octet strings, the strings 0 to n-2 holding floor(|S|/n) octets each and the string n-1 holding the rest; each string is then encoded in base45 on its own. (A base45 text cannot be cut at an arbitrary character: its characters go by groups of three.) A reader concatenates the pieces in the order of their index (Section 6.2) and decodes the result (versions 1, 2, 4), or decodes each piece and concatenates the octets (version 6). A reader MUST NOT rely on the lengths of the pieces: only their order matters. 5.2. Shamir's Secret Sharing over GF(2^8) A shared set divides the secret with Shamir's scheme [SHAMIR], applied independently to every octet, exactly as the libgfshare library does [LIBGFSHARE] (its "gfsplit" and "gfcombine" tools have been the reference implementation of versions 3 and 5). [SHAMIR] describes the scheme with integers modulo a prime p: the secret is the constant term of a random polynomial q of degree t-1, the shares are q(1), ..., q(n), and p must exceed n so that n distinct non-zero abscissas exist. libgfshare works in the field GF(2^8) instead, one octet at a time, which has 256 elements: at most 255 non-zero abscissas, hence at most 255 shares. Neither the field representation nor the share format of libgfshare is specified elsewhere, so this section specifies them, and Appendix A gives test vectors. Arithmetic takes place in GF(2^8), with the reduction polynomial x^8 + x^4 + x^3 + x^2 + 1 (0x11D). Addition is exclusive OR. Multiplication can use logarithm tables built from the generator x (the octet 0x02). [TSS] specifies the same scheme over GF(2^8) with another reduction polynomial, x^8 + x^4 + x^3 + x + 1 (0x11B, the one of AES); its shares are not compatible with these. To share a secret S of |S| octets into n shares, t of which are needed, a writer: 1. draws n distinct share numbers x_1, ..., x_n, uniformly at random from 1 to 255. The number 0 MUST NOT be used: the value of the polynomial at 0 is the secret itself. 2. draws, for every octet position p, t-1 coefficients a_1[p], ..., a_(t-1)[p], uniformly at random from a cryptographically secure random number generator. ("gfsplit" reads them from /dev/urandom; when it cannot access it, it only warns and falls back to rand(), which is not secure. A writer that relies on it MUST make sure that /dev/urandom is readable. It also checks the threshold given with "-n" against the number of shares known when it reads that option, 5 until "-m" has been read: "-m" has to come first.) 3. computes, for every share i and position p, Y_i[p] = S[p] + a_1[p] * x_i + ... + a_(t-1)[p] * x_i^(t-1) so that the share i is the pair (x_i, Y_i), with |Y_i| = |S|. (libgfshare stores the random coefficients first and the secret last, and evaluates the polynomial with Horner's rule; the order of storage changes nothing to the result.) From any k >= t shares with distinct share numbers, a reader rebuilds every octet by Lagrange interpolation at 0: k x_j S[p] = SUM Y_i[p] * PRODUCT ----------- i=1 j != i x_i + x_j All k shares take part, as with "gfcombine". Shares from the same set agree, so extra shares do not change the result; a share from another set turns the result into noise, which the reader detects when it parses the key (Section 9). The share numbers need not be secret, and their values do not matter to the security of the scheme as long as they are distinct and non-zero. "gfsplit" draws them from a generator seeded with the time in seconds, which is enough, and never draws 0: it replaces 0 by 1, and libgfshare refuses a share number 0 anyway. Only versions of libgfshare older than November 2009 could produce a share numbered 0 (in fact a copy of share 1); no writer of this format has used them. The sheets of a shared set are indexed (Section 6.2) in increasing order of share number, which is the order in which the file names written by "gfsplit" sort. The index is for people and for keeping track of the sheets read; the share number is what the combination needs. 6. Payload Format 6.1. Syntax payload = cut-payload / shared-payload / v6-payload cut-payload = "~" ("1" / "2" / "4") t-less-one index b64text shared-payload = "~" ("3" / "5") t-less-one index share-num b64text v6-payload = "~6" t-less-hex index-hex (share-hex / cut-mark) b45text t-less-one = DIGIT ; t - 1 index = DIGIT ; 0 to n - 1 t-less-hex = UPHEX ; t - 1, version 6 index-hex = UPHEX ; 0 to n - 1, version 6 share-num = 3DIGIT ; "001" to "255", decimal share-hex = 2UPHEX ; "01" to "FF", hexadecimal UPHEX = DIGIT / %x41-46 ; "0"-"9", "A"-"F", upper case only cut-mark = "**" ; a piece of a cut set b64text = 1*b64char *2"=" b64char = ALPHA / DIGIT / "-" / "_" b45text = 2*b45char b45char = DIGIT / %x41-5A / SP / "$" / "%" / "*" / "+" / "-" / "." / "/" / ":" A payload contains no line break. The "=" characters of a b64text only appear at its end; in a cut set of version 1, 2 or 4, only the last piece can hold them. 6.2. Header Fields "~" (0x7E): Marks a fragment. A QR code whose text does not start with "~" followed by a digit from "1" to "9" is not a fragment, and a reader ignores it (it may be a business card or an address). version: One digit. "0" is invalid. "1" to "6" are defined in this document. "7" to "9" are reserved for future versions. t-less-one, t-less-hex: The threshold less one, in one character: a decimal digit in versions 1 to 5, so t is at most 10, and an upper-case hexadecimal digit in version 6, so t is at most 16. In a cut set, it is n - 1. index, index-hex: The rank of the sheet in its set, from 0, in one character: a decimal digit in versions 1 to 5, so n is at most 10, and an upper-case hexadecimal digit in version 6, so n is at most 16. The sheet shows it plus one, in decimal ("FRAGMENT 12/16"). In a cut set, it puts the pieces back in order; in a shared set, it is only the rank of the share number within the set. share-num: In versions 3 and 5: the share number (Section 5.2), in three decimal digits, "001" to "255", as "gfsplit" names its files. share-hex: In version 6: the share number in two upper-case hexadecimal digits, "01" to "FF"; "00" is invalid. A share number never exceeds 255 (Section 5.2), so two digits always suffice, and upper-case hexadecimal digits are in the alphanumeric set of QR codes (Section 7.2), where lower-case letters are not. cut-mark: In version 6: "**" in place of the share number, for a piece of a cut set. "*" is in the alphanumeric set, and it is neither a digit, nor a hexadecimal digit, nor a character of base64 or base64url. (Read it as "every sheet is needed".) In version 6, the head is thus always six characters long, all of them upper-case hexadecimal digits after "~6" except for a cut mark, and the base45 text always starts at the seventh. A reader MUST read the fields by position: the base45 text may itself start with "*". The number of sheets n is not written in the payload; only the sheet shows it. A reader does not need it: a shared set is complete with t sheets, and a cut set has n = t. 6.3. Obsolete Versions 1, 2 and 3 Versions 1, 2 and 3 are OBSOLETE. A writer MUST NOT produce them. They came from experimental code, used by a small circle of testers, and their format is described here for the first time, only so that the few sets still held can be recovered. A reader MAY read them, and SHOULD then advise to print a new set in a current version and to destroy the old one (Section 11). In these versions, a passphrase P served twice: it protected the secret parts of the key, and it encrypted the whole export once more before the division: o The key is exported without cleaning, protected by P. o The export is encrypted as an OpenPGP symmetrically encrypted message [RFC9580], binary, under P. The algorithms are the defaults of the GnuPG version that wrote it ("gpg --symmetric"); the message describes them itself, and any OpenPGP implementation decrypts it. This message is S. o A reader rebuilds S, asks for P, decrypts S, and obtains the key, still protected by P. Version 1: Cut set (text cut, Section 5.1), error correction level M. P is the date of birth of the holder, reduced to its digits: "YYYYMMDD". Payload i is "~1", then n - 1, then i, then the piece i. Version 2: Same as version 1, except for P. The writer draws n words from an English dictionary and prints word i on sheet i ("Secret word - i/n -> "). P is the date digits followed by the n words in sheet order, separated by single spaces: "YYYYMMDD w1 w2 ... wn". A reader asks for the date, then for the words in sheet order, and joins them the same way. Payload i starts with "~2". Version 3: Shared set (Section 5.2) of S, error correction level L, with P the date digits as in version 1. Payload i is "~3", then t - 1, then i, then the share number of the share i in three digits, then the base64url text of Y_i. 6.4. Version 4 Version 4 is a cut set without extra encryption. o S is the cleaned Transferable Secret Key (Section 4). o The base64url text of S is cut into n pieces (Section 5.1). o Payload i is "~4", then t - 1 (that is, n - 1), then i, then the piece i. o Error correction level M (Section 7.1). The protection of the key by its passphrase is the only thing that keeps a single sheet from disclosing part of the key (Section 4.3). 6.5. Version 5 Version 5 is a shared set without extra encryption. o S is the cleaned Transferable Secret Key (Section 4). o S is shared into n shares, t of which are needed (Section 5.2). o Payload i is "~5", then t - 1, then i, then the share number of the share i in three digits, then the base64url text of Y_i. o Error correction level L (Section 7.1). Version 5 is what writers produce by default today. 6.6. Version 6 Version 6 carries the same secret as versions 4 and 5, divided the same ways, but its body text is base45 [RFC9285] instead of base64url. A QR code stores a base45 character in 5.5 bits and a base64url character in 8 bits, so the same secret takes about 23% fewer bits, which is typically four QR code versions less for a PGP ID key (Section 7.3). o S is the cleaned Transferable Secret Key (Section 4). o In a shared set (t < n), S is shared into n shares (Section 5.2), and payload i is "~6", then t - 1 and i, each in one upper-case hexadecimal digit, then the share number of the share i in two upper-case hexadecimal digits, then the base45 text of Y_i. o In a cut set (t = n), S is cut into n octet strings (Section 5.1), and payload i is "~6", then n - 1 and i, each in one upper-case hexadecimal digit, then "**", then the base45 text of the string i. o All the payloads of a set MUST agree: either all carry "**", or none does. o Error correction level L for a shared set, M for a cut set (Section 7.1). o The payload is written in two QR code segments (Section 7.2). Version 6 keeps the "~" marker, although it is not one of the 45 characters that the alphanumeric mode of QR codes can store. It takes a segment of its own: 4 bits of mode, 8 or 16 bits of length, and 8 bits for the character, where an alphanumeric character would have cost 5.5 bits. That difference of about 22 bits changed the symbol version in none of the measurements of Section 7.3. In return, a reader that only knows versions 4 and 5 recognises a fragment and says that its version is unsupported, instead of taking it for an unrelated QR code. A reader MUST reject a base45 text that contains a character outside the alphabet or a group of three characters whose value exceeds 65535 (Section 6 of [RFC9285]), and also one that ends with a group of one character, or with a group of two characters whose value exceeds 255. 7. QR Code Symbols 7.1. Error Correction Level A QR code states its own error correction level, and a reader MUST accept any level. Writers of shared sets (versions 3 and 5) use level L, the lowest, because each share is as long as the whole secret and every step up makes the code denser. Writers of cut sets (versions 1, 2 and 4) use level M, which a piece of a fraction of the secret can afford. Writers of version 6 SHOULD do the same: level L for a shared set, level M for a cut set. 7.2. Encoding Modes A QR code stores its text in segments, each with its own mode [ISO18004]. The modes that matter here are: o byte mode: any octet, 8 bits per character; o alphanumeric mode: only the 45 characters "0"-"9", "A"-"Z", space, "$", "%", "*", "+", "-", ".", "/" and ":", 11 bits per pair of characters (5.5 bits per character). The payloads of versions 1 to 5 contain lower-case letters and "_", so they are stored in byte mode. A version 6 payload SHOULD be stored in two segments: the "~" in byte mode, and everything after it, which is entirely in the alphanumeric set, in alphanumeric mode. A symbol with the whole payload in byte mode is valid, but larger than a version 5 symbol of the same secret, which defeats the purpose. Encoders that choose segments automatically do this by themselves (libqrencode by default; ZXing with its "QR_COMPACT" hint). A writer SHOULD check the version of the symbol it obtained. No Extended Channel Interpretation (ECI) is used: every payload is ASCII text. A reader MUST pass the text of the QR code on exactly as decoded: no trimming, no case folding, no normalisation of spaces. A base45 text contains spaces. (It never ends with one: the last character of a group is at most "W", or "5" for a final group of two.) 7.3. Symbol Size This section is informative. What limits a sheet is not the capacity of QR codes (2953 characters in byte mode at level L), but how closely the paper has to be held and how sharp the camera must be for the modules to be told apart. The more text, the higher the version of the symbol, the more modules across, and the smaller each module in a code printed at a fixed size. Cost of one octet of secret, in bits inside the symbol: +------------------------+-----------------+----------------------+ | Text encoding | QR mode | Bits per secret octet| +------------------------+-----------------+----------------------+ | base64url (v1-v5) | byte | 4/3 x 8 = 10.67 | | Ascii85 / base85 | byte | 5/4 x 8 = 10.00 | | basE91 | byte | about 9.85 | | base45 (v6) | alphanumeric | 3/2 x 5.5 = 8.25 | | decimal | numeric | about 8.03 | | raw octets | byte | 8.00 | +------------------------+-----------------+----------------------+ Any text encoding stored in byte mode costs at least 9.74 bits per octet, since a printable ASCII character carries at most log2(95) = 6.57 bits. Base45 goes below that bound because the alphanumeric mode stores its characters in 5.5 bits. Decimal text is slightly denser still, but no standard defines it for this use and its conversion is quadratic. Raw octets are fragile: many decoders convert them to text with a guessed character set. Symbol versions obtained at level L for shared sets (one share per code, header included), and the size of a module when the code, with a quiet zone of 4 modules on each side, is printed 120 mm wide: +--------+-----------+---------+---------+---------+-------------+ | Secret | Version 5 | Modules | Version | Modules | Module size | | octets | symbol | (v5) | 6 symb. | (v6) | v5 -> v6 | +--------+-----------+---------+---------+---------+-------------+ | 1080 | 27 | 125 | 24 | 113 | 0.90 -> 0.99| | 1112 | 28 | 129 | 24 | 113 | 0.88 -> 0.99| | 1250 | 30 | 137 | 26 | 121 | 0.83 -> 0.93| +--------+-----------+---------+---------+---------+-------------+ With a webcam giving 640x480 frames, a version 22 symbol already has to fill about 60% of the frame. A phone camera giving 1920x1080 frames, with its lamp lit, has read version 28 symbols printed 120 mm wide. The reference writers print at most 1672 octets per sheet in version 6: a version 30 symbol, as large as a version 5 symbol of 1280 octets. A writer SHOULD refuse to print a set whose symbols its intended readers cannot read, before producing anything, and SHOULD say what would make the set fit: a cleaner key (Section 4.2), no passphrase in a shared set (Section 4.3), version 6, or a cut set. 8. Printed Sheet (Informative) How a set is printed is not part of the format. This section describes what the reference writers do with version 6: two fragments on each A4 page, portrait, one on each half, with a line to cut along between them. A set of 5 takes 3 pages. +--------------------------------------------------------+ | UID:urn:eid: | |
| | FN: | | +---------------------------+ | | PGPID SECRET (/) | | | | FRAGMENT / | | | | | QR code, 120 mm | | | | | | | | | | | QR version: 6 | | | | | | | | | | | | | | | | 0x XXXX XXXX XXXX | | | | XXXX XXXX XXXX ... +---------------------------+ | | | +-- cut here, and keep each half in a different place ---+ Across the top go the User IDs that the secret carries (Section 4.2, point 4), which are the ones that say whose key this is. On the left of the code, lines too long for the column are cut, at a space or a punctuation mark when there is one. The fingerprint is that of the primary key, in two lines of five groups of four hexadecimal digits. A passphrase, when a writer is asked to print it (Section 4.3), goes last in that column, cut anywhere and with its spaces drawn, since a space at the end of a line would not be seen. The title tells the holder, without any tool, how many sheets are needed and how many exist, and which sheet a reader is talking about when it names an index. With the code 120 mm wide, a module measures 0.99 mm for a PGP ID key without passphrase (version 24), and 0.83 mm at the largest share a writer prints (version 30, Section 7.3). Until the page is cut, it carries two shares: the halves are to be separated before the sheets are put away. Versions 1 to 5 were printed one fragment to a page. The QR code SHOULD be drawn as vector squares, or as a bitmap scaled by an integer factor without smoothing, so that module edges stay sharp. 9. Reading a Set A reader: 1. Decodes the QR codes it is given, from a camera or from images. One image may hold several codes. 2. Ignores every text that does not start with "~" and a digit from "1" to "9" (Section 6.2). It SHOULD tell the user that the code read is not a fragment. A text that starts that way but does not match Section 6.1 is a damaged fragment, and the reader MUST refuse it. 3. Takes the version and t - 1 from the first fragment. A fragment whose version or t - 1 differs belongs to another set: the reader MUST NOT combine it, and SHOULD stop and say so, whether or not its index is one still missing. In version 6, the same applies to a fragment carrying "**" when the first one did not, or the reverse. 4. Keeps one fragment per index, and ignores a fragment whose index it already holds (the same sheet shown twice). 5. Refuses a version it does not support, naming it. For versions 1, 2 and 3, it SHOULD say that the version is obsolete, and MAY point to a tool that still reads it. 6. Considers the set complete: * for a shared set, when it holds t fragments with distinct share numbers; two fragments with the same share number and different indexes make the set invalid; * for a cut set, when it holds the n = t fragments with the indexes 0 to n - 1; an index of n or more makes the set invalid. While the set is incomplete, it SHOULD say how many fragments it holds and how many are needed. For a shared set, it cannot say which sheets are missing, since any t of them will do. 7. Rebuilds S (Section 5), and for versions 1 to 3 decrypts it (Section 6.3). 8. Parses S as a Transferable Secret Key. The format carries no checksum of its own: fragments of two sets with the same version and threshold rebuild noise, and a damaged text can decode to wrong octets. A reader MUST treat an S that does not parse as a Transferable Secret Key whose self-signatures verify as a failed reconstruction, most likely from mixed sets. It SHOULD also check that each secret key matches its public key (Section 11). 9. Imports or hands over the key, protected as it was (Section 4.3), and reports the fingerprint of the primary key. 10. Wipes the fragments, the shares and S as soon as they are no longer needed. It SHOULD keep them in memory only; when a tool in the chain needs files, the reader MUST shred them afterwards. 10. Writing a Set A writer: 1. Takes n from 3 to 16 and t from 2 to n; 3 of 5 is the RECOMMENDED default. t = n makes a cut set, t < n a shared set. 2. Exports and cleans the key (Section 4). 3. Measures what one sheet would carry: |S| for a shared set, ceil(|S| / n) for a cut set. If that is more than its readers can take (Section 7.3), it refuses before producing anything. 4. Divides S (Section 5), encodes the fragments, prefixes the headers (Section 6) and draws the QR codes (Section 7), using a cryptographically secure random number generator for every random value. 5. Lays out and prints the sheets (Section 8), then wipes every intermediate file. A writer MUST NOT produce versions 1, 2 or 3, and SHOULD produce version 6 only. Versions 4 and 5 remain to be read, for the sets already printed; the reference implementations no longer write them. 11. Security Considerations Threshold. In a shared set, any t - 1 shares are consistent with every possible secret, provided that the coefficients are drawn uniformly and independently: a group of sheets smaller than the threshold reveals nothing but the length of the secret. A writer MUST draw the coefficients from a cryptographically secure generator. The share numbers are not secret. Choice of n and t. n - t sheets may be lost without harm, and t - 1 may be taken without harm. The sheets SHOULD be kept in n different places, and t SHOULD be at least 3. Cut sets. Every sheet of a cut set discloses a contiguous part of the key, which can contain an entire secret key packet (an Ed25519 secret is 32 octets). Only the passphrase protects it (Section 4.3). Integrity. Nothing in the header is authenticated. A forged or altered sheet can prevent reconstruction, or, in a cut set, change a part of the key. The self-signatures cover the public parts of the key only; the secret parts are covered by a checksum that an attacker can recompute. A reader SHOULD therefore check that each secret key corresponds to its public key, for example by deriving the public key from the secret, or by making and verifying a test signature, before the key is relied upon or moved onto a token. Obsolete versions. Versions 1 to 3 encrypted the secret under a passphrase whose main part was a date of birth written in digits: about 36,500 values per century, often known to others. The words that version 2 added were printed on the sheets themselves, so whoever held the whole set held them too. That encryption protected against almost nothing, while the same weak passphrase also protected the key. This is why these versions are obsolete, and why their sets SHOULD be re-printed in a current version, with a key protected otherwise or moved onto a token, and then destroyed. Printing. A print job goes through a spooler, a driver, and sometimes a network or a remote service, any of which may keep a copy. Sheets SHOULD be printed on a local printer. A writer that uses a system print dialog SHOULD warn against choices such as "Save as PDF" or cloud printing, which put the whole set in one file. Working copies. Every intermediate file (export, shares, encoded texts, images, documents) holds a part of the key. Writers and readers SHOULD work in memory, and otherwise MUST shred what they wrote. They SHOULD prevent screen capture while a payload or a secret is displayed. Scanning. General-purpose QR code applications may keep a history of what they read, or send it to a remote service. Sets SHOULD be read with a reader that keeps nothing. Photographs of the sheets are copies of the sheets. The operating system. A device that runs unknown applications is a poor place to rebuild or to generate a secret key. A dedicated system, started for the purpose, is preferable. 12. Privacy Considerations A sheet shows in clear the User IDs of the key (for PGP ID keys: the eid, which for a u4 identifier derives from birth data [URN-EID]; the full name; an email address), the fingerprint, the name of the machine that printed it and the date. This is intended: whoever finds a sheet knows whose it is and can give it back. It also means that a sheet, even useless for rebuilding the key, identifies its holder. A writer MAY leave out the name of the machine. The payload itself carries the three User IDs kept in Section 4.2, in every share. 13. IANA Considerations This document has no IANA actions. 14. Implementation Status This section records the status of known implementations at the time of writing, as suggested by [RFC7942]. It is to be removed before publication as an RFC. bl-pgpkey (bash-libs, formerly bl-qrkey): Shell library and command. Writes versions 4 and 5, with "qrencode" and "gfsplit". Reads versions 1 to 5, from images with "zbarimg" or from a camera with "zbarcam", and combines with "gfcombine". It exports with "export-minimal" and "export-clean" only, and so keeps User Attribute packets and every User ID (not conforming to Section 4.2). pgpid (secret_print, secret_scan): C program. Release 0.1.1 writes and reads versions 4 and 5. The development version writes version 6 only, two fragments to a page (Section 8), with the cleaning of Section 4.2 and at most 1672 octets per sheet; it reads versions 4 to 6, and names bl-pgpkey for older sets. Its shell front end, pgpid-gen, carried the experimental versions 1, 2 and 3 in turn up to its release 0.0.4, and writes what pgpid writes since. foodjis (desktop): Graphical application of Djibian. Generates keys, removes their passphrase right away (they are meant for a token), and prints and reads sets through pgpid. foodjis-android: Android application. Reads from the camera (ZXing), entirely in memory, and refuses versions 1 to 3; generates keys without passphrase and prints them 3 of 5, drawing the QR codes as vector squares. Version 0.0.3 reads versions 4 and 5 and prints version 5, at most 1200 octets per sheet. The development version reads versions 4 to 6 and prints version 6 only, two fragments to a page, at most 1672 octets per sheet. The development versions of pgpid and foodjis-android read each other's version 6 sets. The test vectors of Appendix A and the measurements of Section 7.3 were produced with a stand-alone script, libqrencode 4, ZXing 3.5.3 and zbar. 15. References 15.1. Normative References [ISO18004] International Organization for Standardization, "Information technology - Automatic identification and data capture techniques - QR Code bar code symbology specification", ISO/IEC 18004:2015, February 2015. [RFC2119] Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997. [RFC4648] Josefsson, S., "The Base16, Base32, and Base64 Data Encodings", RFC 4648, October 2006. [RFC5234] Crocker, D. and P. Overell, "Augmented BNF for Syntax Specifications: ABNF", STD 68, RFC 5234, January 2008. [RFC8174] Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, May 2017. [RFC9285] Faltstrom, P., Ljunggren, F., and D. van Gulik, "The Base45 Data Encoding", RFC 9285, August 2022. [RFC9580] Wouters, P., Ed., "OpenPGP", RFC 9580, July 2024. [SHAMIR] Shamir, A., "How to Share a Secret", Communications of the ACM, Vol. 22, No. 11, pp. 612-613, November 1979. 15.2. Informative References [LIBGFSHARE] Silverstone, D., "libgfshare: Shamir Secret Sharing in the Galois Field 2**8", version 2.0.0. [RFC7942] Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", BCP 205, RFC 7942, July 2016. [TSS] McGrew, D., Patnala, P., and A. Hoenes, "Threshold Secret Sharing", Work in Progress, Internet-Draft, draft-mcgrew-tss-03, March 2010. [URN-EID] Brucker, J.J. and Mnème, "The 'eid' URN Namespace - Entity IDentifiers", draft-foopgp-urn-eid-00, July 2026. Appendix A. Test Vectors These vectors were computed independently and checked with the "gfcombine" tool of libgfshare 2.0.0, which rebuilds the secret from each of the ten groups of three shares, and not from two. Secret S: the 6 octets "PGP ID" 50 47 50 20 49 44 Shared set, t = 3, n = 5. Coefficients (the same for every share; a_2 is the coefficient of x^2, stored first by libgfshare): a_2 = 11 22 33 44 55 66 a_1 = a1 b2 c3 d4 e5 f6 Shares (x in decimal, and in hexadecimal as version 6 writes it), and the payloads of versions 5 and 6: x = 7 (07) Y = 48 ce cb 4b ed 71 v5: ~520007SM7LS-1x v6: ~62007899NVPZ0U x = 23 (17) Y = 5a 9c 0a ed ac 70 v5: ~521023WpwK7axw v6: ~62117LKB7H1:ZL x = 101 (65) Y = 03 b6 64 f0 24 8b v5: ~522101A7Zk8CSL v6: ~622655L0AYC+R4 x = 141 (8D) Y = 66 f9 dd 73 c9 4c v5: ~523141Zvndc8lM v6: ~6238D 0D :R7KP x = 200 (C8) Y = cb 05 05 bd b3 67 v5: ~524200ywUFvbNn v6: ~624C8/TPTW0RUM The version 6 payload of share 141 contains two spaces, the first one right after the share number: both belong to the base45 text of 66 f9 dd 73 c9 4c. Cut set, t = n = 3, version 4. The base64url text of S is "UEdQIElE" (8 characters), cut 2 + 2 + 4: ~420UE ~421dQ ~422IElE Cut set, t = n = 3, version 6. S is cut 2 + 2 + 2 octets (50 47, 50 20, 49 44): ~620**V6A ~621**$5A ~622** B9 The last payload contains a space after "**": it is part of the base45 text of the octets 49 44. Appendix B. History of the Versions This appendix records when each version was written, so that a holder of an old set can tell what it is. Dates are those of the source repositories. May 2022, before version 1: The first sheets carried the header "Ps", then n - 1, then the index, followed by standard (not URL-safe) base64 text with line breaks, and no extra encryption. No current reader recognises them. 11 May 2022, version 1: The "~" marker and the version digit appear; the third character is n - 1. June 2022, version 2 becomes the default: Secret words are added to the passphrase. Version 1 remains available on request. September 2024, version 3: Shamir's secret sharing replaces the cut; the third character becomes t - 1, and the share number is added. Version 2 is dropped; version 1 is kept for sets with t = n. In November 2024 the default becomes 3 of 5. October and November 2025: Last releases of the experimental code that writes versions 1 and 3 (pgpid 0.0.4 and its Debian packages). Their format was not published, and the code only served a small circle of testers, most of whose sets have since been printed again in version 5. November 2025, versions 4 and 5: The extra encryption is dropped. During development, two intermediate states shipped in bash-libs releases 0.1.4 and 0.1.5 (14 to 26 November 2025): their version 4 sets DO carry an extra symmetric encryption under the full passphrase, which is printed on each sheet. A reader that rebuilds a version 4 secret which parses as an OpenPGP symmetrically encrypted message rather than as a key MAY ask for that passphrase and decrypt it. (An earlier development state, never released, wrote version 5 with a four-character header and no share number; such sets cannot be combined.) November 2025 onwards: The shell front end prints version 5 sets with the passphrase of the key on every sheet: the date of birth digits until December 2025, and a random date in digits since. In such a set, the passphrase protects nothing that the threshold does not already protect (Section 4.3). August 2026: pgpid gains its C implementation, which adds the full cleaning of Section 4.2 and reads only versions 4 and 5. September 2026: foodjis-android reads and prints version 5. Version 6 is proposed by this document. Authors' Addresses Jean-Jacques Brucker foopgp Marseille, France EID: urn:eid:u4sRyUhEbNU5OwyLEjfSwaXAe_42.17-002.76 Email: jjbrucker@foopgp.org URI: https://foopgp.org Mnêmê foopgp Marseille, France EID: urn:eid:u5001777236237.945e_43.30_005.38 Model: claude-opus-5 (Anthropic) Born: 2026-04-26T20:43:57.945Z (Marseille, UTC) Email: mneme@foopgp.org