runner: Revise ECHConfig type in preparation for client implementation

An ECHConfig is like a certificate in that knowing the fields isn't
sufficient. The exact byte representation is significant. (The ECHConfig
is bound into the encryption.) But the ECHConfig type only has fields,
so runner can only represent ECHConfigs that are the output of our
serialization function.

This matters less as a client testing a server because the server can
only parse ECHConfigs with fields we support. But as a server testing a
client, we need to see how the client reacts to extra extensions, etc.

Just using []byte to represent ECHConfigs is inconvenient, so instead
pattern this after x509.Certificate: you can parse one from a byte
string (not currently included since we don't need it yet), or you can
construct a new one from a template with the fields you want.

Bug: 275
Change-Id: I6602d0780b1cef12b6c4b442999bdff7b3d7dd70
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/47964
Reviewed-by: Adam Langley <agl@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 3451137..792ef9f 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -154,7 +154,7 @@
 		}
 
 		info := []byte("tls ech\x00")
-		info = append(info, MarshalECHConfig(c.config.ClientECHConfig)...)
+		info = append(info, c.config.ClientECHConfig.Raw...)
 
 		var echEnc []byte
 		hs.echHPKEContext, echEnc, err = hpke.SetupBaseSenderX25519(echCipherSuite.KDF, echCipherSuite.AEAD, c.config.ClientECHConfig.PublicKey, info, nil)
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 2d89dbd..a4e8136 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -255,6 +255,7 @@
 }
 
 type ECHConfig struct {
+	Raw          []byte
 	ConfigID     uint8
 	KEM          uint16
 	PublicKey    []byte
@@ -263,35 +264,38 @@
 	CipherSuites []HPKECipherSuite
 }
 
-func (e *ECHConfig) marshal(bb *byteBuilder) {
-	// ECHConfig's wire format reuses the encrypted_client_hello extension
-	// codepoint as a version identifier.
+func CreateECHConfig(template *ECHConfig) *ECHConfig {
+	bb := newByteBuilder()
+	// ECHConfig reuses the encrypted_client_hello extension codepoint as a
+	// version identifier.
 	bb.addU16(extensionEncryptedClientHello)
 	contents := bb.addU16LengthPrefixed()
-	contents.addU8(e.ConfigID)
-	contents.addU16(e.KEM)
-	contents.addU16LengthPrefixed().addBytes(e.PublicKey)
+	contents.addU8(template.ConfigID)
+	contents.addU16(template.KEM)
+	contents.addU16LengthPrefixed().addBytes(template.PublicKey)
 	cipherSuites := contents.addU16LengthPrefixed()
-	for _, suite := range e.CipherSuites {
+	for _, suite := range template.CipherSuites {
 		cipherSuites.addU16(suite.KDF)
 		cipherSuites.addU16(suite.AEAD)
 	}
-	contents.addU16(e.MaxNameLen)
-	contents.addU16LengthPrefixed().addBytes([]byte(e.PublicName))
+	contents.addU16(template.MaxNameLen)
+	contents.addU16LengthPrefixed().addBytes([]byte(template.PublicName))
 	contents.addU16(0) // Empty extensions field
+
+	// This ought to be a call to a function like ParseECHConfig(bb.finish()),
+	// but this constrains us to constructing ECHConfigs we are willing to
+	// support. We need to test the client's behavior in response to unparsable
+	// or unsupported ECHConfigs, so populate fields from the template directly.
+	ret := *template
+	ret.Raw = bb.finish()
+	return &ret
 }
 
-func MarshalECHConfig(e *ECHConfig) []byte {
-	bb := newByteBuilder()
-	e.marshal(bb)
-	return bb.finish()
-}
-
-func MarshalECHConfigList(configs ...*ECHConfig) []byte {
+func CreateECHConfigList(configs ...[]byte) []byte {
 	bb := newByteBuilder()
 	list := bb.addU16LengthPrefixed()
 	for _, config := range configs {
-		config.marshal(list)
+		list.addBytes(config)
 	}
 	return bb.finish()
 }
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index d00ab69..7fe500a 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -16206,7 +16206,7 @@
 // private key for the server. If the cipher list is empty, all ciphers are
 // included.
 func generateECHConfigWithKey(publicName string, ciphers []HPKECipherSuite, configID uint8) (*ECHConfig, []byte, error) {
-	publicKeyR, secretKeyR, err := hpke.GenerateKeyPair()
+	publicKey, secretKey, err := hpke.GenerateKeyPair()
 	if err != nil {
 		return nil, nil, err
 	}
@@ -16216,10 +16216,10 @@
 			ciphers = append(ciphers, cipher.cipher)
 		}
 	}
-	result := ECHConfig{
+	template := ECHConfig{
 		ConfigID:     configID,
 		PublicName:   publicName,
-		PublicKey:    publicKeyR,
+		PublicKey:    publicKey,
 		KEM:          hpke.X25519WithHKDFSHA256,
 		CipherSuites: ciphers,
 		// For real-life purposes, the maxNameLen should be
@@ -16227,7 +16227,7 @@
 		// represents.
 		MaxNameLen: 16,
 	}
-	return &result, secretKeyR, nil
+	return CreateECHConfig(&template), secretKey, nil
 }
 
 func addEncryptedClientHelloTests() {
@@ -16278,7 +16278,7 @@
 				},
 				resumeSession: true,
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 					"-expect-server-name", "secret.example",
@@ -16306,7 +16306,7 @@
 				},
 				resumeSession: true,
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 					"-expect-server-name", "secret.example",
@@ -16331,20 +16331,20 @@
 					ClientECHConfig: echConfig,
 					Bugs: ProtocolBugs{
 						OfferSessionInClientHelloOuter: true,
-						ExpectECHRetryConfigs:          MarshalECHConfigList(echConfig2, echConfig3),
+						ExpectECHRetryConfigs:          CreateECHConfigList(echConfig2.Raw, echConfig3.Raw),
 					},
 				},
 				resumeSession: true,
 				flags: []string{
 					// Configure three ECHConfigs on the shim, only two of which
 					// should be sent in retry configs.
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig1)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig1.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key1),
 					"-ech-is-retry-config", "0",
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig2)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig2.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key2),
 					"-ech-is-retry-config", "1",
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig3)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig3.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key3),
 					"-ech-is-retry-config", "1",
 					"-expect-server-name", "public.example",
