Trust Center · last reviewed October 28, 2026

Evidence you don't have to take our word for.

EasyCustody is built so that an election office, a court, an observer, or a journalist can check the custody record without trusting EasyVote, our servers, or anyone's screenshot. This page explains how the record is made, how it is protected, exactly what it contains, and how to verify it yourself.

Signed on the device
ECDSA P-256
Canonical hashing
RFC 8785 · SHA-256
Tree heads
RFC 6962 Merkle
Votes recorded
0 · ever

The boundary

What EasyCustody records. What it never touches.

Custody is about containers, seals, people, places, and time. It is not about what is inside the container. The product is designed so it cannot learn that even by accident.

Recorded, signed, and kept

  • Which container — a registry ID like HC-0412, its kind (ballot bag, memory card, media pouch, transport cage), and what it is nested inside.
  • Which seal — the serial number applied, verified, voided, or destroyed. Seals are inventory with their own chain.
  • Who — the signed-in person's pseudonymous ID, role, and badge; a second person's badge and PIN when a step needs two people.
  • Where — a site, a storage location, a gateway zone. A coordinate only at handoff points and storage scans, if the tenant allows it.
  • When — the device clock with a monotonic counter, and the server's time on receipt.
  • What was unusual — a neutral reason code (seal-mismatch, container-missing), a note, the SHA-256 of a photo.

Never recorded, never connected

  • Votes or tallies. No ballot image, no count, no result, no turnout number. There is no field for one.
  • Voter data. No voter names, registrations, or histories. Observers' contact details are the only personal data beyond staff accounts, and they are encrypted separately from events.
  • Voting equipment. EasyCustody never connects to a tabulator, ballot-marking device, or election management system. Memory cards are tracked as sealed objects, never read.
  • Continuous location. No background tracking. During a custody leg the phone records its position only as the tenant's policy allows, with a visible indicator.
  • Names in public. The public page shows counts and daily root hashes. Observers see logistics status per site, never people or sightings.
  • Judgement. Exceptions describe what was seen, not who was at fault. Wording is neutral by design.

How it works

How one scan becomes evidence.

Seven steps, each verifiable on its own. Every step below carries a label that says whether it is running in this local build, stands in for an Azure service with the same interface, or describes the production design.

  1. 1

    Signed on the device

    At enrollment the phone generates a P-256 key pair inside the Secure Enclave or Android Keystore. The private key never leaves. Each custody event is signed on the phone, with biometric or PIN presence, before it goes anywhere. A second person's badge and PIN land in the same signed event.

    Live: browser WebCrypto key, non-exportable
  2. 2

    Canonicalized and hashed

    The event is serialized with JSON Canonicalization (RFC 8785) and hashed with SHA-256, so a .NET server, a TypeScript browser, and an independent verifier all produce the same bytes. Shared test vectors keep the two implementations honest.

    Live: C# and TypeScript agree byte for byte
  3. 3

    Verified and chained per container

    The server checks the device's certificate and signature, assigns the next sequence number for that container, and links the event to the previous one by hash. Offline devices that append at the same time are both accepted and flagged for review. A signed event is never discarded.

    Live: append-only table, triggers forbid update and delete
  4. 4

    Countersigned

    The server signs the event hash together with its sequence number and chain link. The device's own signature is never altered, so the record carries both: who did it, and when the system accepted it.

    Live: ES256, key published for verifiers
  5. 5

    Added to a Merkle tree

    A worker appends new chain links to the tenant's transparency-log-style Merkle tree and signs a tree head: root, size, time, previous root. Inclusion proofs show an event is in the tree; consistency proofs show nothing earlier was changed.

    Live: RFC 6962 hashing, head every 30 s (5 min in production)
  6. 6

    Anchored outside the database

    Each tree head is written to Azure Confidential Ledger, a permissioned blockchain-based ledger that runs in hardware enclaves and returns a cryptographic receipt for every entry, and to an immutable, time-retained Blob container in a second region. The Records Officer can receive each day's root, publish it on the office's own website, and have it written to storage the tenant owns. Changing history would mean forging every one of those copies.

    Local stand-in: append-only ledger file and write-once files
  7. 7

    Exported and verified by anyone

    A bundle holds every event with its signatures, every device certificate, the proofs, the anchor receipts, and a readable report. An open verifier, a command-line tool and an in-browser one, checks all of it offline and prints a plain report. No account, no server, no trust in EasyVote required.

    Live: two independent verifiers, both pass the same bundles
