Cryptographic Specification
Technical details of application sealing for vote.machineassurance.org
Executive Summary
All submitted applications are encrypted using ECIES (Elliptic Curve Integrated Encryption Scheme) before storage. The sealing uses:
- Curve: P-256 (also called secp256r1, prime256v1)
- Key agreement: ECDH (Elliptic Curve Diffie–Hellman)
- Key derivation: HKDF-SHA256 with domain separation
- Authenticated encryption: AES-256-GCM
The recipient's private key is never deployed to the edge. It is held offline, so an attacker who obtains every secret this system holds — the KV credentials, the API tokens, the whole environment — and who reads the entire KV namespace, gets ciphertext and nothing else. Applications already stored cannot be read without the offline key.
What that does not cover, stated plainly because you would find it anyway: sealing happens in the Worker, so the Worker necessarily handles your submission in plaintext for the moment it takes to encrypt it. An attacker who has achieved code execution in the Worker itself could therefore read submissions made while they hold that position. No encryption-at-rest scheme prevents this; the honest boundary is that the design protects everything already written down, not a submission passing through a compromised server. Applications are sealed on arrival and are never decrypted by any online system afterwards, and the notification sent to our reviewers deliberately carries no application content — only a reference and the record hash — so that reading an application requires the offline key rather than access to a mailbox.
Sealing Public Key
The sealing public key (recipient's public half) is a P-256 point in uncompressed form, base64url encoded:
SHA-256 Fingerprint of the Sealing Public Key (full):
Key ID (first 16 hex characters):
This fingerprint is included in every sealed envelope as the kid field, so you can verify that you are looking at an envelope sealed to this key.
Sealed Envelope Wire Format
Every sealed application is stored as a JSON object with the following structure:
| Field | Description |
|---|---|
| v | Protocol version (currently 1). Allows future format changes without ambiguity. |
| alg | Algorithm identifier. Always ECIES-P256-HKDF-SHA256-AES256GCM for this deployment. Authenticated via the AES-GCM additional data (AAD), so it cannot be rewritten in transit. |
| epk | Ephemeral public key (P-256 uncompressed point, base64url). Generated fresh for every sealed message. 65 bytes when decoded (1 leading 0x04 byte + 2×32 bytes of x,y coordinates). Bound to the ciphertext two ways: via the HKDF salt and via the AAD. |
| iv | AES-GCM initialization vector (12 random bytes, base64url). Generated fresh for every sealed message. A GCM input and part of the AAD. |
| ct | Ciphertext with authentication tag (base64url). Consists of the encrypted plaintext followed immediately by the 16-byte GCM authentication tag (128 bits). No length prefix; the tag is the final 16 bytes. |
| kid | Key ID (first 16 hex characters of SHA-256(recipient_public_key)). Allows readers to identify which key opens this envelope without exposing the full fingerprint. |
Encryption Algorithm
Step 1: Ephemeral Key Generation
Using the standard WebCrypto algorithm:
This yields an ephemeral private key (kept temporarily in memory) and an ephemeral public key (included in the envelope as epk).
Step 2: Shared Secret Derivation (ECDH)
Perform elliptic curve Diffie–Hellman between the ephemeral private key and the recipient's public key:
This produces a 256-bit (32-byte) shared secret. Note: this is the raw shared secret from ECDH, not yet a usable encryption key. The following step derives the encryption key from it.
Step 3: Key Derivation (HKDF-SHA256)
HKDF-SHA256 protects against key substitution attacks by binding the derived key to both parties' public keys:
Salt: SHA-256(ephemeral_pubkey || recipient_pubkey)
The salt is computed by concatenating the ephemeral public key's raw bytes (65 bytes, uncompressed P-256 point) with the recipient's public key's raw bytes (65 bytes), then hashing the concatenation. This salt makes the derived key specific to this exact (ephemeral_key, recipient_key) pair.
Info string: mai/osv/submission/v1
The info string is a domain separation constant that binds the derived key to this specific application and protocol version.
This produces an AES key object suitable for AES-256-GCM encryption (256 bits = 32 bytes).
Step 4: Authenticated Encryption (AES-256-GCM)
Using AES-GCM in authenticated encryption mode:
IV: 12 random bytes, generated fresh for every message (included in envelope as iv)
Tag length: 128 bits (16 bytes)
Additional authenticated data (AAD): the cleartext envelope header is bound to the ciphertext via AES-GCM additional data, so the algorithm and key material cannot be rewritten in transit without invalidating the tag. The AAD is a UTF-8 string constructed deterministically as:
where alg, epk, and iv are the exact base64url strings carried in the envelope. The pipe (|) delimiter is safe because base64url contains no |. The version prefix v1 in the AAD also binds the envelope v field.
The WebCrypto API automatically appends the authentication tag to the ciphertext. The result (ciphertext || tag) is 16 bytes longer than the plaintext.
The tag authenticates the ciphertext, the IV, and the cleartext envelope header (alg, epk, and v via the AAD prefix). The recipient public key fingerprint (kid) is already bound through the HKDF salt and is not included in the AAD. If any byte of the ciphertext, IV, or header is modified, decryption fails with an authentication error.
Decryption (Offline, Private Key Required)
Decryption requires the private key, which is held offline:
1. Extract the ephemeral public key from epk and decode from base64url to get 65 bytes (uncompressed P-256 point).
2. Import the ephemeral public key as a P-256 point.
3. Perform ECDH between the (offline) recipient private key and the ephemeral public key to get the shared secret (32 bytes).
4. Derive the AES key using HKDF-SHA256 with the same salt and info string as encryption:
5. Reconstruct the additional authenticated data exactly as on the sealing side:
6. Decrypt the ciphertext using AES-256-GCM with the derived key, the recovered IV, and the reconstructed AAD.
If the authentication tag is valid, the plaintext is returned. If any bit of the ciphertext, IV, or envelope header was modified, or if the wrong key is used, AES-GCM authentication fails and decryption returns an error.
Security Properties
Authenticity
AES-GCM's 128-bit authentication tag protects the entire message (ciphertext + metadata) against tampering. A forged or modified envelope cannot be decrypted; the authentication check fails deterministically.
Confidentiality
Every message uses a fresh ephemeral key and a fresh IV. An attacker observing the ciphertext learns nothing about the plaintext or the recipient's key. An attacker with access to any subset of messages cannot use that information to break other messages.
Key Isolation
The HKDF salt is derived from both the ephemeral and recipient public keys. This prevents an attacker from creating a new ephemeral key that would derive the same AES key against a different recipient's public key. Each (ephemeral, recipient) pair yields a unique AES key.
Defense in Depth for Stored Data
Because the recipient private key is never deployed to the Worker, KV, or any connected system:
- Compromise of the Worker code or environment does not leak plaintext.
- Leakage of all environment secrets does not leak plaintext.
- Full compromise of the KV namespace yields ciphertext only.
- The only attack that would reveal plaintext is theft of the offline private key, which is stored in a secure vault with access controls and audit logging.
Verifying the Sealing Public Key
You can independently verify that the sealing public key is correct by computing its SHA-256 fingerprint and confirming it matches the value published here.
Using OpenSSL and Command Line
Decode the base64url public key and verify its fingerprint:
Using Python
Implementation Verification
The source code implementing this specification is in the repository at functions/api/osv/_lib/crypto.ts. The implementation:
- Uses only WebCrypto primitives available in Cloudflare Workers (no third-party libraries in the sealing path)
- Follows the ECIES standard without deviation or ad hoc modifications
- Is tested against the live sealing public key to ensure round-trip correctness
- Has no code path by which the Worker could decrypt a sealed envelope (the private key is never imported or present in the Worker context)
Questions or Audit Requests
If you are a security researcher, auditor, or curious technical person and have questions about this specification or would like to verify the implementation independently, please contact security@machineassurance.org.
Last updated: August 7, 2026. This specification describes the exact cryptographic construction and the offline key material status as of this date.