Add post-quantum experiment signal extension.

When testing HRSS-SXY and SIKE, we also want a control group. However,
how are clients to indicate that they're part of the 1/3 of the
experiment population that's not advertising CECPQ? And how are servers
to indicate that they would have negotiated CECPQ2 / 2b if only the
client had asked?

This change adds a temporary signaling extension to solve these issues.

Change-Id: Ic087a09149ef10141568b734396981ae97950a9b
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/36725
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 47fb851..c0247ca 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -3034,6 +3034,19 @@
 OPENSSL_EXPORT const char *SSL_get_psk_identity(const SSL *ssl);
 
 
+// Post-quantum experiment signaling extension.
+//
+// *** EXPERIMENTAL ***
+//
+// In order to define a control group in an experiment of post-quantum key
+// agreements, clients and servers may send a non-IANA defined extension as a
+// signaling bit. These functions should not be used without explicit permission
+// from BoringSSL-team.
+
+OPENSSL_EXPORT int SSL_enable_pq_experiment_signal(SSL *ssl);
+OPENSSL_EXPORT int SSL_pq_experiment_signal_seen(const SSL *ssl);
+
+
 // QUIC transport parameters.
 //
 // draft-ietf-quic-tls defines a new TLS extension quic_transport_parameters
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index 384d102..f7766f2 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -244,6 +244,9 @@
 // This is not an IANA defined extension number
 #define TLSEXT_TYPE_channel_id 30032
 
+// This is not an IANA defined extension number
+#define TLSEXT_TYPE_pq_experiment_signal 54538
+
 // status request value from RFC 3546
 #define TLSEXT_STATUSTYPE_nothing (-1)
 #define TLSEXT_STATUSTYPE_ocsp 1
diff --git a/ssl/internal.h b/ssl/internal.h
index bc17af9..2598058 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -2254,6 +2254,10 @@
   // token_binding_negotiated is set if Token Binding was negotiated.
   bool token_binding_negotiated : 1;
 
+  // pq_experimental_signal_seen is true if the peer was observed
+  // sending/echoing the post-quantum experiment signal.
+  bool pq_experiment_signal_seen : 1;
+
   // hs_buf is the buffer of handshake data to process.
   UniquePtr<BUF_MEM> hs_buf;
 
@@ -2584,6 +2588,11 @@
   // jdk11_workaround is whether to disable TLS 1.3 for JDK 11 clients, as a
   // workaround for https://bugs.openjdk.java.net/browse/JDK-8211806.
   bool jdk11_workaround : 1;
+
+  // pq_experiment_signal indicates that an empty extension should be sent
+  // (for clients) or echoed (for servers) to indicate participation in an
+  // experiment of post-quantum key exchanges.
+  bool pq_experiment_signal : 1;
 };
 
 // From RFC 8446, used in determining PSK modes.
diff --git a/ssl/s3_lib.cc b/ssl/s3_lib.cc
index b6d905d..75e1f68 100644
--- a/ssl/s3_lib.cc
+++ b/ssl/s3_lib.cc
@@ -179,7 +179,8 @@
       wpend_pending(false),
       early_data_accepted(false),
       tls13_downgrade(false),
-      token_binding_negotiated(false) {}
+      token_binding_negotiated(false),
+      pq_experiment_signal_seen(false) {}
 
 SSL3_STATE::~SSL3_STATE() {}
 
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index f27f2b8..45ed62f 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -734,7 +734,8 @@
       handoff(false),
       shed_handshake_config(false),
       ignore_tls13_downgrade(false),
-      jdk11_workaround(false) {
+      jdk11_workaround(false),
+      pq_experiment_signal(false) {
   assert(ssl);
 }
 
@@ -1245,6 +1246,18 @@
   return ssl_send_alert_impl(ssl, SSL3_AL_FATAL, alert);
 }
 
