Why Browsers Lag Behind on Trust

The web is the most powerful application platform in existence. As long as you have the right API, you can safely run anything you want in a browser. Well… anything but cryptography.

It is as true today as it was in 2011 that Javascript cryptography is Considered Harmful. The main problem is code distribution. Consider an end-to-end-encrypted messaging web application. The application generates cryptographic keys in the client’s browser that lets users view and send end-to-end encrypted messages to each other. If the application is compromised, what would stop the malicious actor from simply modifying their Javascript to exfiltrate messages?

It is interesting to note that smartphone apps don’t have this issue. This is because app stores do a lot of heavy lifting to provide security for the app ecosystem. Specifically, they provide integrity, ensuring that apps being delivered are not tampered with, consistency, ensuring all users get the same app, and transparency, ensuring that the record of versions of an app is truthful and publicly visible.

It would be nice if we could get these properties for our end-to-end encrypted web application, and the web as a whole, without requiring a single central authority like an app store. Further, such a system would benefit all in-browser uses of cryptography, not just end-to-end-encrypted apps. For example, many web-based confidential LLMs, cryptocurrency wallets, and voting systems use in-browser Javascript cryptography for the last step of their verification chains.

Here we provide an early look at such a system, called Web Application Integrity, Consistency, and Transparency (WAICT), a W3C-backed effort among browser vendors, cloud providers, and encrypted communication developers to bring stronger security guarantees to the entire web. We will discuss the problem we need to solve, and build up to a solution resembling the current transparency specification draft.

Defining the Web Application

In order to talk about security guarantees of a web application, it is first necessary to define precisely what the application is. A smartphone application is essentially just a zip file. But a website is made up of interlinked assets, including HTML, Javascript, WASM, and CSS, that can each be locally or externally hosted. Further, if any asset changes, it could drastically change the functioning of the application. A coherent definition of an application thus requires the application to commit to precisely the assets it loads. This is done using integrity features, which we describe now.

Subresource Integrity

An important building block for defining a single coherent application is subresource integrity (SRI). SRI is a feature built into most browsers that permits a website to specify the cryptographic hash of external resources, e.g.,

<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.13.7/underscore-min.js" integrity="sha512-dvWGkLATSdw5qWb2qozZBRKJ80Omy2YN/aF3wTUVC5+D1eqbA+TjWpPpoj8vorK5xGLMa2ZqIeWCpDZP/+pQGQ=="></script>

This causes the browser to fetch underscore.js from cdnjs.cloudflare.com and verify that its SHA-512 hash matches the given hash in the tag. If they match, the script is loaded. If not, an error is thrown and nothing is executed.

If every external script, stylesheet, etc. on a page comes with an SRI integrity attribute, then the whole page is defined by just its HTML. This is close to what we want, but a web application can consist of many pages, and there is no way for a page to enforce the hash of the pages it links to.

Integrity Manifest

We would like to have a way of enforcing integrity on an entire site, i.e., every asset under a domain. For this, WAICT defines an integrity manifest, a configuration file that websites can provide to clients. One important item in the manifest is the asset hashes dictionary, mapping a hash belonging to an asset that the browser might load from that domain, to the path of that asset. Assets that may occur at any path, e.g., an error page, map to the empty string:

"hashes": {
"81db308d0df59b74d4a9bd25c546f25ec0fdb15a8d6d530c07a89344ae8eeb02": "/assets/js/main.js",
"fbd1d07879e672fd4557a2fa1bb2e435d88eac072f8903020a18672d5eddfb7c": "/index.html",
"5e737a67c38189a01f73040b06b4a0393b7ea71c86cf73744914bbb0cf0062eb": "/vendored/main.css",
"684ad58287ff2d085927cb1544c7d685ace897b6b25d33e46d2ec46a355b1f0e": "",
"f802517f1b2406e308599ca6f4c02d2ae28bb53ff2a5dbcddb538391cb6ad56a": ""
}

The other main component of the manifest is the integrity policy, which tells the browser which data types are being enforced and how strictly. For example, the policy in the manifest below will:

  1. Reject any script before running it, if it’s missing an SRI tag and doesn’t appear in the hashes
  2. Reject any WASM possibly after running it, if it’s missing an SRI tag and doesn’t appear in hashes
"integrity-policy": "blocked-destinations=(script), checked-destinations=(wasm)"

Put together, these make up the integrity manifest:

"manifest": {
  "version": 1,
  "integrity-policy": ...,
  "hashes": ...,
}

Thus, when both SRI and integrity manifests are used, the entire site and its interpretation by the browser is uniquely determined by the hash of the integrity manifest. This is exactly what we wanted. We have distilled the problem of endowing authenticity, consistent distribution, etc. to a web application to one of endowing the same properties to a single hash.

Designing for transparency without breaking the web

A transparent web application keeps its code in a publicly accessible, append-only log. That gives users two forms of protection: if they are served malicious code and later find out, they can prove to third parties exactly what ran; and if they never find out, an independent auditor can still scan historical logs and catch the attack. Transparency doesn't prevent malicious code from being served—it makes the serving of it publicly auditable.

With a single hash committing to a website’s full contents, the next step is getting that hash into a public log. That requires a system meeting a demanding set of constraints:

  • No breakage. Participation must be strictly opt-in and must not interfere with how existing sites work.
  • No added round trips. Transparency must not introduce extra client-server network latency.
  • Privacy. Users must not identify themselves to any party they wouldn’t already identify themselves to—no new third-party connections, no identifying data sent to the site.
  • Statelessness. Users must not store per-site cryptographic data.
  • No centralization. No single point of failure or trust; the system must keep making progress if any party goes down, and remain secure if any single party is distrusted.
  • Low barrier to entry. Site operators should be able to enroll cheaply, without deep expertise.
  • Easy opt-out. Sites must be able to leave, even if all cryptographic material is lost (e.g., domain seizure or sale), avoiding the lock-in that killed HPKP.
  • Transparent opt-out. Since transparency is optional, an attacker could disable it, serve malicious content, and re-enable it. Disabling must itself be logged so this attack is detectable.
  • Monitorability. Operators must be able to watch their site’s transparency data efficiently, without running an always-on, high-load monitor.

Hash chains as the building block

At the core of nearly every transparency mechanism is an append-only log that can produce two kinds of proofs: an inclusion proof that an element appears at a given index, and a consistency proof that one log version is an extension of another—proving nothing was modified or deleted, only added.

The simplest such log is a hash chain, where each element is hashed with the running chain hash. The final chain hash concisely represents the whole list.

Two rows of three circles each. The top row (green) is shifted to the right of the bottom row (orange). Each green node has a solid line connecting it to the orange node to its bottom-left, and the green node to its left, if there is one. The orange nodes are labeled, from left to right, elem1, elem2, elem3. The green nodes are labeled, from left to right, “ch1 = H(elem1)”, “ch2 = H(ch1, elem2)”, and “ch3 = H(ch2, elem3)”.

Inclusion proofs and consistency proofs have simple structures. For inclusion at index i, the prover gives the chain hash before i plus all later elements; the verifier recomputes and checks the final hash.

Three labeled components. The first component, labeled “inclusion proof”, is a box containing a green node labeled ch1, and two orange circles labeled elem2 and elem3, respectively. The second component, labeled “Verifier knows” is a box containing a single green node with a purple ring around it, labeled ch-prime-3. The third component, labeled “Verifier computes”, is as follows. Similar to the hash chain before, there is a top row of three green nodes and a bottom row of two orange nodes. As before, there are lines connecting each green node to its left and bottom-left neighbor, but now these lines are light grey and dashed. The orange nodes are labeled elem2 and elem3. The leftmost node in the top row is labeled ch1. The remaining top row nodes are lighter green. They are labeled ch2 and ch3. Finally, separately from the rows, there is an equation, with a question mark above the equals sign.  On the left hand side is a copy of the light green node labeled ch3. On the right hand side is a copy of the node labeled ch-prime-3. 

For consistency between chains of sizes i and j, the prover supplies the elements between those points.

