Version 1 · for anyone writing their own verifier
Everything needed to verify an ArtFirma tag without any of our software or servers.
An NXP NTAG 424 DNA chip on the artwork holds one NDEF URI record:
https://purl.org/artfirma/v?u=<UID>&c=<CTR>&m=<CMAC>#1.<work>.<hash>.<sig>
| Part | Written by | Meaning |
|---|---|---|
u | the chip, on every read (SUN mirror) | chip serial number, 7 bytes, 14 upper-case hex |
c | the chip, on every read | read counter, 3 bytes big-endian, 6 hex; grows by one per read |
m | the chip, on every read | SUN CMAC, 8 bytes, 16 hex (see "Chip code") |
1 | the artist, once | format version |
work | the artist, once | work number, [A-Za-z0-9-]{1,64} — usually the artwork's slug in the artist's shop |
hash | the artist, once | first 16 bytes of SHA-256 of the catalogue record file, base64url without padding (22 chars); - if the tag is not bound to a record |
sig | the artist, once | Ed25519 signature (RFC 8032), 64 bytes, base64url without padding (86 chars) |
Older tags may point to https://verify.d.123automate.it/?u=… instead of the purl address. The data is the same.
The signed message is the UTF-8 string:
NFCSIGN1|<UID>|<work>|<hash>
<UID> is the 14 upper-case hex characters from u. The signature is valid if Ed25519 verification of sig over this message succeeds with one of the artist's public keys (keys.json, 32-byte keys in hex). An entry with revoked is a key withdrawn as an impostor's: treat its signatures as invalid.
A valid signature means: the artist declared that the chip with this serial number is this work, bound to this record. Copying the part after # to another chip fails, because the other chip reports a different u.
The record is the file r/<work>.json published next to the verification page (and in archived copies), or <registry>/r/<public key hex>/<work>.json in the online registry (records are kept per artist key: work numbers are the artist's own). SHA-256 of its exact bytes, truncated to 16 bytes and base64url-encoded, must equal hash. A file that is not JSON (e.g. a host serving its start page for a missing file) is "no record here", not a mismatch.
The record is a JSON object. Fields, in this order when present: v (1), id (= work), artist, title, year, series, artform, medium, size, notes, statement, photo (file name of the main photo, next to the record), photos (list of {file, sha256, width, height} — SHA-256 of each photo file, lowercase hex), source ({url, product_id, imported, description_html} — where the data was taken from). Show a photo only if its bytes match its sha256. Compare the fields and the photo with the physical work.
A published record also carries a detached signature by the artist, so any copy of the registry can be checked without the tag: file <work>.json.sig next to the record, one line <public key hex>.<signature base64url>. The signature is Ed25519 over the UTF-8 string
NFCSIGN-RECORD1|<work>|<SHA-256 of the record file, lowercase hex>
with one of the keys in keys.json. The record lists its photos with their SHA-256, so the signature covers them too. index.json lists the records: {"v": 1, "records": [{"artist": <public key hex>, "id": <work>, "fingerprint": <hash>}]}. A published record never changes: the registry refuses different bytes under the same artist key and work number.
Every registry file is also stored on Arweave as a separate data item, findable through any Arweave gateway's GraphQL by its tags: App-Name: ArtFirma, Type: record | record-sig | photo, Artist: <public key hex>, Work: <work>, Fingerprint: <hash> (record and its signature), Sha256: <lowercase hex of the file>, File (photos). To find a tag's record with no other service alive: query Type=record, Fingerprint=<hash from the tag>, download it from the gateway, and check its SHA-256 against hash as above; the photo is Type=photo, Sha256=<sha256 from the record>. Gateways are not trusted: only bytes matching the fingerprint count.
Besides keys added by hand, keys.json holds keys that artists registered themselves (entries with site). Such a key is vouched for by the artist's own website, checked once, at registration:
{"v":1,"name":…,"site":<domain>,"pub":<key hex>,"sdm":<chip-code master hex>,"ts":<unix ms>} signed with that key (Ed25519 over NFCSIGN-ARTIST1| + the JSON text);artfirma-key=<key hex> in a DNS TXT record _artfirma.<domain>, or on https://<domain>/pages/artfirma, /.well-known/artfirma.txt or /artfirma.txt.name is chosen by the artist; the domain is what was checked — show the domain first. The check happens once, at registration (the registry keeps where and how the line was found); the artist may remove the line afterwards. Keys withdrawn later are kept in keys.json with revoked: <date>.
m proves that the URL was produced by the genuine chip just now. It needs the secret tag key, so only the artist's online service can check it. A copied link keeps its old counter, so a service that remembers the last counter it saw can detect it.
The chip computes m with its key slot 1 (SDMFileRead = 1). Slots 0, 2–4 hold different keys that allow rewriting the tag; they are never given to the online service.
AES-CMAC(master, 0x01 || UID || "NFCSIGN1"), where master is a 16-byte AES key: for slot 1 the artist's *chip-code master*, for slots 0, 2–4 the artist's *admin master* (tags signed before 2026-10-06 used one master for all slots). Version N ≥ 2: AES-CMAC(master, 0x01 || UID || "NFCSIGN1" || N). The version is the chip's key version (GetKeyVersion); every re-signing of a tag moves to the next version, because re-enabling SUN resets the chip's read counter. A link made with an older key version was taken before the tag was re-signed.AES-CMAC(tagKey, 3C C3 00 01 00 80 || UID || CTR as 3 bytes little-endian).m = bytes 1, 3, 5, … 15 of AES-CMAC(sessionKey, "u=<UID>&c=<CTR>&m=").This is the NXP NTAG 424 DNA SUN MAC (NXP AN12196) over the URL fragment between u= and m=.
NXP signs each chip's UID at the factory (originality signature, ECDSA secp224r1, NXP AN12196). It is read with the chip's Read_Sig command (NFC reader app, not a browser). The NTAG 424 DNA public key is:
048A9B380AF2EE1B98DC417FECC263F8449C7625CECE82D9B916C992DA209D68422B81EC20B65A66B5102A61596AF3379200599316A00A1410
The artist's keys are derived from a 12-word phrase, using published wallet standards only:
m/7365'/1'/<index>' (index 0 = first key);"NFCSIGN" / "tag admin key" (slots 0, 2–4) and "NFCSIGN" / "tag sdm key" (slot 1, chip code), first 16 bytes each; "NFCSIGN" / "tag master key" was the single master of earlier tags.The public keys in keys.json are the only thing a verifier needs (with revoked and site as described above).
A tag proves what the artist signed about a chip. It cannot prove that the chip was not moved to another object. The record (description, photo) and the paper certificate are there so a person can compare them with the work itself.