+int SSL_enable_pq_experiment_signal(SSL *ssl) {
+  if (!ssl->config) {
+    return 0;
+  }
+  ssl->config->pq_experiment_signal = true;
+  return 1;
+}
+
+int SSL_pq_experiment_signal_seen(const SSL *ssl) {
+  return ssl->s3->pq_experiment_signal_seen;
+}
+
 int SSL_set_quic_transport_params(SSL *ssl, const uint8_t *params,
                                   size_t params_len) {
   return ssl->config && ssl->config->quic_transport_params.CopyFrom(
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index 3388556..c05e2c6 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -2886,6 +2886,67 @@
   return true;
 }
 
+
+// Post-quantum experiment signal
+//
+// This extension may be used in order to identify a control group for
+// experimenting with post-quantum key exchange algorithms.
+
+static bool ext_pq_experiment_signal_add_clienthello(SSL_HANDSHAKE *hs,
+                                                     CBB *out) {
+  if (hs->config->pq_experiment_signal &&
+      (!CBB_add_u16(out, TLSEXT_TYPE_pq_experiment_signal) ||
+       !CBB_add_u16(out, 0))) {
+    return false;
+  }
+
+  return true;
+}
+
+static bool ext_pq_experiment_signal_parse_serverhello(SSL_HANDSHAKE *hs,
+                                                       uint8_t *out_alert,
+                                                       CBS *contents) {
+  if (contents == nullptr) {
+    return true;
+  }
+
+  if (!hs->config->pq_experiment_signal || CBS_len(contents) != 0) {
+    return false;
+  }
+
+  hs->ssl->s3->pq_experiment_signal_seen = true;
+  return true;
+}
+
+static bool ext_pq_experiment_signal_parse_clienthello(SSL_HANDSHAKE *hs,
+                                                       uint8_t *out_alert,
+                                                       CBS *contents) {
+  if (contents == nullptr) {
+    return true;
+  }
+
+  if (CBS_len(contents) != 0) {
+    return false;
+  }
+
+  if (hs->ssl->config->pq_experiment_signal) {
+    hs->ssl->s3->pq_experiment_signal_seen = true;
+  }
+
+  return true;
+}
+
+static bool ext_pq_experiment_signal_add_serverhello(SSL_HANDSHAKE *hs,
+                                                     CBB *out) {
+  if (hs->ssl->s3->pq_experiment_signal_seen &&
+      (!CBB_add_u16(out, TLSEXT_TYPE_pq_experiment_signal) ||
+       !CBB_add_u16(out, 0))) {
+    return false;
+  }
+
+  return true;
+}
+
 // kExtensions contains all the supported extensions.
 static const struct tls_extension kExtensions[] = {
   {
@@ -3074,6 +3135,14 @@
     ext_delegated_credential_parse_clienthello,
     dont_add_serverhello,
   },
+  {
+    TLSEXT_TYPE_pq_experiment_signal,
+    NULL,
+    ext_pq_experiment_signal_add_clienthello,
+    ext_pq_experiment_signal_parse_serverhello,
+    ext_pq_experiment_signal_parse_clienthello,
+    ext_pq_experiment_signal_add_serverhello,
+  },
 };
 
 #define kNumExtensions (sizeof(kExtensions) / sizeof(struct tls_extension))
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 98da2ec..dfcb2c7 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -672,6 +672,14 @@
             SSL_delegated_credential_used(ssl) ? "" : "no");
     return false;
   }
+
+  if (config->expect_pq_experiment_signal !=
+      !!SSL_pq_experiment_signal_seen(ssl)) {
+    fprintf(stderr, "Got %sPQ experiment signal, but wanted opposite. \n",
+            SSL_pq_experiment_signal_seen(ssl) ? "" : "no ");
+    return false;
+  }
+
   return true;
 }
 
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 1729289..9930bd3 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -126,6 +126,7 @@
 	extensionQUICTransportParams        uint16 = 0xffa5 // draft-ietf-quic-tls-13
 	extensionChannelID                  uint16 = 30032  // not IANA assigned
 	extensionDelegatedCredentials       uint16 = 0xff02 // not IANA assigned
+	extensionPQExperimentSignal         uint16 = 54538
 )
 
 // TLS signaling cipher suite values
@@ -500,6 +501,11 @@
 
 	CertCompressionAlgs map[uint16]CertCompressionAlg
 
