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
What is decided on the device
Section titled “What is decided on the device”| 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 |
Revocation when offline
Section titled “Revocation when offline”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.
Set the policy
Section titled “Set the policy”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:
package samples
import com.wallet.model.AcceptanceDecisionimport 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 }}import Wallet
/// An admission policy on top of the result: stricter where the risk is higher.enum OfflinePolicy { static func admit(_ r: AcceptanceResult, highRisk: Bool) -> Bool { switch r.decision { case .accept: return !highRisk || !r.revocationDataStale // high risk: only with a fresh revocation answer case .inconclusive: return !highRisk // revocation could not be established default: return false // REJECT } }}Keeping the device ready
Section titled “Keeping the device ready”- 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.