Issuing documents
An organisation issues stamped documents by publishing a public key in the DNS of its domain and signing the key facts of each document with the corresponding private key. This guide describes each step, from generating the key to revoking it.
Overview#
Issuing stamped documents requires control over the DNS records of a domain and software that creates stamps, such as dkds.js or another conforming implementation. The work falls into two parts. Setting up a key is done once per key and involves the DNS. Creating a stamp is done for each document and requires no network access.
| Step | Frequency | Section |
|---|---|---|
| 1. Choose a key ID | once per key | Choosing a key ID |
| 2. Generate a key pair | once per key | Generating a key |
| 3. Publish the key record in the DNS | once per key | Publishing the key record |
| 4. Decide which fields each type of document carries | once per document type | Choosing the fields |
| 5. Create a stamp | for each document | Creating stamps |
| 6. Print the stamp on the document | for each document | Printing |
Keys are later replaced as a matter of routine, and revoked if they may have been compromised. Both are described at the end of this guide.
The examples on this page use the domain example.com, the key ID k2026 and the command-line
tool of dkds.js, which is installed with npm install --global dkds.js.
Choosing a key ID#
Every key of a domain has a key ID: 1 to 16 characters from a to z and 0 to 9. The key ID is
part of every stamp and of the DNS name at which the key is published. It is chosen by the issuer
and has no meaning to verifiers.
- A key ID identifies one key permanently. It MUST NOT be reused for a different key, because changing the key behind a key ID invalidates every stamp signed with the old key (requirement 10.2).
- Common schemes are the year in which the key is introduced (
k2026) or the issuing system and the year (inv2026,hr2026). Separate keys for separate systems limit the effect of a compromise: revoking one key leaves the stamps of the other keys valid. - The key ID can be read by anyone and should not contain confidential information.
Generating a key#
The keygen command generates an Ed25519 key pair, writes the private key to a file and prints the
key record:
dkds keygen --out k2026.pemv=DKDS1; k=ed25519; p=FcrcpGSk4rhUAqlnJVa/U6sDcz3BPDNjBUZ77jkz07w=The private key is written in PKCS#8 PEM format (RFC 8410), readable only by its owner on systems with file permissions. The command does not overwrite an existing file. Keys generated with OpenSSL are equally suitable:
openssl genpkey -algorithm ed25519 -out k2026.pemA key generated for DKDS MUST NOT be used for any other purpose (requirement 10.1), such as TLS or code signing.
Publishing the key record#
The public key is published as a TXT record at the name <key ID>._dkds.<domain>. The record
command prints the complete record for a private key:
dkds record --key k2026.pem --domain example.com --key-id k2026k2026._dkds.example.com. TXT "v=DKDS1; k=ed25519; p=FcrcpGSk4rhUAqlnJVa/U6sDcz3BPDNjBUZ77jkz07w="In the management interface of a DNS provider, the record is entered as follows:
| Setting | Value |
|---|---|
| Type | TXT |
| Name | k2026._dkds, or k2026._dkds.example.com where the interface expects the full name |
| Value | v=DKDS1; k=ed25519; p=FcrcpGSk4rhUAqlnJVa/U6sDcz3BPDNjBUZ77jkz07w= |
| TTL | the provider's default |
The value is shorter than 255 characters and fits in a single character string. Some interfaces add the surrounding quotation marks themselves and others expect them to be typed.
Exactly one record beginning with v=DKDS1 may exist at the name. A second such record, for
example one left over from an earlier attempt, makes the key unusable, and every stamp is then
reported as key-not-found (section 7.3). Other
TXT records at the same name are ignored.
Checking the record#
With --check, the record command retrieves the published record and compares it with the key.
Its exit status is 0 when they match and 1 otherwise:
dkds record --key k2026.pem --domain example.com --key-id k2026 --checkThe record can also be inspected with standard DNS tools, for example
dig +short TXT k2026._dkds.example.com.
A resolver that looked up the name before the record existed may keep that negative answer for the negative-caching time of the zone (RFC 2308), commonly between a few minutes and an hour. The record is therefore published, and checked, before the first stamp signed with the key is distributed.
DNSSEC#
When the zone of the domain is signed with DNSSEC, verifiers can confirm that the key record was not forged in transit, and report the answer as validated (section 9.1). DKDS works without DNSSEC; verifiers then report that the answer was not validated. Most DNS providers offer DNSSEC as a setting of the zone.
Delegation to a service provider#
The key record MAY be reached through a CNAME record
(section 7.1). An issuer that uses an external service to create
stamps can point k2026._dkds.example.com to a name managed by that service. The service then
controls which key is valid for the domain under that key ID.
Choosing the fields#
The fields are the facts of the document that the stamp protects. Each field is a name and a value, both text, and the verifier displays them in the order given.
| Document | Example fields |
|---|---|
| Invoice | Supplier, Invoice no., Date, Customer, Amount, Pay to IBAN, Due |
| Payslip | Employer, Employee, Employee no., Period, Gross pay, Net pay |
| Certificate | Institution, Holder, Certificate no., Qualification, Awarded |
The following rules make the comparison between stamp and document meaningful:
- Subject and document. Each stamp SHOULD contain at least one field that identifies the subject of the document, such as a name, and one that identifies the document, such as a number. Without them, a valid stamp can be copied onto another document unnoticed (requirement 10.4).
- Values as printed. Values SHOULD be written exactly as they are printed on the document, so
that the reader can compare them character by character
(requirement 10.5). An amount printed as
4,812.50 EURis stated in the stamp as4,812.50 EUR, not as4812.5. - Names in the language of the document. The reader sees the names as written. They are presented as statements of the issuer, separately from the verifier's own statements.
- No confidential data. Anyone who scans a stamp can read every field. A stamp contains only facts that are also printed on the document.
- One line per value. Control characters, including line breaks and tabs, are not permitted (Appendix B). An address is written on one line, with its parts separated by commas.
Size#
The envelope is limited to 1,088 bytes. Of these, 73 bytes plus the length of the domain and of the key ID are taken by the header and the signature; the rest is available for the compressed fields.
| Domain | Key ID | Available for the compressed fields |
|---|---|---|
example.com (11 characters) |
k2026 (5 characters) |
999 bytes |
| a domain of 30 characters | 16 characters | 969 bytes |
Short field lists compress little. The seven fields of the
example in Appendix C occupy 172 bytes, and 155 bytes after
compression. In practice, field lists of up to several hundred characters fit comfortably. dkds.js
reports the error too-large when the fields do not fit.
Creating stamps#
A stamp is created from the fields, the domain, the key ID and the private key. dkds.js offers three interfaces, which produce identical stamps.
Library#
In JavaScript, createStamp returns the text form and the QR code as SVG:
import { readFileSync } from "node:fs";
import { createStamp } from "dkds.js";
const stamp = await createStamp({
domain: "example.com",
keyId: "k2026",
privateKey: readFileSync("k2026.pem", "utf8"),
fields: [
["Invoice no.", "2026-0917"],
["Customer", "Harbour Logistics B.V."],
["Amount", "4,812.50 EUR"],
],
});
stamp.text; // "DKDS:..."
stamp.svg; // the QR code, 0.33 mm per module, with its quiet zoneCommand line#
dkds create --key k2026.pem --domain example.com --key-id k2026 \
--field "Invoice no.=2026-0917" --field "Amount=4,812.50 EUR" --svg stamp.svgThe command prints the text form and writes the QR code to stamp.svg. With --fields fields.json,
the fields are read from a JSON file containing a list of [name, value] pairs.
Local HTTP API#
Software written in other languages can create stamps through a local HTTP service. The service
holds the key and listens on 127.0.0.1:8445:
dkds serve --key k2026.pem --domain example.com --key-id k2026curl -s http://127.0.0.1:8445/create \
-H "Authorization: Bearer $DKDS_TOKEN" -H "Content-Type: application/json" \
-d '{"fields": [["Invoice no.", "2026-0917"], ["Amount", "4,812.50 EUR"]]}'The response contains text and svg. The token is read from the environment variable
DKDS_TOKEN, or generated and printed when the service starts. The
dkds.js documentation describes the service in full.
Time of issue#
Each stamp records the time at which it was signed. The clock of the issuing system must be correct: a verifier rejects a stamp whose time of issue lies more than 600 seconds in its future (section 8.2, step 5).
Printing#
A stamp is printed black on white, at a module size of at least 0.33 mm, with a quiet zone of four modules on every side (requirement 10.6). The SVG produced by dkds.js has exactly these dimensions and is placed at 100 percent scale; it may be enlarged but not reduced.
| Envelope | QR version | Printed width, including the quiet zone |
|---|---|---|
| 250 bytes (Appendix C) | 12 | 2.4 cm |
| 640 bytes | 20 | 3.5 cm |
| 1,088 bytes (maximum) | 27 | 4.4 cm |
- The stamp is placed where it is not folded, perforated or covered, and with clear space around the quiet zone.
- In PDF documents the stamp is embedded as a vector graphic. It remains verifiable on screen and when the PDF is printed.
- A document may carry several stamps, each verified independently (section 3.3).
Before the first documents are sent, a test stamp is checked with an independent verifier, such as
dkds verify or scan.dkds.org. The outcome must be valid, and the fields
must appear as intended.
Storing private keys#
Whoever holds the private key can sign stamps in the issuer's name, with any time of issue. The private key is therefore stored with the same care as other signing credentials:
- On the issuing system, as a file readable only by the account that creates stamps, and not in source control.
- In a hardware security module or a key management service, where the key never leaves the device. dkds.js uses such a key through a signer, a function that returns the signature of a message:
const stamp = await createStamp({
domain: "example.com",
keyId: "k2026",
signer: {
publicKey, // 32 bytes
sign: async (message) => kms.sign(message), // returns the 64-byte Ed25519 signature
},
fields,
});Verification needs only the public key, so the loss of a private key does not affect stamps already issued. A new key with a new key ID takes its place. Backups of a private key are therefore not required for verification, and each backup is a further copy that can be stolen.
Replacing a key#
Keys are replaced periodically, for example once a year. Short-lived keys limit the number of legitimate stamps that are affected if a key must later be revoked.
- Generate a key with a new key ID, for example
k2027. - Publish its record and check it.
- Configure the issuing software to sign with the new key.
- Keep the record of the old key published for as long as documents signed with it are expected to be verified (requirement 10.3). For invoices, this is commonly the statutory retention period.
- Destroy the old private key once it is no longer used for signing. Its public record stays.
Removing an old record makes every stamp signed with it unverifiable: verifiers report
key-not-found, and verifiers that remember keys warn that the key has disappeared.
Revoking a key#
A key that may have been compromised is revoked by replacing its record with one whose p tag is
empty (section 7.4):
dkds record --revoke --domain example.com --key-id k2026k2026._dkds.example.com. TXT "v=DKDS1; k=ed25519; p="After revocation, every stamp signed with the key is reported as key-revoked, including stamps
signed before the revocation. This is unavoidable: a holder of a stolen key can choose any time of
issue, so stamps made before and after the theft cannot be told apart. Documents that must remain
verifiable are issued again with a new key.
A revoked record is kept in place rather than deleted. Readers then see that the key was revoked, rather than that no key was found. The key ID of a revoked key is not used again.
Checklist#
Before the first stamped document is issued:
- The key ID is new for the domain and contains nothing confidential.
- The private key is stored with restricted access and is used for nothing but DKDS.
- Exactly one
v=DKDS1record is published at<key ID>._dkds.<domain>, anddkds record --checkconfirms it. - Each type of document has fields that identify its subject and the document itself, written as printed, and containing nothing that is not printed on the document.
- Stamps are printed black on white, at 0.33 mm per module or larger, with a quiet zone of four modules.
- A test stamp verifies as
validwith an independent verifier. - The procedure for revoking the key is documented, together with the people who may carry it out.