+	// PQExperimentSignal instructs a client to send a non-IANA defined extension
+	// that signals participation in an experiment of post-quantum key exchange
+	// methods.
+	PQExperimentSignal bool
+
 	// Bugs specifies optional misbehaviour to be used for testing other
 	// implementations.
 	Bugs ProtocolBugs
@@ -1650,6 +1656,10 @@
 	// DisableDelegatedCredentials, if true, disables client support for delegated
 	// credentials.
 	DisableDelegatedCredentials bool
+
+	// ExpectPQExperimentSignal specifies whether or not the post-quantum
+	// experiment signal should be received by a client or server.
+	ExpectPQExperimentSignal bool
 }
 
 func (c *Config) serverInit() {
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 45dc75d..2574ec3 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -129,6 +129,7 @@
 		omitExtensions:          c.config.Bugs.OmitExtensions,
 		emptyExtensions:         c.config.Bugs.EmptyExtensions,
 		delegatedCredentials:    !c.config.Bugs.DisableDelegatedCredentials,
+		pqExperimentSignal:      c.config.PQExperimentSignal,
 	}
 
 	if maxVersion >= VersionTLS13 {
@@ -1666,6 +1667,10 @@
 		c.quicTransportParams = serverExtensions.quicTransportParams
 	}
 
+	if c.config.Bugs.ExpectPQExperimentSignal != serverExtensions.pqExperimentSignal {
+		return fmt.Errorf("tls: PQ experiment signal presence (%t) was not what was expected", serverExtensions.pqExperimentSignal)
+	}
+
 	return nil
 }
 
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index f12ca1a..ac52eed 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -298,6 +298,7 @@
 	pad                     int
 	compressedCertAlgs      []uint16
 	delegatedCredentials    bool
+	pqExperimentSignal      bool
 }
 
 func (m *clientHelloMsg) equal(i interface{}) bool {
@@ -352,7 +353,8 @@
 		m.emptyExtensions == m1.emptyExtensions &&
 		m.pad == m1.pad &&
 		eqUint16s(m.compressedCertAlgs, m1.compressedCertAlgs) &&
-		m.delegatedCredentials == m1.delegatedCredentials
+		m.delegatedCredentials == m1.delegatedCredentials &&
+		m.pqExperimentSignal == m1.pqExperimentSignal
 }
 
 func (m *clientHelloMsg) marshalKeyShares(bb *byteBuilder) {
@@ -598,6 +600,11 @@
 		extensions.addU16(extensionDelegatedCredentials)
 		extensions.addU16(0) // Length is always 0
 	}
+	if m.pqExperimentSignal {
+		extensions.addU16(extensionPQExperimentSignal)
+		extensions.addU16(0) // Length is always 0
+	}
+
 	// The PSK extension must be last. See https://tools.ietf.org/html/rfc8446#section-4.2.11
 	if len(m.pskIdentities) > 0 && !m.pskBinderFirst {
 		extensions.addU16(extensionPreSharedKey)
@@ -724,6 +731,7 @@
 	m.extendedMasterSecret = false
 	m.customExtension = ""
 	m.delegatedCredentials = false
+	m.pqExperimentSignal = false
 
 	if len(reader) == 0 {
 		// ClientHello is optionally followed by extension data
@@ -959,6 +967,11 @@
 				return false
 			}
 			m.delegatedCredentials = true
+		case extensionPQExperimentSignal:
+			if len(body) != 0 {
+				return false
+			}
+			m.pqExperimentSignal = true
 		}
 
 		if isGREASEValue(extension) {
@@ -1226,6 +1239,7 @@
 	supportedCurves         []CurveID
 	quicTransportParams     []byte
 	serverNameAck           bool
+	pqExperimentSignal      bool
 }
 
 func (m *serverExtensions) marshal(extensions *byteBuilder) {
@@ -1360,6 +1374,10 @@
 		extensions.addU16(extensionServerName)
 		extensions.addU16(0) // zero length
 	}
+	if m.pqExperimentSignal {
+		extensions.addU16(extensionPQExperimentSignal)
+		extensions.addU16(0) // zero length
+	}
 }
 
 func (m *serverExtensions) unmarshal(data byteReader, version uint16) bool {
@@ -1468,6 +1486,11 @@
 				return false
 			}
 			m.hasEarlyData = true
