Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions .github/workflows/pages.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ name: Build and Deploy Site

# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the "main" branch
push:
branches: [ "main" ]
pull_request:
Expand All @@ -11,23 +10,27 @@ on:
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

permissions:
contents: read

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains two jobs: "build" and "deploy"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
permissions:
contents: read

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v4
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false

# Install Python
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
with:
python-version: '3.13.1' # Change this to your desired Python version
python-version: '3.13.1'
cache: 'pipenv'

# Install pipenv
Expand All @@ -48,13 +51,14 @@ jobs:
# Upload site, does not deploy
- name: Upload static files as artifact
id: deployment
uses: actions/upload-pages-artifact@v3 # or specific "vX.X.X" version tag for this action
uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1
with:
path: output_html/

# Deployment job
deploy:
needs: build
if: github.ref == 'refs/heads/main'

# Grant GITHUB_TOKEN the permissions required to make a Pages deployment
permissions:
Expand All @@ -67,4 +71,4 @@ jobs:
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4 # or specific "vX.X.X" version tag for this action
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5
38 changes: 14 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
# Chrome Root Program
# Google Chrome Root Programs

Google Chrome relies on Certification Authority systems ("CAs") to issue certificates to websites. Chrome uses these certificates to help ensure the connections it makes on behalf of its users are properly secured. Chrome accomplishes this by verifying that a website's certificate was issued by a recognized CA, while also performing additional evaluations of the HTTPS connection's security properties. Certificates not issued by a CA recognized by Chrome or a user's local settings can cause users to see warnings and error pages.

When making HTTPS connections, Chrome refers to a list of root certificates from CAs that have demonstrated why continued trust in them is justified. This list is known as a "Root Store." CA certificates included in the [Chrome Root Store](https://g.co/chrome/root-store) are selected on the basis of publicly available and verified information, such as that within the Common CA Database ([CCADB](https://ccadb.org/)), and ongoing reviews by the Chrome Root Program.
To secure the web today and prepare for the post-quantum future, Chrome operates two distinct root programs:

In Chrome 105, Chrome began a platform-by-platform transition from relying on the host operating system's Root Store to its own on Windows, macOS, ChromeOS, Linux, and Android. This change makes Chrome more secure and promotes consistent user and developer experiences across platforms. Apple policies prevent the Chrome Root Store and corresponding Chrome Certificate Verifier from being used on Chrome for iOS. Learn more about the Chrome Root Store and Chrome Certificate Verifier [here](https://chromium.googlesource.com/chromium/src/+/main/net/data/ssl/chrome_root_store/faq.md).
* **[The Chrome Root Program](content/crp/policy.md)**: [Launched in 2022](https://blog.chromium.org/2022/09/announcing-launch-of-chrome-root-program.html), this program establishes the minimum requirements for self-signed root CA certificates to be included in the [Chrome Root Store](https://g.co/chrome/root-store) by default, providing consistent and reliable security for HTTPS connections across platforms. The authoritative policy is available at [https://g.co/chrome/root-policy](https://g.co/chrome/root-policy).
* **[The Chrome Quantum-resistant Root Program](content/cqrp/draft-policy.md)**: Designed for the post-quantum era, this program uses Merkle Tree Certificates (MTCs) to deliver quantum-safe security without slowing down page connections. Learn more about Google's planned adoption of MTCs on the [Google Security Blog](https://blog.google/security/cultivating-a-robust-and-efficient-quantum-safe-https/).

The Chrome Root Program Policy establishes the minimum requirements for self-signed root CA certificates to be included as trusted in a default installation of Chrome. This GitHub repository contains a Markdown-formatted version of the authoritative version available [here](https://g.co/chrome/root-policy).
Any questions regarding the Chrome Root Program can be directed to `chrome-root-program [at] google [dot] com`.

Any questions regarding the Chrome Root Program Policy can be directed to chrome-root-program [at] google [dot] com.
Any questions regarding the Chrome Quantum-Resistant Root Program can be directed to `chrome-quantum-resistant-root-program [at] google [dot] com`.

## Updating the Policy
## Updating the CRP Policy

The site is deployed on commits to `main`. To add a new policy revision:
The site is deployed automatically on commits to `main`. To add a new Chrome Root Program policy revision:

- Archive the current version in `content/policy-archive/` (create the directory
and copy it to the proper index.md file).
- Archive the current version in `content/policy-archive/` (create the directory and copy the current policy to the proper version file).
- Update `config.yaml`:
- Update `context.versions` array so that the path for the now archived
version is no longer marked as `current`
- Add a new entry at the bottom of the array for the next version, with an
archive path, `path: content/policy-archive/policy-version-NEW-VERSION`.
The file and folder do not need to exist.
- Bump `context.current_version` to the next version value. It should match
the version number in at the end of the versions array. Be sure all version
numbers are in quotes so they are interpreted as strings, not floats.
- Update `content/index.md` with the new content. This way the PR will show a
diff of the new policies in index.md.
- Update `context.versions` array so that the path for the now archived version is no longer marked as `current`.
- Add a new entry at the bottom of the array for the next version, with an archive path, `path: content/policy-archive/policy-version-NEW-VERSION`.
- Bump `context.current_version` to the next version value. It should match the version number at the end of the versions array. Be sure all version numbers are in quotes so they are interpreted as strings, not floats.
- Update `content/crp/policy.md` with the new policy content.

This can all be done in a single pull request. The diff in the PR will show the diff between the two policy versions.

### A note on links

Links in Markdown to other documents in this repository should end in `md`, e.g.
`[Policy](index.md)`. Links in raw HTML, e.g. `<a
href=/moving-forward-together>` should not. This make the links in the Github UI
work for Markdown, and results in a correctly-compiled site. Hardcoded HTML
links will not resolve correctly in previews.
Links in Markdown to other documents in this repository should end in `.md`, e.g. `[Policy](content/crp/policy.md)`. Links in raw HTML, e.g. `<a href="{{ '/crp/moving-forward-together' | absolute_url }}">` should not. This makes the links in the GitHub UI work for Markdown while also resulting in a correctly compiled static site. Hardcoded HTML links will not resolve correctly in previews.
6 changes: 6 additions & 0 deletions content/cqrp/apply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Chrome Quantum-resistant Root Program - Apply for Inclusion
---
# Apply for Inclusion

Coming soon.
6 changes: 6 additions & 0 deletions content/cqrp/draft-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Chrome Quantum-resistant Root Program Policy
---
# Chrome Quantum-resistant Root Program Policy

Coming soon.
25 changes: 25 additions & 0 deletions content/cqrp/faq.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: Chrome Quantum-resistant Root Program - Frequently Asked Questions
---
# Frequently Asked Questions

[TOC]

### Does Chrome support use of ML-DSA certificates?
Yes. Beginning in [Chrome 150](https://chromestatus.com/feature/5174590524489728), Chrome supports ML-DSA in private PKI hierarchies, making it possible to test non-MTC post-quantum X.509 certificates locally or within enterprise environments.

### Why does Chrome not support ML-DSA in the Chrome Quantum-resistant Root Store?
Post-quantum cryptographic algorithms such as ML-DSA feature significantly larger key and signature sizes than classical algorithms (like RSA or ECDSA). In a traditional PKI model using X.509 certificates, sending heavy, serialized chains of post-quantum signatures and Certificate Transparency (CT) proofs during every TLS handshake creates severe bandwidth penalties and increases connection latency across the web.

Instead of traditional X.509 chains, Chrome uses **Merkle Tree Certificates (MTCs)** developed in the [IETF PLANTS working group](https://datatracker.ietf.org/group/plants/about/):

* **Lightweight Proofs**: CAs sign a single "Tree Head" representing millions of certificates. The server sends only a compact Merkle Tree proof of inclusion rather than full signature chains.
* **Decoupled Payload Size**: Cryptographic strength is decoupled from transmitted data size, preserving fast handshake speeds.
* **Built-in Transparency**: Transparency is an intrinsic property of MTC issuance, making it impossible to issue a certificate without including it in a public tree, eliminating the need for extra CT overhead in the TLS handshake.

### When will MTCs be usable in Chrome?
Chrome's rollout of Merkle Tree Certificates spans three distinct phases:

- **Phase 1 (Underway)**: Feasibility study conducted in collaboration with Cloudflare to evaluate real-world performance and security of MTC connections. Experimental MTC connections are dual-backed by traditional X.509 certificates for safe fallback.
- **Phase 2 (Target: Q1 2027)**: Initial public MTC bootstrapping. Qualified CT Log operators (operating usable CT logs in Chrome prior to February 1, 2026) will be invited to participate in running initial public MTC issuance logs and mirrors.
- **Phase 3 (Target: Q3 2027)**: Launch of the Chrome Quantum-resistant Root Store (CQRS) and onboarding of prospective MTC CA Operators under the Chrome Quantum-resistant Root Program Policy. This phase will also introduce optional downgrade protections for sites using quantum-resistant certificates.
164 changes: 164 additions & 0 deletions content/cqrp/root-store.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
---
title: Chrome Quantum-resistant Root Program - CQRP Root Store
---

# CQRP Root Store

Below is the list of **MTC CA Cosigners** and **Mirror Cosigners** included in the Chrome Quantum-resistant Root Store.

Data is fetched directly from [`https://www.gstatic.com/mtcs/cosigners/v1/cosigners.json`](https://www.gstatic.com/mtcs/cosigners/v1/cosigners.json).

<div id="loading-spinner" style="padding: 1.5em; font-weight: bold; color: #57606a; background: #f6f8fa; border-radius: 6px; margin: 1em 0;">
⏳ Loading cosigners data...
</div>

<div id="cosigners-content" style="display: none;">
<p style="background: #e8f0fe; color: #1a73e8; padding: 0.75em 1em; border-radius: 6px; font-weight: 500;">
<strong>Store Version:</strong> <span id="store-version">-</span> &nbsp;|&nbsp;
<strong>Last Updated:</strong> <span id="store-timestamp">-</span>
</p>

<h3>MTC CA Cosigners (Issuers)</h3>
<table>
<thead>
<tr>
<th>Environment</th>
<th>Friendly Name</th>
<th>Operator</th>
<th>Base URL</th>
<th>Base ID</th>
<th>Max Lifetime</th>
<th>Key SHA-256</th>
</tr>
</thead>
<tbody id="issuers-body"></tbody>
</table>

<h3>Mirror Cosigners</h3>
<table>
<thead>
<tr>
<th>Environment</th>
<th>Friendly Name</th>
<th>Operator</th>
<th>Base URL</th>
<th>State</th>
<th>Key SHA-256</th>
</tr>
</thead>
<tbody id="mirrors-body"></tbody>
</table>
</div>

<div id="error-message" style="display: none; color: #cf222e; padding: 1em; background: #ffebe9; border-radius: 6px; margin: 1em 0;">
<strong>Failed to load data.</strong> You can view the raw JSON directly at <a href="https://www.gstatic.com/mtcs/cosigners/v1/cosigners.json" target="_blank">cosigners.json</a>.
</div>

<script>
function getRealmBadge(realm) {
if (realm === "PUBLICLY_TRUSTED") {
return `<span style="background: #dafbe1; color: #1a7f37; border: 1px solid #4ac26b; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px;" title="PUBLICLY_TRUSTED (Production)">PROD</span>`;
} else if (realm === "UNTRUSTED_VALIDATION_ONLY") {
return `<span style="background: #fff8c5; color: #9a6700; border: 1px solid #d4a72c; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px;" title="UNTRUSTED_VALIDATION_ONLY (Testing)">TEST</span>`;
} else {
return `<span style="background: #f6f8fa; color: #57606a; border: 1px solid #d0d7de; padding: 2px 8px; border-radius: 4px; font-weight: bold; font-size: 11px;">${realm || "UNSET"}</span>`;
}
}

function formatTimestamp(ts) {
if (!ts) return "N/A";
const d = new Date(ts);
return d.toISOString().replace("T", " ").substring(0, 19) + " UTC";
}

async function loadCosigners() {
const loading = document.getElementById("loading-spinner");
const content = document.getElementById("cosigners-content");
const errorMsg = document.getElementById("error-message");

let data;
try {
const response = await fetch("https://www.gstatic.com/mtcs/cosigners/v1/cosigners.json");
if (!response.ok) throw new Error("HTTP " + response.status);
data = await response.json();
} catch (err) {
console.warn("Direct fetch from gstatic failed (CORS). Falling back to relative static snapshot:", err);
try {
const fallbackUrl = new URL("../../static/cosigners_fallback.json", window.location.href).href;
const fallbackResp = await fetch(fallbackUrl);
if (!fallbackResp.ok) throw new Error("Fallback HTTP " + fallbackResp.status);
data = await fallbackResp.json();
} catch (fallbackErr) {
console.error("Fallback fetch failed:", fallbackErr);
loading.style.display = "none";
errorMsg.style.display = "block";
return;
}
}

try {
document.getElementById("store-version").textContent = data.version || "N/A";
document.getElementById("store-timestamp").textContent = formatTimestamp(data.timestamp);

// Build Issuers table
const issuersBody = document.getElementById("issuers-body");
issuersBody.innerHTML = "";
if (data.issuers && data.issuers.length > 0) {
data.issuers.forEach(item => {
const opName = (item.operator_history && item.operator_history.length > 0) ? item.operator_history[0].name : "Unknown";
const lifetime = item.max_cert_lifetime_seconds ? (item.max_cert_lifetime_seconds / 86400) + " days" : "N/A";
const realmBadge = getRealmBadge(item.realm);
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${realmBadge}</td>
<td><strong>${item.friendly_name || ""}</strong></td>
<td>${opName}</td>
<td><a href="${item.base_url}" target="_blank" rel="noopener">${item.base_url}</a></td>
<td><code>${item.base_id || ""}</code></td>
<td>${lifetime}</td>
<td><code title="${item.key_sha256}">${(item.key_sha256 || "").substring(0, 12)}...</code></td>
`;
issuersBody.appendChild(tr);
});
} else {
issuersBody.innerHTML = '<tr><td colspan="7">No issuer cosigners currently listed.</td></tr>';
}

// Build Mirrors table
const mirrorsBody = document.getElementById("mirrors-body");
mirrorsBody.innerHTML = "";
if (data.mirrors && data.mirrors.length > 0) {
data.mirrors.forEach(item => {
const opName = (item.operator_history && item.operator_history.length > 0) ? item.operator_history[0].name : "Unknown";
const state = (item.state_history && item.state_history.length > 0) ? item.state_history[0].state : "UNKNOWN";
const realmBadge = getRealmBadge(item.realm);
const tr = document.createElement("tr");
tr.innerHTML = `
<td>${realmBadge}</td>
<td><strong>${item.friendly_name || ""}</strong></td>
<td>${opName}</td>
<td><a href="${item.base_url}" target="_blank" rel="noopener">${item.base_url}</a></td>
<td><span style="background: #dafbe1; color: #1a7f37; padding: 2px 6px; border-radius: 4px; font-weight: bold; font-size: 11px;">${state}</span></td>
<td><code title="${item.key_sha256}">${(item.key_sha256 || "").substring(0, 12)}...</code></td>
`;
mirrorsBody.appendChild(tr);
});
} else {
mirrorsBody.innerHTML = '<tr><td colspan="6">No mirror cosigners currently listed.</td></tr>';
}

loading.style.display = "none";
content.style.display = "block";
} catch (renderErr) {
console.error("Rendering error:", renderErr);
loading.style.display = "none";
errorMsg.style.display = "block";
}
}

if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", loadCosigners);
} else {
loadCosigners();
}
</script>
Loading
Loading