Three labeled components. The first component, labeled “consistency proof”, is a box containing two orange circles labeled elem2 and elem3. The next component, labeled “verifier knows,” is a box containing chain hash values ch-prime-1 and ch-prime-3, both in green with a purple ring around it. The third component, labeled “verifier computes” is as follows. Similar to the hash chain before, there is a top row of three green nodes and a bottom row of two orange nodes. Again, all lines are grey and dashed, the orange nodes are labeled elem2 and elem3, and the two rightmost green nodes are light green and labeled ch2 and ch3. The leftmost node in the top row is now labeled ch-prime-1. Finally, as before, there is an equation comparing ch3 with ch-prime-3.

Building a transparency scheme

Per-site logs and witnesses

Give each site its own log as a hash chain whose items are the hashes of the site’s manifests at each point in time. The log stores manifest hashes, not manifests themselves; a separate asset host maps hashes to content using content-addressable, strongly cached static storage.

A hash chain as originally. The bottom row is three orange circles and the top row is three green circles, shifted to the right of the bottom row. The top rightmost circle is labeled “chain hash”. The bottom row circles are labeled manifest1, manifest2, and manifest3. Solid lines connect each green circle with the green one to its left (if it exists) and an orange circle to its bottom left.

A raw log isn’t trustworthy—whoever runs it can rewrite history and recompute the chain. To enforce append-only behavior, a trusted third party called a witness verifies a consistency proof against its stored chain hash, and if valid, signs the new chain hash with a timestamp.

When a user visits a transparency-enabled site:

  1. The site serves its manifest, an inclusion proof that the manifest is in the log, and the signatures from all witnesses that validated the chain hash.
  2. The browser verifies the signatures from witnesses it trusts.
  3. The browser verifies the inclusion proof, confirming the manifest is the newest entry.
  4. The browser proceeds with the usual manifest and SRI integrity checks.

The user can now trust that the manifest is recorded in a log whose chain hash a witness has saved, so it can’t be deleted from history—and, assuming the asset host works, a copy of the served code is publicly available.

The transparency service

There’s a problem: if an attacker takes over a site, they can simply stop serving transparency data, silently disabling it. The system needs an explicit record of every site enrolled in transparency.

That record is a global data structure mapping each site domain to its log’s chain hash. A prefix tree (trie) fits well. Each leaf represents a domain and stores the site log’s chain hash, the log’s current size, and the asset host URL. A site proves its transparency status by presenting an inclusion proof for its leaf—efficient for tries.

A binary tree. Each node is a blue circle. The top circle, the root node, is labeled “root hash”. Two solid lines come out of the bottom of the root node, one to the left and one to the right. The left one is labeled with the alphabet range “a-o”. It connects to a node labeled “example.com”. This node has a curvy dashed arrow coming from it and pointing to the rightmost green node in a miniature hash chain representation. The right line coming from the root node is labeled with the alphabet range “p-z”, and connects to another blue node. This node itself has children. The right line is labeled with the alphabet range “r-z” and connects to a node labeled “rust-lang.org”. The left line is labeled with the alphabet range “p-q” and connects to another blue node. This blue node itself has children. The left line is labeled with the alphabet range “pa-ps” and connects to a node labeled “pets.com”. The right line is labeled with the alphabet range “pr-q” and connects to a node labeled “produces.com”.

The party running the trie, the transparency service, requires a site to prove domain possession before enrollment. Updates are handled by sending the service a new entry, which computes the new chain hash. Unenrollment is simply a request to remove the leaf—an adversary could also do this, which is why the removal mechanism matters (discussed below).

Witnesses verify whole-tree updates

Witnesses now check the prefix tree rather than individual site logs. For each tree update, the system must produce a proof covering every added, deleted, or modified entry, plus a consistency proof for each site log showing it was properly appended to. After verifying, the witness signs the tree root.

A sequence diagram with parties, left to right, User, Site, Asset Host, Transparency Service, and Witnesses. The sequence is as follows. The site sends new assets to the asset host and the new manifest hash to the transparency service. The transparency service waits to collect these updates from many sites. Eventually, the transparency service sends a new root and tree update proof to the witnesses, and gets back a many-times signed root. It returns the signed root and prefix tree inclusion proof to the site. Later, a user connects to the site, requesting GET /index.html. The site responds with index.html, the signed root, the inclusion proof, and the integrity manifest. The user checks the integrity, inclusion proof, and signatures.