Is this blockchain?

Same mathematics. No coins, no tokens, no public network.

Every event is hash-chained to the one before it, and the whole record rolls up into a Merkle tree, the structure blockchains are built from. The signed roots are then written to Azure Confidential Ledger, a permissioned blockchain-based ledger that runs in hardware enclaves and returns a cryptographic receipt for every entry, and to immutable storage the office can hold itself. There is no cryptocurrency, no token, and no public network, and nothing here records a vote. What matters is not the label but the proof: anyone can check the record with the open verifier against a root the office published.

Anatomy of one event

Two halves, two signers.

The blue fields are produced and signed on the device. They never change. The green fields are added by the server when it accepts the event, and covered by the countersignature. A verifier re-derives every hash from the fields and checks both signatures against published keys.

eventId
UUIDv7 made on the device. Also the idempotency key, so a retry can never double-record.
actors
One or two people. device-key means the signer's phone; badge-pin means a second person confirmed on that phone.
devicePrevHash
The last link the phone knew about. A mismatch is flagged, not rejected.
seq · chainPrevHash
Server order inside this container's chain. A gap or a wrong link fails verification.
serverCountersignature
Binds the device-signed event to its place in the chain.
{
  "eventId": "019a2b3c-4d5e-7f80-9123-456789abcdef",
  "tenantId": "harbor",  "electionId": "gen-2026-11",
  "containerId": "HC-0412",
  "type": "SealApplied",  "schemaVersion": 1,
  "payload": { "sealNumber": "GA-00400412", "previousSeal": null },
  "actors": [
    { "userId": "u-2101", "role": "FieldOperator", "badgeId": "B-2101",
      "deviceId": "d-7f3a", "presence": "device-key" },
    { "userId": "u-2102", "role": "FieldOperator", "badgeId": "B-2102",
      "presence": "badge-pin" }
  ],
  "deviceTime": "2026-11-03T19:12:08.412Z",  "bootId": "b7",  "counter": 118,
  "geo": null,
  "devicePrevHash": "c97cf24d157e…",
  "deviceSignatures": [{ "deviceId": "d-7f3a", "alg": "ES256", "sig": "MEUCIQ…" }],

  "seq": 9,
  "chainPrevHash": "7e3726487fb7…",
  "serverTime": "2026-11-03T19:12:09.004Z",
  "eventHash": "a577d3316994…",
  "chainHash": "4b1e0c88d2a0…",
  "serverKeyId": "countersign-1",
  "serverCountersignature": "MEQCIB…"
}

Try it yourself

Change one record and watch the chain show it.

This is a real chain, built in your browser a moment ago: five events for one container, each signed with a fresh P-256 key and hashed with SHA-256, then rolled into a Merkle root that we "published". Edit any field and see what a verifier would see. Nothing leaves your browser.

Re-linking the chain hides the break but cannot forge the device's signature, because the private key is in the phone's secure hardware, and cannot change the root that was already anchored and published. In production the server's countersignature adds a third thing an attacker would have to forge.

Security controls

Defence in depth, labelled honestly.

Every control from the specification, with its state in the build you are looking at. Live runs here on one laptop. Local stand-in keeps the production interface with a local implementation. Production design is specified for the Azure deployment and not exercised locally.