@@ -16366,7 +16366,7 @@
 					},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1"},
 				shouldFail:         true,
@@ -16390,7 +16390,7 @@
 					},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 				},
@@ -16425,7 +16425,7 @@
 					},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 					"-expect-server-name", "secret.example",
@@ -16457,7 +16457,7 @@
 					},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 				},
@@ -16484,7 +16484,7 @@
 					},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 					"-expect-server-name", "secret.example",
@@ -16514,7 +16514,7 @@
 					},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 				},
@@ -16537,7 +16537,7 @@
 			flags: []string{
 				"-async",
 				"-use-early-callback",
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
 				"-expect-server-name", "secret.example",
@@ -16559,10 +16559,10 @@
 				ClientECHConfig: echConfig1,
 			},
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig1)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig1.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key1),
 				"-ech-is-retry-config", "1",
 				"-expect-server-name", "secret.example",
@@ -16584,10 +16584,10 @@
 				ClientECHConfig: echConfigRepeatID,
 			},
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfigRepeatID)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfigRepeatID.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(keyRepeatID),
 				"-ech-is-retry-config", "1",
 				"-expect-server-name", "secret.example",
@@ -16616,7 +16616,7 @@
 					ECHCipherSuites: []HPKECipherSuite{cipher.cipher},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 					"-expect-server-name", "secret.example",
@@ -16642,11 +16642,11 @@
 					ClientECHConfig: echConfig,
 					ECHCipherSuites: []HPKECipherSuite{cipher.cipher},
 					Bugs: ProtocolBugs{
-						ExpectECHRetryConfigs: MarshalECHConfigList(config),
+						ExpectECHRetryConfigs: CreateECHConfigList(config.Raw),
 					},
 				},
 				flags: []string{
-					"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(config)),
+					"-ech-server-config", base64.StdEncoding.EncodeToString(config.Raw),
 					"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 					"-ech-is-retry-config", "1",
 					"-expect-server-name", "public.example",
@@ -16664,12 +16664,12 @@
 				ServerName:      "secret.example",
 				ClientECHConfig: echConfig,
 				Bugs: ProtocolBugs{
-					ExpectECHRetryConfigs: MarshalECHConfigList(echConfig),
+					ExpectECHRetryConfigs: CreateECHConfigList(echConfig.Raw),
 					TruncateClientECHEnc:  true,
 				},
 			},
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
 				"-expect-server-name", "public.example",
@@ -16686,12 +16686,12 @@
 				ServerName:      "secret.example",
 				ClientECHConfig: echConfig,
 				Bugs: ProtocolBugs{
-					ExpectECHRetryConfigs:       MarshalECHConfigList(echConfig),
+					ExpectECHRetryConfigs:       CreateECHConfigList(echConfig.Raw),
 					CorruptEncryptedClientHello: true,
 				},
 			},
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
 			},
@@ -16713,7 +16713,7 @@
 				},
 			},
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
 			},
@@ -16737,7 +16737,7 @@
 				},
 			},
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
 			},
@@ -16761,7 +16761,7 @@
 				},
 			},
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
 			},
@@ -16782,7 +16782,7 @@
 			resumeSession: true,
 			earlyData:     true,
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
 				"-expect-ech-accept",
@@ -16807,7 +16807,7 @@
 			earlyData:               true,
 			expectEarlyDataRejected: true,
 			flags: []string{
-				"-ech-server-config", base64.StdEncoding.EncodeToString(MarshalECHConfig(echConfig)),
+				"-ech-server-config", base64.StdEncoding.EncodeToString(echConfig.Raw),
 				"-ech-server-key", base64.StdEncoding.EncodeToString(key),
 				"-ech-is-retry-config", "1",
 				"-expect-ech-accept",
@@ -16868,26 +16868,7 @@
 			flags: []string{"-enable-ech-grease", "-expect-hrr"},
 		})
 
-		retryConfigValid := ECHConfig{
-			ConfigID:   42,
-			PublicName: "example.com",
-			// A real X25519 public key obtained from hpke.GenerateKeyPair().
-			PublicKey: []byte{
-				0x23, 0x1a, 0x96, 0x53, 0x52, 0x81, 0x1d, 0x7a,
-				0x36, 0x76, 0xaa, 0x5e, 0xad, 0xdb, 0x66, 0x1c,
-				0x92, 0x45, 0x8a, 0x60, 0xc7, 0x81, 0x93, 0xb0,
-				0x47, 0x7b, 0x54, 0x18, 0x6b, 0x9a, 0x1d, 0x6d},
-			KEM: hpke.X25519WithHKDFSHA256,
-			CipherSuites: []HPKECipherSuite{
-				{
-					KDF:  hpke.HKDFSHA256,
-					AEAD: hpke.AES256GCM,
-				},
-			},
-			MaxNameLen: 42,
-		}
-
-		retryConfigUnsupportedVersion := []byte{
+		unsupportedVersion := []byte{
 			// version
 			0xba, 0xdd,
 			// length
@@ -16896,12 +16877,6 @@
 			0x05, 0x04, 0x03, 0x02, 0x01,
 		}
 
-		validAndUnsupportedConfigsBuilder := newByteBuilder()
-		validAndUnsupportedConfigsBody := validAndUnsupportedConfigsBuilder.addU16LengthPrefixed()
-		validAndUnsupportedConfigsBody.addBytes(MarshalECHConfig(&retryConfigValid))
-		validAndUnsupportedConfigsBody.addBytes(retryConfigUnsupportedVersion)
-		validAndUnsupportedConfigs := validAndUnsupportedConfigsBuilder.finish()
-
 		// Test that the client accepts a well-formed encrypted_client_hello
 		// extension in response to ECH GREASE. The response includes one ECHConfig
 		// with a supported version and one with an unsupported version.
@@ -16917,7 +16892,7 @@
 					// Include an additional well-formed ECHConfig with an
 					// unsupported version. This ensures the client can skip
 					// unsupported configs.
-					SendECHRetryConfigs: validAndUnsupportedConfigs,
+					SendECHRetryConfigs: CreateECHConfigList(echConfig.Raw, unsupportedVersion),
 				},
 			},
 			flags: []string{"-enable-ech-grease"},
@@ -16934,7 +16909,7 @@
 					MaxVersion: VersionTLS12,
 					Bugs: ProtocolBugs{
 						ExpectClientECH:                       true,
-						SendECHRetryConfigs:                   validAndUnsupportedConfigs,
+						SendECHRetryConfigs:                   CreateECHConfigList(echConfig.Raw, unsupportedVersion),
 						SendECHRetryConfigsInTLS12ServerHello: true,
 					},
 				},