Skip to content

Work offline

An in-person check needs no internet. The QR code, the Bluetooth link, the signed request, the holder’s answer and every cryptographic check happen between the two phones. Field staff in a car park, a basement or a remote checkpoint get the same result as staff at a desk.

sequenceDiagram
    autonumber
    actor P as Operator
    participant O as Your app (verifier)
    participant W as Wallet (holder's phone)
    actor H as Holder
    Note over O,W: No network on either phone
    P->>O: Picks a check
    H->>W: Present: QR code
    O->>W: Scan, then Bluetooth
    O->>W: Signed request
    W->>H: Names the verifier, lists the fields
    H->>W: Approves
    W->>O: Approved fields, signed by the phone
    O->>O: Issuer signature (bundled trust list), device binding, validity dates
    O->>O: Revocation: last downloaded status, with its age
    O-->>P: Accept / reject / inconclusive, with the portrait
Check Needs network? Why not
The issuer really signed these fields no the issuer certificates (or the signed VICAL) ship inside your app’s TrustConfig
The credential belongs to this phone no the answer is signed with the device key and bound to this session
The credential is valid today no validity dates are inside the signed data
The verifier’s name on the holder’s screen no the wallet carries its reader trust list
The credential has not been revoked not for the check itself the verifier keeps the last downloaded status and reports how old it is

Issuers publish a status list. When the verifier is online it downloads it and caches it. Offline, the check uses the cached answer and the result says so:

Field on AcceptanceResult Meaning
revocation VALID, REVOKED, SUSPENDED, or UNKNOWN (no status data yet)
revocationPublished false when the credential carries no status reference at all, so there is nothing to check
revocationDataStale true when the cached status is older than the freshness window
revocationCheckedAt when the status was last established
decision INCONCLUSIVE when everything else passed but revocation could not be established

The ready-made screen shows this as “Last established: 2 hours ago” under the result, and the status line at the top of every screen reads Online or Offline, so the operator always knows which mode they are in.

Decide before launch what staff do with an INCONCLUSIVE result and with a stale answer. Three common policies:

Policy When How
Accept with a note low-risk checks (age at a door) treat INCONCLUSIVE as accept; log revocationCheckedAt
Retry online medium risk (a rental hand-over) ask the operator to move to coverage and scan again; the app refreshes the status list when it sees a network
Refuse high risk (a controlled area) require VALID with revocationDataStale == false

With your own screens, the policy is a when (Swift: switch) on the result:

OfflinePolicy.kt
package samples
import com.wallet.model.AcceptanceDecision
import com.wallet.model.AcceptanceResult
/** An admission policy on top of the result: stricter where the risk is higher. */
object OfflinePolicy {
fun admit(r: AcceptanceResult, highRisk: Boolean): Boolean = when (r.decision) {
AcceptanceDecision.ACCEPT -> !highRisk || !r.revocationDataStale // high risk: only with a fresh revocation answer
AcceptanceDecision.INCONCLUSIVE -> !highRisk // revocation could not be established
AcceptanceDecision.REJECT -> false
}
}
  • Trust lists are part of the app build or its configuration. Ship an updated VICAL with each app update, or load one from your server when online; nothing is fetched during a check.
  • Status lists refresh whenever the device has a network. A device that is online once a day keeps its revocation answers fresh enough for most policies.
  • Bluetooth and camera are the only hardware needs. Airplane mode with Bluetooth on is a valid field setup.

Next: Go live.