Cryptographic Specification

Technical details of application sealing for vote.machineassurance.org

Purpose: Every submitted application is sealed with ECIES encryption against a public key held offline. This specification documents the exact algorithm, wire format, and key material so that independent auditors and security researchers can verify the implementation and the claim that the stored ciphertext is unreadable without the offline private key.

Executive Summary

All submitted applications are encrypted using ECIES (Elliptic Curve Integrated Encryption Scheme) before storage. The sealing uses:

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:

BKfx9YXZq4Q5nYL5EFmBlmTQMJAk6Trye5v9pg_LLoNAaxrvx8w4pFEHTidxiBHZ3AwChyi1DLWIZD08ZU1vtJk

SHA-256 Fingerprint of the Sealing Public Key (full):

ccd9c7a629a8fd8d944481021b4a94f53e784d87e0fb5dc09aec520c863d3644

Key ID (first 16 hex characters):

ccd9c7a629a8fd8d

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.

Private Key Status: The private half of this keypair is held offline in secure storage and is never deployed to Cloudflare Workers, the Pages environment, KV, or any connected system. It is the only method by which sealed applications can be decrypted. Loss of this key makes every stored application permanently unreadable.

Sealed Envelope Wire Format

Every sealed application is stored as a JSON object with the following structure:

{ "v": 1, "alg": "ECIES-P256-HKDF-SHA256-AES256GCM", "epk": "<base64url ephemeral public key>", "iv": "<base64url AES-GCM initialization vector>", "ct": "<base64url ciphertext with authentication tag>", "kid": "<first 16 hex chars of SHA-256 of recipient pubkey>" }
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

Generate a fresh P-256 keypair for each message.

Using the standard WebCrypto algorithm:

crypto.subtle.generateKey({name: "ECDH", namedCurve: "P-256"}, true, ["deriveBits"])

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)

Compute the shared secret via ECDH.

Perform elliptic curve Diffie–Hellman between the ephemeral private key and the recipient's public key:

crypto.subtle.deriveBits({name: "ECDH", public: recipient_public_key}, ephemeral_private_key, 256)

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)

Derive the AES key using HKDF with domain separation.

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.

crypto.subtle.deriveKey({name: "HKDF", hash: "SHA-256", salt: SHA-256(epk || recipient_pubkey), info: "mai/osv/submission/v1"}, hkdf_key, {name: "AES-GCM", length: 256}, false, ["encrypt"])

This produces an AES key object suitable for AES-256-GCM encryption (256 bits = 32 bytes).

Step 4: Authenticated Encryption (AES-256-GCM)

Encrypt the plaintext and generate an authentication tag.

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:

aad := "mai/osv/aad/v1|" + alg + "|" + epk + "|" + iv

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.

crypto.subtle.encrypt({name: "AES-GCM", iv: random_12_bytes, tagLength: 128, additionalData: aad_bytes}, aes_key, plaintext_bytes)

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:

salt := SHA-256(epk_raw || recipient_pubkey_raw)

5. Reconstruct the additional authenticated data exactly as on the sealing side:

aad := "mai/osv/aad/v1|" + envelope.alg + "|" + envelope.epk + "|" + envelope.iv

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:

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:

cat << 'EOF' | node const key = "BKfx9YXZq4Q5nYL5EFmBlmTQMJAk6Trye5v9pg_LLoNAaxrvx8w4pFEHTidxiBHZ3AwChyi1DLWIZD08ZU1vtJk"; function b64uDecode(s) { const pad = s.length % 4 === 0 ? "" : "=".repeat(4 - (s.length % 4)); return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64"); } const { createHash } = require("crypto"); const raw = b64uDecode(key); const hash = createHash("sha256").update(raw).digest("hex"); console.log("Fingerprint:", hash); console.log("Expected: ", "ccd9c7a629a8fd8d944481021b4a94f53e784d87e0fb5dc09aec520c863d3644"); console.log("Match:", hash === "ccd9c7a629a8fd8d944481021b4a94f53e784d87e0fb5dc09aec520c863d3644" ? "✓" : "✗"); EOF

Using Python

python3 << 'EOF' import hashlib import base64 key_b64u = "BKfx9YXZq4Q5nYL5EFmBlmTQMJAk6Trye5v9pg_LLoNAaxrvx8w4pFEHTidxiBHZ3AwChyi1DLWIZD08ZU1vtJk" # Decode base64url pad = "=" * (4 - len(key_b64u) % 4) if len(key_b64u) % 4 else "" key_raw = base64.urlsafe_b64decode(key_b64u + pad) # Compute SHA-256 fingerprint = hashlib.sha256(key_raw).hexdigest() expected = "ccd9c7a629a8fd8d944481021b4a94f53e784d87e0fb5dc09aec520c863d3644" print(f"Fingerprint: {fingerprint}") print(f"Expected: {expected}") print(f"Match: {'✓' if fingerprint == expected else '✗'}") EOF

Implementation Verification

The source code implementing this specification is in the repository at functions/api/osv/_lib/crypto.ts. The implementation:

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.