Identity and access

  • Live Authenticator-app MFA (TOTP, RFC 6238) required for every account. No SMS, no email codes. A used code is rejected for its window.
  • Live Step-up: a fresh code within 5 minutes to release a hold, change roles or retention, export a bundle, or reset someone's MFA.
  • Live Short-lived ES256 access tokens, refresh-token rotation with reuse detection, progressive lockout, per-user and per-IP rate limits.
  • Live Default-deny RBAC. The permission matrix is data, and authorization tests are generated from it for every role and capability.
  • Live Tenant comes only from the validated token; every query is scoped by it. Separation of duties: the person who starts a handoff cannot accept it; a hold needs a second Records Officer to release.
  • Production design OpenID Connect with PKCE through our own identity service (no Entra ID for end users), Argon2id passwords screened against breach lists, two-admin MFA resets, new-device alerts.

Devices and signing keys

  • Live One non-exportable signing key per enrolled device; every event carries that device's signature and certificate ID.
  • Live Revoking a device stops new events from it. Events it signed earlier stay valid and verifiable.
  • Live Offline first: signed events wait in an encrypted outbox and sync in order. Rejected events stay visible; the app never drops a signed event.
  • Local stand-in Device attestation is recorded as local-dev; production verifies Apple App Attest and Google Play Integrity before issuing a device certificate from a private CA.
  • Production design Secure Enclave and StrongBox keys, biometric or PIN presence to sign, SQLCipher database, certificate pinning, no over-the-air code updates, MDM-compatible remote wipe.

Data and evidence

  • Live Events are append-only and the store enforces it, not only the code. There is no update or delete API for events.
  • Live Per-container hash chains, server countersignatures, signed Merkle tree heads with inclusion and consistency proofs.
  • Live Gateway sightings and locate results are observations, stored apart from the chain with a 90-day life, never evidence.
  • Live People are referenced by pseudonymous ID in events; contact details live apart. Retention defaults to 22 months (52 U.S.C. 20701) and a hold always overrides disposal.
  • Local stand-in Anchoring to Azure Confidential Ledger and immutable Blob is an append-only ledger file and write-once files, labelled as such in every export.
  • Production design Customer-managed keys in Key Vault, AES-256-GCM field-level encryption for personal data, TLS 1.3, Managed HSM for tenants that require dedicated hardware.

Platform and supply chain

  • Live Problem-detail errors with correlation IDs, request bodies never logged, CORS limited to the app origins, strict request size limits.
  • Live A user-action audit log for every administrative change, append-only and exportable.
  • Production design One locked API gateway behind Front Door and a WAF; private endpoints for every data service; outbound firewall allow-list; just-in-time admin access, no public SSH or RDP.
  • Production design OWASP ASVS Level 2 target; SAST, dependency, secret, container, and infrastructure scanning in CI; signed images with SBOMs; nonce-based Content Security Policy; annual independent penetration test.

Threat model

What we assume will be tried, and what stops it.

ThreatMain controls
An insider or attacker edits history in the databaseDevice signatures, per-container hash chain, Merkle roots anchored outside the database and published by the tenant, no update or delete path, least-privilege data access
A phone is lost or stolenHardware-bound key, biometric or PIN lock, session and device revocation, encrypted local database, remote wipe; earlier events stay valid, new ones are refused
Phished or guessed credentialsMandatory TOTP, breached-password screening, lockout, step-up for sensitive actions, new-device alerts, short token lifetimes
Replayed or duplicated eventsDevice-generated unique event IDs double as idempotency keys; signed device time with boot ID and counter; per-container sequence
Two offline phones append at onceBoth events accepted and chained; a system event flags the branch for the Custody Manager; nothing is silently dropped
Cross-tenant accessTenant only from the token, tenant in every partition key, generated authorization tests, default deny
Spoofed Bluetooth tags or sightingsRotating ephemeral IDs only our server can resolve; gateways sign every sighting; sightings are informational and never enter the chain or an export unless chosen
Denial of service on election nightOffline-first apps keep working with no network; rate limits per user, device, and tenant; autoscale and a WAF in production
Vendor compromise, or a dispute with the vendorVerifiable exports with an open verifier, tenant-held copies and witnessed roots, break-glass access only with tenant approval
Misleading screenshots or out-of-context logsNeutral reason codes, silent default logging, complete exports with full context, a public page that shows only aggregates

