EdgeNFC
Security
A QR code is an image: photograph it and you have it. A static NFC tag is a string: read it and you have it. An EdgeNFC tag holds a key it will not disclose, and uses that key to sign a message that is different on every tap.
The claim, stated precisely. Forging a tap requires the tag's AES key. Replaying a captured tap fails a counter check. Neither is a matter of obscurity — both are the ordinary properties of a MAC and a monotonic counter.
Key hierarchy
The master key exists so that per-tag keys can be derived from it. It is never written to a tag, and it is not needed to answer a tap — only to re-derive the key of the tag that produced it.
Each tag's key is derived from your System Master Key and data that includes the chip's own 7-byte UID, using NXP's AN10922 AES-128 diversification. Two tags from the same master never share a key.
An attacker who successfully extracts a key from a chip in a lab learns that chip's key. The derivation is one-way: it does not run backwards to the master, and it says nothing about any other tag in the system.
The key that encrypts the tag's identity (SDMMetaReadKey) and the key that signs the tap (SDMFileReadKey) are derived with different role labels, so the two capabilities are not interchangeable.
SDM is a read operation. The chip performs the cryptography internally and emits the result; the key itself is never part of the NDEF payload and never appears in the URL a phone opens.
Threat model
Each row is a specific attack and the specific mechanism that answers it — not a posture statement.
Every tap carries an AES-CMAC computed with that tag's diversified key. Producing a code that verifies means possessing the key; copying the chip's memory does not produce it.
The chip's read counter is signed into the message and only increases. A URL captured from a real tap carries a counter we have already seen, and is rejected even though its signature is valid.
In encrypted-PICC mode the UID and counter are AES-encrypted into one opaque parameter, so the URL does not expose a stable identifier to anyone who reads it over your customer's shoulder.
Hosted master keys are envelope-encrypted under a KMS-held wrapping key and exist in plaintext only transiently in memory at verify time. No API returns key bytes after creation, and keys are never written to logs or error messages.
MAC comparison is constant-time with no early exit, so the time a rejection takes tells an attacker nothing about how close the guess was.
The expected SDM mode and key version are pinned per system server-side. A tag that presents a weaker configuration than the system expects does not get a pass because it asked for one.
Key custody
Custody is a decision you make, not a tier we assign. All hosted plans use hosted custody; the perpetual Edge License is how you take custody yourself.
Held in Android Keystore-wrapped storage, used in-process to derive per-tag keys, and zeroized after each provisioning operation. Never logged, and never exported in plaintext except by an explicit action you take.
Stored envelope-encrypted: a KMS-held wrapping key encrypts the master key at rest. Plaintext exists only transiently in memory at verify time and is then zeroized.
With the Edge License, the master key is shown to you once at creation and is never persisted on our side. The self-host package ships public configuration only — it contains no key.
Primitives
The only thing we designed is how these pieces are composed. The pieces themselves are standard, and implemented with vetted libraries.
All ciphers are AES-128; all MACs are AES-CMAC as specified in NIST SP 800-38B. Key generation uses a cryptographically secure RNG.
Implemented to NXP's published specification and checked against NXP's published test vector — including its deliberate divergence from generic CMAC, which is a classic place for an implementation to be subtly and silently wrong.
Every tag and MAC comparison is constant-time. Derived and session keys are zeroized after use. The core returns non-secret error values and does not panic on attacker input.
Hosted and self-hosted run the same Rust/WASM build, and the Kotlin provisioning side is held to the same test vectors as the Rust verification side, so the two ends of the system cannot quietly disagree.
Privacy
Encrypted-PICC mode — the recommended configuration — keeps the tag's UID out of the URL entirely, so a scan URL is not a durable identifier for the object it is attached to.
Per-tap verification URLs are transient and are excluded from indexing and from our sitemap, so an individual tap cannot end up in a search result.
Checking whether a product is genuine is the function of the page, so it runs without a cookie banner standing in the way. Nothing non-essential loads before you consent. See the Privacy Policy.
The verification event log retains outcomes and counters so you can see what happened. It never contains key material.
Straight answers
Not yet. We have not commissioned a third-party audit or obtained any security certification, and we will say so here until that changes. What we can point at today is the design above, standard primitives, and conformance against published NXP vectors.
It is implemented in the verification core and covered by round-trip tests. Our published conformance vectors currently cover the plaintext-mirror mode end to end; treat encrypted-PICC as supported and tested, but not yet independently vector-validated against NXP AN12196.
Rotation changes the key every field tag was provisioned against. Today, tags provisioned under the old key need re-provisioning to verify again — plan a rotation as a re-provisioning exercise, not as a switch you flip. A rolling migration window is on the roadmap and is not shipped.
It proves the tag is genuine, present, and not replayed. Whether that transfers to the item depends on how the tag is attached — a tag that can be peeled off a real item and stuck on a fake one proves only that the tag is real. Design the attachment as carefully as the crypto.
Under self-hosted custody, no — that is the point of it. The key is shown once and never persisted by us, so a lost key means re-provisioning the tags. Back it up accordingly before you provision at scale.
Next
The demo runs the real verification core in your browser. Change a byte of the MAC, or reuse a counter, and watch it refuse.