diff --git a/gpc/META.yml b/gpc/META.yml
new file mode 100644
index 00000000000000..e9bbee032d854f
--- /dev/null
+++ b/gpc/META.yml
@@ -0,0 +1,7 @@
+spec: https://www.w3.org/TR/gpc/
+suggested_reviewers:
+ - AramZS
+ - bvandersloot-mozilla
+ - j-br0
+ - pes10k
+ - SebastianZimmeck
diff --git a/gpc/WEB_FEATURES.yml b/gpc/WEB_FEATURES.yml
new file mode 100644
index 00000000000000..02c6a0f383eaa8
--- /dev/null
+++ b/gpc/WEB_FEATURES.yml
@@ -0,0 +1,3 @@
+features:
+- name: gpc
+ files: "**"
diff --git a/gpc/global_privacy_control.testdriver.html b/gpc/global_privacy_control.testdriver.html
new file mode 100644
index 00000000000000..138bba2eb4528f
--- /dev/null
+++ b/gpc/global_privacy_control.testdriver.html
@@ -0,0 +1,24 @@
+
+
+
GPC Testdriver Validation
+
+
+
+
+
diff --git a/gpc/idlharness.any.js b/gpc/idlharness.any.js
new file mode 100644
index 00000000000000..7a7305eb623535
--- /dev/null
+++ b/gpc/idlharness.any.js
@@ -0,0 +1,15 @@
+// META: global=window,worker
+// META: script=/resources/WebIDLParser.js
+// META: script=/resources/idlharness.js
+
+idl_test(
+ ['gpc'],
+ ['html'],
+ idl_array => {
+ if (self.Window) {
+ idl_array.add_objects({ Navigator: ['navigator'] });
+ } else {
+ idl_array.add_objects({ WorkerNavigator: ['navigator'] });
+ }
+ }
+);
diff --git a/gpc/navigator-globalPrivacyControl.https.html b/gpc/navigator-globalPrivacyControl.https.html
new file mode 100644
index 00000000000000..6ddc7ffca4194a
--- /dev/null
+++ b/gpc/navigator-globalPrivacyControl.https.html
@@ -0,0 +1,33 @@
+
+
+Primary navigator.globalPrivacyControl test window
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gpc/sec-gpc.https.html b/gpc/sec-gpc.https.html
new file mode 100644
index 00000000000000..6acc350d241cab
--- /dev/null
+++ b/gpc/sec-gpc.https.html
@@ -0,0 +1,33 @@
+
+
+Primary Sec-GPC test window
+
+
+
+
+
+
+
+
+
+
+
diff --git a/gpc/support/getGPC.py b/gpc/support/getGPC.py
new file mode 100644
index 00000000000000..d1c46aac254f9d
--- /dev/null
+++ b/gpc/support/getGPC.py
@@ -0,0 +1,90 @@
+import base64
+
+def maybeBoolToJavascriptLiteral(value):
+ if value == None:
+ return "undefined"
+ if value == True:
+ return "true"
+ if value == False:
+ return "false"
+ raise ValueError("Expected bool or None")
+
+def main(request, response):
+ destination = request.headers.get("sec-fetch-dest").decode("utf-8")
+ gpcValue = request.headers.get("sec-gpc") == b'1'
+ expectedGPCValue = request.GET.get(b"gpc") == b"true"
+ inFrame = request.GET.get(b"framed") != None
+ destinationDescription = "framed " + destination if inFrame else destination
+ if destination == "document" or destination == "iframe":
+ response.headers.set('Content-Type', 'text/html');
+ return f"""
+
+
+Sec-GPC {destination}
+
+
+
+
+
+
+
+""" + (f"""
+
+ """ if destination == "document" else "") + f"""
+
+
+
+"""
+ elif destination == "image":
+ if gpcValue == expectedGPCValue:
+ return (200, [(b"Content-Type", b"image/png")], base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEUlEQVR42mP8nzaTAQQYYQwALssD/5ca+r8AAAAASUVORK5CYII="))
+ return (400, [], "")
+ elif destination == "script":
+ response.headers.set('Content-Type', 'application/javascript');
+ return f"""
+debugger;
+test(function(t) {{
+ assert_equals({maybeBoolToJavascriptLiteral(gpcValue)}, {maybeBoolToJavascriptLiteral(expectedGPCValue)}, "Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch");
+}}, `Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch`);
+"""
+ elif destination == "worker" or destination == "sharedworker" or destination == "serviceworker":
+ response.headers.set('Content-Type', 'application/javascript');
+ return f"""
+importScripts("/resources/testharness.js");
+test(function(t) {{
+ assert_equals({maybeBoolToJavascriptLiteral(gpcValue)}, {maybeBoolToJavascriptLiteral(expectedGPCValue)}, "Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch");
+}}, `Expected Sec-GPC value ({maybeBoolToJavascriptLiteral(expectedGPCValue)}) is on the {destinationDescription} fetch`);
+done();
+"""
+ raise ValueError("Unexpected destination")
diff --git a/gpc/support/navigator-globalPrivacyControl.html b/gpc/support/navigator-globalPrivacyControl.html
new file mode 100644
index 00000000000000..270023ed9cf81a
--- /dev/null
+++ b/gpc/support/navigator-globalPrivacyControl.html
@@ -0,0 +1,35 @@
+
+
+navigator.globalPrivacyControl Window
+
+
+
+
+
+
+
+
diff --git a/gpc/support/navigator-globalPrivacyControl.js b/gpc/support/navigator-globalPrivacyControl.js
new file mode 100644
index 00000000000000..beb9ea372cd44b
--- /dev/null
+++ b/gpc/support/navigator-globalPrivacyControl.js
@@ -0,0 +1,11 @@
+importScripts("/resources/testharness.js");
+
+const queryString = self.location.search;
+const urlParams = new URLSearchParams(queryString);
+const expectedValue = urlParams.has("gpc", "true");
+const workerType = urlParams.get("workerType");
+test(function(t) {
+ assert_equals(navigator.globalPrivacyControl, expectedValue, "Expected navigator.globalPrivacyControl value is read from the worker");
+}, `Expected navigator.globalPrivacyControl value (${expectedValue}) is read from the ${workerType} worker`);
+
+done();