+		case extensionPQExperimentSignal:
+			if len(body) != 0 {
+				return false
+			}
+			m.pqExperimentSignal = true
 		default:
 			// Unknown extensions are illegal from the server.
 			return false
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 2dc7032..1eb92bf 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -228,6 +228,10 @@
 		}
 	}
 
+	if c.config.Bugs.ExpectPQExperimentSignal != hs.clientHello.pqExperimentSignal {
+		return fmt.Errorf("tls: PQ experiment signal presence (%t) was not what was expected", hs.clientHello.pqExperimentSignal)
+	}
+
 	c.clientVersion = hs.clientHello.vers
 
 	// Use the versions extension if supplied, otherwise use the legacy ClientHello version.
@@ -1456,6 +1460,7 @@
 	}
 
 	serverExtensions.serverNameAck = c.config.Bugs.SendServerNameAck
+	serverExtensions.pqExperimentSignal = hs.clientHello.pqExperimentSignal
 
 	return nil
 }
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index ee09faf..316373a 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -15531,6 +15531,67 @@
 	})
 }
 
+func addPQExperimentSignalTests() {
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "PQExperimentSignal-Server-NoEchoIfNotConfigured",
+		config: Config{
+			MinVersion: VersionTLS13,
+			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				ExpectPQExperimentSignal: false,
+			},
+			PQExperimentSignal: true,
+		},
+	})
+
+	testCases = append(testCases, testCase{
+		testType: serverTest,
+		name:     "PQExperimentSignal-Server-Echo",
+		config: Config{
+			MinVersion: VersionTLS13,
+			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				ExpectPQExperimentSignal: true,
+			},
+			PQExperimentSignal: true,
+		},
+		flags: []string{
+			"-enable-pq-experiment-signal",
+			"-expect-pq-experiment-signal",
+		},
+	})
+
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "PQExperimentSignal-Client-NotDefault",
+		config: Config{
+			MinVersion: VersionTLS13,
+			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				ExpectPQExperimentSignal: false,
+			},
+			PQExperimentSignal: true,
+		},
+	})
+
+	testCases = append(testCases, testCase{
+		testType: clientTest,
+		name:     "PQExperimentSignal-Client",
+		config: Config{
+			MinVersion: VersionTLS13,
+			MaxVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				ExpectPQExperimentSignal: true,
+			},
+		},
+		flags: []string{
+			"-enable-pq-experiment-signal",
+			"-expect-pq-experiment-signal",
+		},
+	})
+}
+
 func worker(statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {
 	defer wg.Done()
 
@@ -15668,6 +15729,7 @@
 	addCertCompressionTests()
 	addJDK11WorkaroundTests()
 	addDelegatedCredentialTests()
+	addPQExperimentSignalTests()
 
 	testCases = append(testCases, convertToSplitHandshakeTests(testCases)...)
 
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index bad295e..8de81f5 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -150,6 +150,8 @@
     {"-key-update", &TestConfig::key_update},
     {"-expect-delegated-credential-used",
      &TestConfig::expect_delegated_credential_used},
+    {"-enable-pq-experiment-signal", &TestConfig::enable_pq_experiment_signal},
+    {"-expect-pq-experiment-signal", &TestConfig::expect_pq_experiment_signal},
 };
 
 const Flag<std::string> kStringFlags[] = {
@@ -1714,5 +1716,11 @@
     }
   }
 
+  if (enable_pq_experiment_signal &&
+      !SSL_enable_pq_experiment_signal(ssl.get())) {
+    fprintf(stderr, "SSL_enable_pq_experiment_signal failed.\n");
+    return nullptr;
+  }
+
   return ssl;
 }
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index fc2dded..57bf66a 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -176,6 +176,8 @@
   bool expect_delegated_credential_used = false;
   std::string delegated_credential;
   std::string expect_early_data_reason;
+  bool enable_pq_experiment_signal = false;
+  bool expect_pq_experiment_signal = false;
 
   int argc;
   char **argv;