Client-side verification remains largely the same, with two changes: the client verifies two inclusion proofs—one for the integrity policy in the site log, and one for that log in the prefix tree—and it verifies the witness signature over the prefix tree root.

Signaling enrollment without extra round trips

Knowing whether a site is enrolled could mean fetching its tree entry—but that adds a round trip. Instead, browsers ship with the list of enrolled sites, the transparency preload list. If a site is on the preload list, the browser expects it to present an inclusion proof in the prefix tree, or a proof of non-inclusion in a newer tree version indicating unenrollment. A site must supply one of these until the last preload list containing it has expired. Because nothing enforces that the preload list matches the tree, the preload list itself must be published transparently.

Completing the property set

Cheap monitoring with timestamps

Monitoring without this design would mean constantly polling transparency services and verifying a domain hasn’t been tampered with—better than Certificate Transparency’s roughly 500k events per hour, but still a standing burden on both monitor and service.

The fix adds a “created” timestamp to each leaf, set at enrollment. Witnesses enforce that this field stays constant across all leaf updates and is deleted when the leaf is deleted. A site operator only needs to remember the last observed “created” and “log size” values. If both are unchanged on the next fetch, nothing happened since the last check.

Transparent opt-out via tombstones

Leaf deletion must also be auditable. Rather than removing a leaf outright, the transparency service replaces it with a tombstone—a value containing only a “created” timestamp. Witnesses ensure the field is unchanged until the leaf is either permanently deleted after a visibility period or re-enrolled.

Multiple services, no single point of trust

To avoid a single point of failure or trust, the design anticipates a small set of non-colluding transparency service providers, each running its own prefix tree. Like Certificate Transparency, the set must be small enough for reasonable trust assumptions and for independent auditors to handle the load of verifying all of them.

Why Consistency Matters

Transparency alone is not enough. If a site can serve any version from its entire history, an auditor would have to review every version ever published to ensure no user was ever served malware. Even a site that releases just one version per week would accumulate hundreds of servable versions over the years, including old, vulnerable ones. For transparency to be useful, we need consistency: the property that all browsers see the same version of a site at a given time.

Perfect consistency is unattainable, but weaker forms suffice. If a site had eight valid versions at any moment, an auditor could manage that. Users might not all see identical versions, but they would still all benefit from transparency.

We distinguish two types of inconsistency:

Tree Inconsistency

Tree inconsistency arises when transparency services' prefix trees disagree on the chain hash of a site, meaning they disagree on the site's history. Consensus mechanisms could eliminate this entirely—for example, majority voting across five transparency services would require a site to present three tree inclusion proofs. But that triples proof sizes and reduces fault tolerance: if three log operators go down, no transparent site can publish updates.

Instead, we limit the number of transparency services. Chrome trusts eight Certificate Transparency logs in 2025, and a similar number would work here. Inconsistencies between trees remain detectable and provable, since witnesses sign roots. If sites are expected to use the same version across all trees, violations become subject to social pressure.

Temporal Inconsistency

Temporal inconsistency occurs when a user receives an older or newer version of a site (both still unexpired) based on external factors like geography or cookies. If a signed prefix root remains valid for ten years, a site could serve any version from that decade.

Consensus mechanisms could also solve this—publishing the latest manifest on a blockchain would let users fetch the current head and verify they have the latest version. But that adds a network round trip per client, forces sites to wait for on-chain publication before updating, and dramatically increases specification complexity. We're aiming for v1.0.

Our mitigation is to require reasonably short validity periods for witness signatures. If prefix root signatures expire weekly, the number of simultaneously servable versions shrinks dramatically. The cost: site operators must query the transparency service at least weekly for a fresh signed root and inclusion proof, even with no changes. The transparency service must handle that load, so this parameter needs careful tuning.

Beyond the Core Properties

Integrity, consistency, and transparency are substantial, but additional app store-like features can be layered on with modest effort.

Code Signing

