ArtFirma

Version 1 · for anyone writing their own verifier

Tag specification

Everything needed to verify an ArtFirma tag without any of our software or servers.

What is on the tag

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>
PartWritten byMeaning
uthe chip, on every read (SUN mirror)chip serial number, 7 bytes, 14 upper-case hex
cthe chip, on every readread counter, 3 bytes big-endian, 6 hex; grows by one per read
mthe chip, on every readSUN CMAC, 8 bytes, 16 hex (see "Chip code")
1the artist, onceformat version
workthe artist, oncework number, [A-Za-z0-9-]{1,64} — usually the artwork's slug in the artist's shop
hashthe artist, oncefirst 16 bytes of SHA-256 of the catalogue record file, base64url without padding (22 chars); - if the tag is not bound to a record
sigthe artist, onceEd25519 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.

Checking the artist's signature

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.

Checking the catalogue record

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.

Artist's signature on the record itself

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.

Permanent archive (Arweave)

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.

Artists who registered themselves

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:

  • the artist sent {"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);
  • the site carried the exact line 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>.

Chip code (optional online layer)

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.

  • Tag key, version 1: 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.
  • Session key: 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=.

Chip authenticity

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

Where the artist's keys come from

The artist's keys are derived from a 12-word phrase, using published wallet standards only:

  • words → seed: BIP-39 (English wordlist, empty passphrase);
  • seed → Ed25519 signing key: SLIP-0010, path m/7365'/1'/<index>' (index 0 = first key);
  • seed → tag masters: SLIP-0021, labels "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).

Physical limits

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.