Privacy

Custody of containers, not surveillance of people.

Location is a policy, not a default

Tenants choose Off, Handoff points only, or Full route per leg type. Position is captured only at the moment of a signed action or, during an active custody leg, while a visible indicator is on the phone. There is no background tracking between actions.

Sightings are not evidence

Gateway and locate sightings help people find a bag. They are stored apart from the chain, expire after 90 days unless a hold applies, are never shown to observers or the public, and carry no weight in a verification report.

Tracker fixes from the AirFinder SuperTags shared with EasyAssetTracker are observations too, never custody: stored apart from the chain with the same 90-day life, handled as restricted route data visible only to the Custody Manager and Records Officer, and left out of exports unless the office selects them. Attaching and removing a tag are the only tracker actions that enter the chain, because people do them.

Every lookup is logged

A locate request names who asked, which container, and why. Route data, when a tenant enables it, is visible only to the Custody Manager and the Records Officer and is excluded from exports unless the tenant selects it.

Staff see what is recorded about them

The field app's Sync screen lists exactly what this device records: signed actions with name, badge, and device ID; location only at handoff points; tether state during a leg. Never what is inside a container.

Observers get status, not people

The observer view shows expected, sealed, in transit, received, and stored counts per assigned site and the number of open exceptions. No names, no seal numbers, no sightings, no exception detail.

Right to be forgotten without breaking the record

Events reference people by pseudonymous ID. Where law allows, deleting a person's encryption key makes their details unreadable while every event remains verifiable.

Verify it yourself

Three ways to check the record, none of which need us.

1

In your browser

Drop an export bundle onto the public verifier. Every file hash, device certificate, signature, chain link, Merkle proof, and anchor receipt is checked by code running in your browser. Nothing is uploaded.

Open the browser verifier
2

On your own laptop

The command-line verifier is a separate implementation in a different language. Both must agree on every bundle, and both fail if a single byte of a single event changes.

dotnet run --project src/EasyCustody.Verifier -- verify bundle.zip
3

Against a published root

Each day's tree head root is published on the office's transparency page. Save it. Later, any bundle's consistency proof must lead back to that root, or history changed.

See the public page

Inside a bundle

  • manifest.json scope, time range, schema versions, SHA-256 of every file
  • events.jsonl every event in server order with all signatures
  • keys/ device credentials and server keys with their validity windows
  • proofs/ signed tree head, per-event inclusion proofs, consistency proofs
  • anchors/ ledger and immutable-storage receipts, tenant witness records
  • report.html a readable timeline and exception list
  • bundle.sig signature over the manifest

Compliance and disclosure

Where we are, and where we are going.

These are targets and alignments, stated as such. None is a certification we hold today.

SOC 2 Type II

Target for the production service. Controls above map to the Trust Services Criteria for security, availability, and confidentiality.

NIST 800-53 Moderate

Alignment target, with the CIS Azure benchmark for the cloud footprint and GovRAMP (formerly StateRAMP) readiness.

Azure Government

Infrastructure is kept portable so the same deployment can run in Azure Government for tenants that require it.

WCAG 2.2 AA

Design target across console, field app, and public pages: keyboard operable, screen-reader tested, no colour-only status.

OWASP ASVS Level 2

Application security target, with a threat model reviewed each release and an independent penetration test before the first pilot and yearly after.

Records retention

22 months by default for federal elections (52 U.S.C. 20701), longer where state rules say so. A hold always overrides disposal.

Report a vulnerability

We practise coordinated disclosure with a safe harbour for good-faith research. Tell us what you found, and we will acknowledge within two business days and keep you informed until it is fixed. Please do not test against a live election.

security@easyvote.demo PGP key and policy published at /.well-known/security.txt in production

This Trust Center describes the local reference build running on one laptop. Every Azure service it names has a local stand-in behind the same interface, and every claim labelled Live is exercised by the test suite: 313 .NET tests, the shared canonicalization vectors, and tamper tests that must make both verifiers fail.