WAICT doesn't address provenance: precisely where did the code originate? For heavily audited code, third-party review fills the gap. But small self-hosted deployments of open-source software lack that coverage. If Alice hosts her own Cryptpad instance for her friend Bob, how can Bob verify the code matches the upstream repository?

WEBCAT, built by the Freedom of the Press Foundation (FPF), solves this. The protocol lets site owners announce which developers signed the site's integrity manifest—meaning all code and assets served to users. Users running the WEBCAT plugin can inspect those developers' Sigstore signatures and decide whether to trust the code.

WAICT is extensible enough to accommodate WEBCAT. Manifests may carry additional metadata we call extensions; here, the extension holds a list of developers' Sigstore identities. For this to work, browsers must expose an API so plugins can read these extension values, enabling independent parties to build features on top of WAICT.

Cooldowns as a Safety Net

Nothing described so far stops an attack in progress. An attacker who compromises a website can strip code-signing extensions or remove the site from transparency entirely, continuing their attack undisturbed. The removal will be logged, but the malicious code won't be, and discovery may come too late.

Unenrollment cooldown prevents spontaneous removal. With a 24-hour cooldown, the client requires that any site on the preload list either has transparency enabled or has a tombstone entry at least 24 hours old. An attacker must then either serve a transparency-compliant version or run a broken site for a full day.

Extension cooldown similarly blocks sudden modifications, using code signing as the example. The dev-ids extension gets its own preload list identifying sites that opted into code signing—without one, any site could delete the extension at will. The client enforces two rules: dev-ids must exist in the manifest, and dev-ids-inclusion must contain an inclusion proof showing the current value sat in a prefix tree at least 24 hours old. Clients reject newer values. To remove dev-ids, a site must first request removal from the preload list, then set dev-ids to an empty string and update dev-ids-inclusion accordingly.

Deployment Roles

This ecosystem has several distinct actors with different trust and resource profiles.

Transparency services store metadata for every transparency-enabled site. With 100 million domains at 256 bytes per entry (a few hashes plus a URL), a single tree totals roughly 26GB, excluding intermediate hashes. Pruning rules would likely unenroll inactive sites to control growth. Services need moderate storage, high availability, and uncorrelated downtime, since all services going down would block updates for every transparent site.

Their trust requirements are bounded by witnesses. A service could theoretically swap any leaf's chain hash and obtain witness validation if the consistency proof is valid, but such changes are detectable by anyone monitoring that leaf.

Witnesses verify prefix tree updates and sign resulting roots. They hold a full copy of each tree they witness, giving them storage costs similar to transparency services, plus high uptime demands. They must also protect their signing keys for extended periods—long enough for browser trust stores to update during key rotation.

Asset hosts carry little trust. They cannot serve bad data because query responses are hashed and compared against known hashes. Their only misbehavior is refusing to respond, which can also happen accidentally through downtime.

Clients—the web browsers themselves—are the most trust-sensitive component, performing all transparency and integrity checks.

At Cloudflare, we intend to run both a transparency service and a witness. Crucially, our witness should not monitor our own service. Instead, we witness other organizations' services, and they witness ours.

Supporting Alternate Ecosystems

WAICT should work where large centralized players are absent. We are collaborating with the FPF to define transparency for networks with different trust environments, primarily the Tor ecosystem.

Paranoid Tor users may distrust existing transparency services and witnesses, and may lack the resources to self-host them. For such cases, placing the prefix tree on a blockchain may be appropriate. Standard domain validation becomes impossible without a validator server, but that's acceptable for onion services: an onion address is a public key, so a signature alone proves domain ownership.

A consensus-backed tree eliminates the need for witnesses and leaves a single canonical transparency service. Tree inconsistency largely disappears, at the cost of update latency.

What Comes Next

The standardization process is just beginning. Near-term work includes extending subresource integrity to WASM and images, followed by standardizing the integrity manifest format and then the remaining features. We intend to develop this specification with browsers and the IETF, with betas expected soon.

You can follow the transparency specification draft, examine open problems, and contribute ideas. Pull requests and issues are welcome.

Acknowledgements

Thanks to Dennis Jackson of Mozilla for extensive design discussions, to Giulio B and Cory Myers of the FPF for feedback and influence, and to Richard Hansen for valuable input.