"Tap to see if it's real" — that's the experience consumers expect from our tags. But when they tap, they might be in a basement warehouse, at cruising altitude, or in a small pharmacy in a remote town: zero to one bar of signal. If your verifier app fails or hangs at that moment, the experience users get is not "reassurance" — it's "frustration." And frustrated customers eventually stop trusting the brand.
That is why VeriScan SDK is offline-first by design: it performs better online, but the vast majority of verifications still complete locally without a network round trip. This post is how we do it.
The problem with the naive approach
The straightforward design is: every scan posts UID + CMAC to the cloud and waits for a 200 OK. That looks fine on stage. It has two failure modes that bite in the field:
- No network. User gets a timeout and never learns if the product is authentic.
- Cloud incident. A single regional outage breaks verification globally.
The solution: a cached key bundle plus local CMAC verification
The elegant property of NTAG 424 DNA is that CMAC verification needs only (UID, counter, master key, CMAC from tag). If we can put the derived key for that UID on the device ahead of time, verification can be entirely local — zero network round trips.
Our approach: at SDK startup or during a background sync we deliver an encrypted key bundle. The bundle contains derived keys for a customer's relevant SKU subset, scoped to a 24–72 hour validity. For the customer, the bundle stays in memory and protected storage. For an attacker, it's an opaque encrypted blob bound to the device's hardware keystore.
// Pseudocode of the verify path
let uid = tag.uid
let counter = tag.counter
let cmacFromTag = tag.cmac
// 1. Resolve key from local bundle (no network)
let derivedKey = bundle.keyFor(uid)
if derivedKey == nil {
// Unknown UID -> fall back to network verify
return await network.verify(tag)
}
// 2. Recompute CMAC locally
let expected = aesCmac(key: derivedKey, data: uid + counter)
// 3. Constant-time compare
guard constantTimeEqual(expected, cmacFromTag) else {
return .suspect(reason: .cmacMismatch)
}
// 4. Replay protection: counter must advance
let lastSeen = localCounterStore.get(uid) ?? 0
guard counter > lastSeen else {
return .suspect(reason: .replay)
}
localCounterStore.set(uid, counter)
// 5. Queue event for later upload
eventQueue.append(VerifyEvent(uid, counter, .authentic))
return .authenticThe counter is the interesting part
Offline, the locally stored lastSeenCounter is a tradeoff: it catches replay on the same device, but cross-device clones can't be detected immediately — only when events sync and the central system deduplicates. Our approach is to emit a provisional authentic state: locally confirmed, event signed and queued. Once online, VeriShield performs the final global dedupe and, if a conflict is found, fires a retroactive notification.
From engineering to operations
For integrators, this translates to a few concrete behaviors:
- Boot the SDK once with network so it can sync the key bundle.
- Subsequent scans on a plane, in a warehouse, or in a low-coverage retail floor still work — flagged "verified offline."
- The moment the device reconnects, queued events upload and the UI absorbs any retroactive suspect notifications.
Anti-counterfeit infrastructure is only truly tested when it's needed at its least convenient. If it doesn't work in a basement parking lot, the infrastructure isn't there.
Full SDK architecture documentation and the threat model for local storage are available under NDA.