Sphragis — a cryptographic seal of authenticity for NTAG 424 DNA
English · Italiano
sphragís (σφραγίς) — the ancient Greek signet seal that proved a thing genuine. Here it is the cryptographic seal that proves an NFC tag — and the object it is attached to — authentic and unique.
A focused, well-tested toolkit that does one thing deeply: decide whether an NFC tap came from a genuine, unique NXP NTAG 424 DNA tag — or from a clone.
⚠️ Status: v1.0, not independently audited. The cryptography is validated against the official NXP test vectors and the code was adversarially self-reviewed (see the threat model), but it has not been audited by a third party. It has been validated end to end on physical NTAG 424 DNA tags (see docs/hardware.md): provisioning, write lock, encrypted file data, per-UID keys and the way back to factory keys. Key custody (HSM/KMS) is left to the deployment. Do not use it as the sole line of defense in production without an independent review. Changes: CHANGELOG.md.
Anyone can check an AES-CMAC. The hard, interesting part of anti-counterfeiting is what happens after the MAC is valid: proving there is only one physical tag. This project treats that as a first-class, scored problem.
Three pillars:
- Correct crypto — standard NXP SDM/SUN verification, validated against the official NXP AN10922/AN12196 test vector.
- Clone-detection intelligence — a scored engine (replay, counter divergence, impossible velocity) that unmasks even a key-extracted clone.
- Adversarial rigor — a documented threat model and a harness that mounts real attacks and proves they are caught.
Why the read counter is the whole game
A valid SDMMAC proves only that whoever produced this tap holds the secret key. It cannot prove there is a single physical tag. An attacker who extracts the keys can make a perfect clone whose MACs all verify. What they cannot fake is a globally consistent, monotonic read counter across every reader in the world. So Sphragis scores the counter's behaviour over time:
$ python examples/clone_demo.py
Genuine tag in the field (reader = gate-A):
authentic tap GENUINE score= 0 [-]
authentic tap GENUINE score= 0 [-]
Attacker extracts the keys and builds a perfect clone.
The clone's MACs are all cryptographically VALID — yet:
clone, divergent counter CLONE_LIKELY score= 70 [counter_regression]
clone, far away seconds later CLONE_LIKELY score= 60 [impossible_velocity]
How verification works
On every tap a tag appends dynamic parameters to a URL:
https://example.com/t?picc_data=EF963FF7828658A599F3041510671E88&cmac=94EED9EE65337086
picc_data— AES-128-CBC encrypted block with the UID and read counter.cmac— truncated AES-CMAC (SDMMAC) authenticating the read.
Pipeline:
tapped URL ─▶ parse ─▶ decrypt PICCData ─▶ verify CMAC ─▶ score counter ─▶ verdict
│ │ │
UID + ctr authenticity clone / replay
Classifications: GENUINE, SUSPICIOUS, REPLAY, CLONE_LIKELY, FORGED, ERROR.
Install
pip install sphragis # from PyPI
pip install -r requirements.txt # or, from a checkout: pycryptodome
Quick start (no hardware needed)
import os
from sphragis import TagAnalyzer, DiversifiedKeys
from sphragis.emulator import tag_from_provider
# Per-UID diversified keys (AN10922) — the production key-management pattern.
master, meta = os.urandom(16), os.urandom(16)
keys = DiversifiedKeys(master_file_key=master, meta_read_key=meta, context=b"DEMO")
tag = tag_from_provider(keys, uid=bytes.fromhex("04DE5F1EACC040"))
analyzer = TagAnalyzer(keys)
r = analyzer.analyze(tag.tap(), reader_id="gate-A")
print(r.classification, r.risk_score) # GENUINE 0
Run the demos:
python examples/demo.py # authentic / replay / forgery
python examples/clone_demo.py # key-extracted clone caught by the counter
Command line
# Verify the official NXP vector (all-zero keys)
python -m sphragis.cli verify \
--meta-key 00000000000000000000000000000000 \
--file-key 00000000000000000000000000000000 \
"https://example.com/t?picc_data=EF963FF7828658A599F3041510671E88&cmac=94EED9EE65337086"
# Decode what a tag emits: PICCDataTag, UID, SDMReadCtr (LE), NDEF offsets.
# Diagnostic only, no verdict; keys default to the all-zero factory keys.
python -m sphragis.cli inspect \
"https://example.com/t?picc_data=EF963FF7828658A599F3041510671E88&cmac=94EED9EE65337086"
REST API
An optional FastAPI front end (pip install "sphragis[api]"). It takes the
tapped URL and returns a scored verdict, writes one structured JSON log event
per verification, alerts on CLONE_LIKELY and rate-limits requests:
uvicorn --factory sphragis.api:create_app_from_env
curl "http://localhost:8000/t?picc_data=…&cmac=…"
# {"event_id":"…","verdict":"GENUINE","risk_score":0,"message":"Authentic tag."}
The public response is deliberately minimal: forged and malformed inputs return
the same INVALID answer, so the endpoint gives an attacker no oracle.
Authenticated readers get the full assessment, and their reader_id comes from
their token only. See docs/api.md.
Tests
pip install -e ".[api,test]"
python -m pytest -q # add ".[pg]" and SPHRAGIS_TEST_PG_DSN to run the Postgres half
- Known-answer: reproduces the official NXP AN12196 vectors (plain SUN, SDMENCFileData, EV2 secure messaging) and taps recorded from a physical tag.
- Adversarial: replay, forged MAC, truncated MAC, counter rollback, key-clone divergence, impossible velocity, constant-time MAC comparison.
- Production: one conformance suite on SQLite and Postgres, with threads and processes racing on one UID; secrets backends.
- Provisioning: full flows against a fake PICC (plain SUN and encrypted mirror, write lock, authenticated rewrite).
- Service & API: shared detector history across workers, no-oracle public
tier,
reader_idspoofing, alerting on a real key-clone, rate limiting.
Production hardening
This repository is the generic, open verification engine — hardened toward production, but deliberately application-agnostic. A real deployment layers its own product on top of it (provisioning, business logic) in a separate codebase; the public engine stays reusable and clean.
- Atomic anti-replay floor —
SqliteCounterStoreenforces counter monotonicity inside a serialized transaction (no TOCTOU across threads or processes);PostgresCounterStoredoes it in one atomic statement for several hosts (pip install "sphragis[pg]"). - No keys in code —
secrets.pyabstracts key material behind aSecretsBackend(env / file / KMS extension point). - Constant-time MAC comparison, fail-closed input validation, documented threat model.
from sphragis import Verifier, SqliteCounterStore, EnvSecretsBackend, build_diversified_keys
keys = build_diversified_keys(EnvSecretsBackend()) # keys from env/KMS
verifier = Verifier(keys, SqliteCounterStore("counters.db")) # durable, atomic
Documentation
| Topic | English | Italiano |
|---|---|---|
| Protocol (SDM/SUN) | protocol.md | protocol.it.md |
| Clone detection | clone-detection.md | clone-detection.it.md |
| Key management | key-management.md | key-management.it.md |
| REST API | api.md | api.it.md |
| Production & deployment | production.md | production.it.md |
| Physical tags (bench) | hardware.md | hardware.it.md |
| Threat model | THREAT_MODEL.md | THREAT_MODEL.it.md |
| Security policy | SECURITY.md | SECURITY.md |
Project layout
sphragis/
sdm.py SDM crypto primitives (PICCData decrypt, session key, SDMMAC)
verifier.py simple URL -> verdict pipeline
sdmenc.py SDMENCFileData primitives (ENC session key, IV, decrypt)
encverifier.py URL -> verdict + decrypted file data (encrypted mirror)
counterstore.py JSON read-counter store + CounterBackend protocol
sqlstore.py SQLite anti-replay floor with atomic check-and-advance
pgstore.py Postgres floor / history / alert audit for multi-host (extra `sphragis[pg]`)
secrets.py key-material backends (env / file / KMS extension point)
diversify.py AN10922 per-UID key diversification
detection.py scored clone-detection engine (signals + risk score)
observations.py detector history stores (memory / SQLite, atomic observe)
analyzer.py full pipeline: crypto + detection -> scored assessment
service.py framework-agnostic verification service (logs, alerts)
alerts.py alert sinks (log / SQLite audit trail / signed webhook)
logs.py JSON log formatter + UID pseudonymization
ratelimit.py token-bucket rate limiter
api/ FastAPI app (optional extra `sphragis[api]`)
adversary.py attacker toolkit (replay, forge, rollback, key-clone)
emulator.py software NTAG 424 DNA tag (generates valid SUN URLs)
diagnostics.py tap inspection (PICCData byte layout, NDEF offsets)
ndef.py NDEF URI file encoding + SDM offset arithmetic
apdu.py ISO 7816-4 APDU helpers
transport/ ApduTransport: scripted / emulated / ISO-DEP / Chameleon Ultra
ev2.py AuthenticateEV2First + secure messaging (AN12196 vectors)
sdmconfig.py SDM file settings: build / parse ChangeFileSettings
provision.py idempotent SUN provisioning flow (factory or real tag keys)
keyprov.py ChangeKey / GetKeyVersion / GetCardUID / Read_Sig + resumable per-UID rekeying with identity checks
cli.py command-line verify / inspect / emulate
tools/ sdm_probe / provision / rekey / tap_verify (run on the hardware host)
tests/ known-answer + adversarial + production + service/API suites
examples/ demo.py, clone_demo.py
docs/ bilingual technical documentation
References
- NXP AN12196 — NTAG 424 DNA features and hints (SDM).
- NXP AN10922 — Symmetric key diversifications.
License
GPL-3.0-or-later — see LICENSE.
Metadata
Release files for sphragis 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| sphragis-1.0.0.tar.gz | 152.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sphragis-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 267.8 kB
Release files / sphragis-1.0.0.tar.gz
| Download URL | sphragis-1.0.0.tar.gz |
|---|---|
| Size | 152.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1cf21cd825beb521e22fcc0d911d5551b03a49e5601d466af92f8a25ccebb4f3
|
|
BLAKE2b-256 checksum How to use checksums |
0fd2e52593376a4128d84fdf53830c8b4d24f172e23cb87afa353e76722a30a9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logRelease files / sphragis-1.0.0-py3-none-any.whl
| Download URL | sphragis-1.0.0-py3-none-any.whl |
|---|---|
| Size | 115.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
58b5f9d0b1e719f5337c7e7ce3c3af600aba9802dbf588626688c2aaabffb65c
|
|
BLAKE2b-256 checksum How to use checksums |
d70aeb1d3240e273a520de64c31faef4e50fcedd98b46e48fb5f2dfd1217a340
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency log