Implement the client side of certificate compression.

Change-Id: I0aced480af98276ebfe0970b4afb9aa957ee07cb
Reviewed-on: https://boringssl-review.googlesource.com/29024
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index 45eadad..e788f0b 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -33,6 +33,7 @@
 SSL,274,CERTIFICATE_AND_PRIVATE_KEY_MISMATCH
 SSL,125,CERTIFICATE_VERIFY_FAILED
 SSL,126,CERT_CB_ERROR
+SSL,292,CERT_DECOMPRESSION_FAILED
 SSL,127,CERT_LENGTH_MISMATCH
 SSL,128,CHANNEL_ID_NOT_P256
 SSL,129,CHANNEL_ID_SIGNATURE_INVALID
@@ -191,6 +192,7 @@
 SSL,1117,TOO_MUCH_READ_EARLY_DATA
 SSL,270,TOO_MUCH_SKIPPED_EARLY_DATA
 SSL,221,UNABLE_TO_FIND_ECDH_PARAMETERS
+SSL,293,UNCOMPRESSED_CERT_TOO_LARGE
 SSL,222,UNEXPECTED_EXTENSION
 SSL,279,UNEXPECTED_EXTENSION_ON_EARLY_DATA
 SSL,223,UNEXPECTED_MESSAGE
@@ -199,6 +201,7 @@
 SSL,226,UNINITIALIZED
 SSL,227,UNKNOWN_ALERT_TYPE
 SSL,228,UNKNOWN_CERTIFICATE_TYPE
+SSL,294,UNKNOWN_CERT_COMPRESSION_ALG
 SSL,229,UNKNOWN_CIPHER_RETURNED
 SSL,230,UNKNOWN_CIPHER_TYPE
 SSL,231,UNKNOWN_DIGEST
diff --git a/crypto/pool/pool.c b/crypto/pool/pool.c
index 9cfbf1e..15c7484 100644
--- a/crypto/pool/pool.c
+++ b/crypto/pool/pool.c
@@ -135,6 +135,25 @@
   return buf;
 }
 
+CRYPTO_BUFFER *CRYPTO_BUFFER_alloc(uint8_t **out_data, size_t len) {
+  CRYPTO_BUFFER *const buf = OPENSSL_malloc(sizeof(CRYPTO_BUFFER));
+  if (buf == NULL) {
+    return NULL;
+  }
+  OPENSSL_memset(buf, 0, sizeof(CRYPTO_BUFFER));
+
+  buf->data = OPENSSL_malloc(len);
+  if (len != 0 && buf->data == NULL) {
+    OPENSSL_free(buf);
+    return NULL;
+  }
+  buf->len = len;
+  buf->references = 1;
+
+  *out_data = buf->data;
+  return buf;
+}
+
 CRYPTO_BUFFER* CRYPTO_BUFFER_new_from_CBS(CBS *cbs, CRYPTO_BUFFER_POOL *pool) {
   return CRYPTO_BUFFER_new(CBS_data(cbs), CBS_len(cbs), pool);
 }
diff --git a/include/openssl/pool.h b/include/openssl/pool.h
index 373952f..2c19c88 100644
--- a/include/openssl/pool.h
+++ b/include/openssl/pool.h
@@ -48,6 +48,16 @@
 OPENSSL_EXPORT CRYPTO_BUFFER *CRYPTO_BUFFER_new(const uint8_t *data, size_t len,
                                                 CRYPTO_BUFFER_POOL *pool);
 
+// CRYPTO_BUFFER_alloc creates an unpooled |CRYPTO_BUFFER| of the given size and
+// writes the underlying data pointer to |*out_data|. It returns NULL on error.
+//
+// After calling this function, |len| bytes of contents must be written to
+// |out_data| before passing the returned pointer to any other BoringSSL
+// functions. Once initialized, the |CRYPTO_BUFFER| should be treated as
+// immutable.
+OPENSSL_EXPORT CRYPTO_BUFFER *CRYPTO_BUFFER_alloc(uint8_t **out_data,
+                                                  size_t len);
+
 // CRYPTO_BUFFER_new_from_CBS acts the same as |CRYPTO_BUFFER_new|.
 OPENSSL_EXPORT CRYPTO_BUFFER *CRYPTO_BUFFER_new_from_CBS(
     CBS *cbs, CRYPTO_BUFFER_POOL *pool);
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 2558732..5a44020 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -4793,6 +4793,9 @@
 #define SSL_R_OCSP_CB_ERROR 289
 #define SSL_R_SSL_SESSION_ID_TOO_LONG 290
 #define SSL_R_APPLICATION_DATA_ON_SHUTDOWN 291
+#define SSL_R_CERT_DECOMPRESSION_FAILED 292
+#define SSL_R_UNCOMPRESSED_CERT_TOO_LARGE 293
+#define SSL_R_UNKNOWN_CERT_COMPRESSION_ALG 294
 #define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
 #define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
 #define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index bb9a816..03b218b 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -266,6 +266,10 @@
 #define TLSEXT_hash_sha384 5
 #define TLSEXT_hash_sha512 6
 
+// From https://tools.ietf.org/html/draft-ietf-tls-certificate-compression-03#section-3
+#define TLSEXT_cert_compression_zlib 1
+#define TLSEXT_cert_compression_brotli 2
+
 #define TLSEXT_MAXLEN_host_name 255
 
 // PSK ciphersuites from 4279
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 6ff7948..d6e0713 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -3954,6 +3954,67 @@
   EXPECT_TRUE(SSL_is_signature_algorithm_rsa_pss(SSL_SIGN_RSA_PSS_RSAE_SHA384));
 }
 
+static bool XORCompressFunc(SSL *ssl, CBB *out, Span<const uint8_t> in) {
+  for (size_t i = 0; i < in.size(); i++) {
+    if (!CBB_add_u8(out, in[i] ^ 0x55)) {
+      return false;
+    }
+  }
+
+  SSL_set_app_data(ssl, XORCompressFunc);
+
+  return true;
+}
+
+static bool XORDecompressFunc(SSL *ssl, bssl::UniquePtr<CRYPTO_BUFFER> *out,
+                              size_t uncompressed_len, Span<const uint8_t> in) {
+  if (in.size() != uncompressed_len) {
+    return false;
+  }
+
+  uint8_t *data;
+  out->reset(CRYPTO_BUFFER_alloc(&data, uncompressed_len));
+  if (out->get() == nullptr) {
+    return false;
+  }
+
+  for (size_t i = 0; i < in.size(); i++) {
+    data[i] = in[i] ^ 0x55;
+  }
+
+  SSL_set_app_data(ssl, XORDecompressFunc);
+
+  return true;
+}
+
+TEST(SSLTest, CertCompression) {
+  bssl::UniquePtr<SSL_CTX> client_ctx(SSL_CTX_new(TLS_method()));
+  bssl::UniquePtr<SSL_CTX> server_ctx(SSL_CTX_new(TLS_method()));
+  ASSERT_TRUE(client_ctx);
+  ASSERT_TRUE(server_ctx);
+
+  bssl::UniquePtr<X509> cert = GetTestCertificate();
+  bssl::UniquePtr<EVP_PKEY> key = GetTestKey();
+  ASSERT_TRUE(cert);
+  ASSERT_TRUE(key);
+  ASSERT_TRUE(SSL_CTX_use_certificate(server_ctx.get(), cert.get()));
+  ASSERT_TRUE(SSL_CTX_use_PrivateKey(server_ctx.get(), key.get()));
+
+  ASSERT_TRUE(SSL_CTX_set_max_proto_version(client_ctx.get(), TLS1_3_VERSION));
+  ASSERT_TRUE(SSL_CTX_set_max_proto_version(server_ctx.get(), TLS1_3_VERSION));
+  ASSERT_TRUE(SSL_CTX_add_cert_compression_alg(
+      client_ctx.get(), 0x1234, XORCompressFunc, XORDecompressFunc));
+  ASSERT_TRUE(SSL_CTX_add_cert_compression_alg(
+      server_ctx.get(), 0x1234, XORCompressFunc, XORDecompressFunc));
+
+  bssl::UniquePtr<SSL> client, server;
+  ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx.get(),
+                                     server_ctx.get()));
+
+  EXPECT_TRUE(SSL_get_app_data(client.get()) == XORDecompressFunc);
+  EXPECT_TRUE(SSL_get_app_data(server.get()) == XORCompressFunc);
+}
+
 void MoveBIOs(SSL *dest, SSL *src) {
   BIO *rbio = SSL_get_rbio(src);
   BIO_up_ref(rbio);
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index 4687f52..20b96f4 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -2764,7 +2764,26 @@
 // Certificate compression
 
 static bool cert_compression_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
-  return true;
+  bool first = true;
+  CBB contents, algs;
+
+  for (const auto& alg : hs->ssl->ctx->cert_compression_algs) {
+    if (alg->decompress == nullptr) {
+      continue;
+    }
+
+    if (first && (!CBB_add_u16(out, TLSEXT_TYPE_cert_compression) ||
+                  !CBB_add_u16_length_prefixed(out, &contents) ||
+                  !CBB_add_u8_length_prefixed(&contents, &algs))) {
+      return false;
+    }
+    first = false;
+    if (!CBB_add_u16(&algs, alg->alg_id)) {
+      return false;
+    }
+  }
+
+  return first || CBB_flush(out);
 }
 
 static bool cert_compression_parse_serverhello(SSL_HANDSHAKE *hs,
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 66b4295..56ee0dc 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -1618,6 +1618,14 @@
 	// ExpectedCompressedCert specifies the compression algorithm ID that must be
 	// used on this connection, or zero if there are no special requirements.
 	ExpectedCompressedCert uint16
+
+	// SendCertCompressionAlgId, if not zero, sets the algorithm ID that will be
+	// sent in the compressed certificate message.
+	SendCertCompressionAlgId uint16
+
+	// SendCertUncompressedLength, if not zero, sets the uncompressed length that
+	// will be sent in the compressed certificate message.
+	SendCertUncompressedLength uint32
 }
 
 func (c *Config) serverInit() {
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index c92d979..5324d74 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -1712,7 +1712,7 @@
 	}
 
 	certMsg := newByteBuilder()
-	certMsg.addU8(typeCertificate)
+	certMsg.addU8(typeCompressedCertificate)
 	certificate := certMsg.addU24LengthPrefixed()
 	certificate.addU16(m.algID)
 	certificate.addU24(int(m.uncompressedLength))
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 52c2002..c0653b1 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -885,8 +885,47 @@
 			}
 		}
 		certMsgBytes := certMsg.marshal()
-		hs.writeServerHash(certMsgBytes)
-		c.writeRecord(recordTypeHandshake, certMsgBytes)
+		sentCompressedCertMsg := false
+
+	FindCertCompressionAlg:
+		for candidate, alg := range c.config.CertCompressionAlgs {
+			for _, id := range hs.clientHello.compressedCertAlgs {
+				if id == candidate {
+					if expected := config.Bugs.ExpectedCompressedCert; expected != 0 && expected != id {
+						return fmt.Errorf("expected to send compressed cert with alg %d, but picked %d", expected, id)
+					}
+
+					if override := config.Bugs.SendCertCompressionAlgId; override != 0 {
+						id = override
+					}
+
+					uncompressed := certMsgBytes[4:]
+					uncompressedLen := uint32(len(uncompressed))
+					if override := config.Bugs.SendCertUncompressedLength; override != 0 {
+						uncompressedLen = override
+					}
+
+					compressedCertMsgBytes := (&compressedCertificateMsg{
+						algID:              id,
+						uncompressedLength: uncompressedLen,
+						compressed:         alg.Compress(uncompressed),
+					}).marshal()
+
+					hs.writeServerHash(compressedCertMsgBytes)
+					c.writeRecord(recordTypeHandshake, compressedCertMsgBytes)
+					sentCompressedCertMsg = true
+					break FindCertCompressionAlg
+				}
+			}
+		}
+
+		if !sentCompressedCertMsg {
+			if config.Bugs.ExpectedCompressedCert != 0 {
+				return errors.New("unexpectedly sent uncompressed certificate")
+			}
+			hs.writeServerHash(certMsgBytes)
+			c.writeRecord(recordTypeHandshake, certMsgBytes)
+		}
 
 		certVerify := &certificateVerifyMsg{
 			hasSignatureAlgorithm: true,
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 41fc5e4..29ac50a 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -14424,7 +14424,7 @@
 	// value in the shim.
 	expandingPrefix := []byte{1, 2, 3, 4}
 
-	shinking := CertCompressionAlg{
+	shrinking := CertCompressionAlg{
 		Compress: func(uncompressed []byte) []byte {
 			if !bytes.HasPrefix(uncompressed, shrinkingPrefix) {
 				panic(fmt.Sprintf("cannot compress certificate message %x", uncompressed))
@@ -14540,7 +14540,7 @@
 			config: Config{
 				MinVersion:          ver.version,
 				MaxVersion:          ver.version,
-				CertCompressionAlgs: map[uint16]CertCompressionAlg{shrinkingAlgId: shinking},
+				CertCompressionAlgs: map[uint16]CertCompressionAlg{shrinkingAlgId: shrinking},
 				Bugs: ProtocolBugs{
 					ExpectedCompressedCert: shrinkingAlgId,
 				},
@@ -14558,7 +14558,7 @@
 				MinVersion: ver.version,
 				MaxVersion: ver.version,
 				CertCompressionAlgs: map[uint16]CertCompressionAlg{
-					shrinkingAlgId: shinking,
+					shrinkingAlgId: shrinking,
 					expandingAlgId: expanding,
 				},
 				Bugs: ProtocolBugs{
@@ -14566,6 +14566,100 @@
 				},
 			},
 		})
+
+		testCases = append(testCases, testCase{
+			testType:     clientTest,
+			name:         "CertCompressionExpandsClient-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			flags:        []string{"-install-cert-compression-algs"},
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				CertCompressionAlgs: map[uint16]CertCompressionAlg{
+					expandingAlgId: expanding,
+				},
+				Bugs: ProtocolBugs{
+					ExpectedCompressedCert: expandingAlgId,
+				},
+			},
+		})
+
+		testCases = append(testCases, testCase{
+			testType:     clientTest,
+			name:         "CertCompressionShrinksClient-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			flags:        []string{"-install-cert-compression-algs"},
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				CertCompressionAlgs: map[uint16]CertCompressionAlg{
+					shrinkingAlgId: shrinking,
+				},
+				Bugs: ProtocolBugs{
+					ExpectedCompressedCert: shrinkingAlgId,
+				},
+			},
+		})
+
+		testCases = append(testCases, testCase{
+			testType:     clientTest,
+			name:         "CertCompressionBadAlgIdClient-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			flags:        []string{"-install-cert-compression-algs"},
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				CertCompressionAlgs: map[uint16]CertCompressionAlg{
+					shrinkingAlgId: shrinking,
+				},
+				Bugs: ProtocolBugs{
+					ExpectedCompressedCert:   shrinkingAlgId,
+					SendCertCompressionAlgId: 1234,
+				},
+			},
+			shouldFail:    true,
+			expectedError: ":UNKNOWN_CERT_COMPRESSION_ALG:",
+		})
+
+		testCases = append(testCases, testCase{
+			testType:     clientTest,
+			name:         "CertCompressionTooSmallClient-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			flags:        []string{"-install-cert-compression-algs"},
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				CertCompressionAlgs: map[uint16]CertCompressionAlg{
+					shrinkingAlgId: shrinking,
+				},
+				Bugs: ProtocolBugs{
+					ExpectedCompressedCert:     shrinkingAlgId,
+					SendCertUncompressedLength: 12,
+				},
+			},
+			shouldFail:    true,
+			expectedError: ":CERT_DECOMPRESSION_FAILED:",
+		})
+
+		testCases = append(testCases, testCase{
+			testType:     clientTest,
+			name:         "CertCompressionTooLargeClient-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			flags:        []string{"-install-cert-compression-algs"},
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				CertCompressionAlgs: map[uint16]CertCompressionAlg{
+					shrinkingAlgId: shrinking,
+				},
+				Bugs: ProtocolBugs{
+					ExpectedCompressedCert:     shrinkingAlgId,
+					SendCertUncompressedLength: 1 << 20,
+				},
+			},
+			shouldFail:    true,
+			expectedError: ":UNCOMPRESSED_CERT_TOO_LARGE:",
+		})
 	}
 }
 
diff --git a/ssl/tls13_both.cc b/ssl/tls13_both.cc
index 993dd02..495838c 100644
--- a/ssl/tls13_both.cc
+++ b/ssl/tls13_both.cc
@@ -105,7 +105,61 @@
 int tls13_process_certificate(SSL_HANDSHAKE *hs, const SSLMessage &msg,
                               int allow_anonymous) {
   SSL *const ssl = hs->ssl;
-  CBS body = msg.body, context, certificate_list;
+  CBS body = msg.body;
+  bssl::UniquePtr<CRYPTO_BUFFER> decompressed;
+
+  if (msg.type == SSL3_MT_COMPRESSED_CERTIFICATE) {
+    CBS compressed;
+    uint16_t alg_id;
+    uint32_t uncompressed_len;
+
+    if (!CBS_get_u16(&body, &alg_id) ||
+        !CBS_get_u24(&body, &uncompressed_len) ||
+        !CBS_get_u24_length_prefixed(&body, &compressed) ||
+        CBS_len(&body) != 0) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+      OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
+      return 0;
+    }
+
+    if (uncompressed_len > ssl->max_cert_list) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+      OPENSSL_PUT_ERROR(SSL, SSL_R_UNCOMPRESSED_CERT_TOO_LARGE);
+      ERR_add_error_dataf("requested=%u",
+                          static_cast<unsigned>(uncompressed_len));
+      return 0;
+    }
+
+    bssl::CertDecompressFunc decompress = nullptr;
+    for (const auto& alg : ssl->ctx->cert_compression_algs) {
+      if (alg->alg_id == alg_id) {
+        decompress = alg->decompress;
+        break;
+      }
+    }
+
+    if (decompress == nullptr) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
+      OPENSSL_PUT_ERROR(SSL, SSL_R_UNKNOWN_CERT_COMPRESSION_ALG);
+      ERR_add_error_dataf("alg=%d", static_cast<int>(alg_id));
+      return 0;
+    }
+
+    if (!decompress(ssl, &decompressed, uncompressed_len, compressed) ||
+        CRYPTO_BUFFER_len(decompressed.get()) != uncompressed_len) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_DECODE_ERROR);
+      OPENSSL_PUT_ERROR(SSL, SSL_R_CERT_DECOMPRESSION_FAILED);
+      ERR_add_error_dataf("alg=%d", static_cast<int>(alg_id));
+      return 0;
+    }
+
+    CBS_init(&body, CRYPTO_BUFFER_data(decompressed.get()),
+             CRYPTO_BUFFER_len(decompressed.get()));
+  } else {
+    assert(msg.type == SSL3_MT_CERTIFICATE);
+  }
+
+  CBS context, certificate_list;
   if (!CBS_get_u8_length_prefixed(&body, &context) ||
       CBS_len(&context) != 0 ||
       !CBS_get_u24_length_prefixed(&body, &certificate_list) ||
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index 49f528b..b8bd546 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -547,8 +547,13 @@
   if (!ssl->method->get_message(ssl, &msg)) {
     return ssl_hs_read_message;
   }
-  if (!ssl_check_message_type(ssl, msg, SSL3_MT_CERTIFICATE) ||
-      !tls13_process_certificate(hs, msg, 0 /* certificate required */) ||
+
+  if (msg.type != SSL3_MT_COMPRESSED_CERTIFICATE &&
+      !ssl_check_message_type(ssl, msg, SSL3_MT_CERTIFICATE)) {
+    return ssl_hs_error;
+  }
+
+  if (!tls13_process_certificate(hs, msg, 0 /* certificate required */) ||
       !ssl_hash_message(hs, msg)) {
     return ssl_hs_error;
   }