Preliminary support for compressed certificates.

This change adds server-side support for compressed certificates.

(Although some definitions for client-side support are included in the
headers, there's no code behind them yet.)

Change-Id: I0f98abf0b782b7337ddd014c58e19e6b8cc5a3c2
Reviewed-on: https://boringssl-review.googlesource.com/27964
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 96cd7e5..1218a46 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -4565,6 +4565,50 @@
 BORINGSSL_MAKE_DELETER(SSL_CTX, SSL_CTX_free)
 BORINGSSL_MAKE_DELETER(SSL_SESSION, SSL_SESSION_free)
 
+// Certificate compression.
+//
+// Certificates in TLS 1.3 can be compressed[1]. BoringSSL supports this as both
+// a client and a server, but does not link against any specific compression
+// libraries in order to keep dependencies to a minimum. Instead, hooks for
+// compression and decompression can be installed in an |SSL_CTX| to enable
+// support.
+//
+// [1] https://tools.ietf.org/html/draft-ietf-tls-certificate-compression-03.
+
+// CertCompressFunc is a pointer to a function that performs compression. It
+// must write the compressed representation of |in| to |out|, returning one on
+// success and zero on error. The results of compressing certificates are not
+// cached internally. Implementations may wish to implement their own cache if
+// they expect it to be useful given the certificates that they serve.
+typedef bool (*CertCompressFunc)(SSL *ssl, CBB *out, Span<const uint8_t> in);
+
+// CertDecompressFunc is a pointer to a function that performs decompression.
+// The compressed data from the peer is passed as |in| and the decompressed
+// result must be exactly |uncompressed_len| bytes long. It returns one on
+// success, in which case |*out| must be set to the results of decompressing
+// |in|, or zero on error. The results of decompression are not cached
+// internally. Implementations may wish to implement their own cache if they
+// expect it to be useful.
+typedef bool (*CertDecompressFunc)(SSL *ssl,
+                                   bssl::UniquePtr<CRYPTO_BUFFER> *out,
+                                   size_t uncompressed_len,
+                                   Span<const uint8_t> in);
+
+// SSL_CTX_add_cert_compression_alg registers a certificate compression
+// algorithm on |ctx| with ID |alg_id|. (The value of |alg_id| should be an IANA
+// assigned value and each can only be registered once.)
+//
+// One of the function pointers may be nullptr to avoid having to implement both
+// sides of a compression algorithm if you're only going to use it in one
+// direction. In this case, the unimplemented direction acts like it was never
+// configured.
+//
+// For a server, algorithms are registered in preference order with the most
+// preferable first. It returns one on success or zero on error.
+OPENSSL_EXPORT int SSL_CTX_add_cert_compression_alg(
+    SSL_CTX *ctx, uint16_t alg_id, CertCompressFunc compress,
+    CertDecompressFunc decompress);
+
 enum class OpenRecordResult {
   kOK,
   kDiscard,
diff --git a/include/openssl/ssl3.h b/include/openssl/ssl3.h
index e32a6d7..67d06f4 100644
--- a/include/openssl/ssl3.h
+++ b/include/openssl/ssl3.h
@@ -311,6 +311,7 @@
 #define SSL3_MT_CERTIFICATE_STATUS 22
 #define SSL3_MT_SUPPLEMENTAL_DATA 23
 #define SSL3_MT_KEY_UPDATE 24
+#define SSL3_MT_COMPRESSED_CERTIFICATE 25
 #define SSL3_MT_NEXT_PROTO 67
 #define SSL3_MT_CHANNEL_ID 203
 #define SSL3_MT_MESSAGE_HASH 254
diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h
index 4b25806..bb9a816 100644
--- a/include/openssl/tls1.h
+++ b/include/openssl/tls1.h
@@ -205,9 +205,15 @@
 // ExtensionType value from draft-ietf-tokbind-negotiation-10
 #define TLSEXT_TYPE_token_binding 24
 
-// ExtensionType value from draft-ietf-quic-tls
+// ExtensionType value from draft-ietf-quic-tls. Note that this collides with
+// TLS-LTS and, based on scans, something else too. Since it's QUIC-only, that
+// shouldn't be a problem in practice.
 #define TLSEXT_TYPE_quic_transport_parameters 26
 
+// ExtensionType value assigned to
+// https://tools.ietf.org/html/draft-ietf-tls-certificate-compression-03
+#define TLSEXT_TYPE_cert_compression 27
+
 // ExtensionType value from RFC4507
 #define TLSEXT_TYPE_session_ticket 35
 
diff --git a/ssl/handshake.cc b/ssl/handshake.cc
index bd304eb..1f3f29b 100644
--- a/ssl/handshake.cc
+++ b/ssl/handshake.cc
@@ -147,7 +147,8 @@
       extended_master_secret(false),
       pending_private_key_op(false),
       grease_seeded(false),
-      handback(false) {
+      handback(false),
+      cert_compression_negotiated(false) {
   assert(ssl);
 }
 
diff --git a/ssl/internal.h b/ssl/internal.h
index 3deedc4..f8214e8 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1481,6 +1481,11 @@
   // sent.
   uint16_t negotiated_token_binding_version;
 
+  // cert_compression_alg_id, for a server, contains the negotiated certificate
+  // compression algorithm for this client. It is only valid if
+  // |cert_compression_negotiated| is true.
+  uint16_t cert_compression_alg_id;
+
   // server_params, in a TLS 1.2 server, stores the ServerKeyExchange
   // parameters. It has client and server randoms prepended for signing
   // convenience.
@@ -1595,6 +1600,14 @@
   // grease_seeded is true if |grease_seed| has been initialized.
   bool grease_seeded:1;
 
+  // handback indicates that a server should pause the handshake after
+  // finishing operations that require private key material, in such a way that
+  // |SSL_get_error| returns |SSL_HANDBACK|.  It is set by |SSL_apply_handoff|.
+  bool handback:1;
+
+  // cert_compression_negotiated is true iff |cert_compression_alg_id| is valid.
+  bool cert_compression_negotiated:1;
+
   // client_version is the value sent or received in the ClientHello version.
   uint16_t client_version = 0;
 
@@ -1619,11 +1632,6 @@
   // should be echoed in a ServerHello, or zero if no extension should be
   // echoed.
   uint16_t dummy_pq_padding_len = 0;
-
-  // handback indicates that a server should pause the handshake after
-  // finishing operations that require private key material, in such a way that
-  // |SSL_get_error| returns |SSL_HANDBACK|.  It is set by |SSL_apply_handoff|.
-  bool handback : 1;
 };
 
 UniquePtr<SSL_HANDSHAKE> ssl_handshake_new(SSL *ssl);
@@ -1987,6 +1995,14 @@
 
 DECLARE_LHASH_OF(SSL_SESSION)
 
+struct CertCompressionAlg {
+  bssl::CertCompressFunc compress;
+  bssl::CertDecompressFunc decompress;
+  uint16_t alg_id;
+};
+
+DEFINE_STACK_OF(CertCompressionAlg);
+
 namespace bssl {
 
 // SSLContext backs the public |SSL_CTX| type. Due to compatibility constraints,
@@ -2192,6 +2208,9 @@
   // SRTP profiles we are willing to do from RFC 5764
   STACK_OF(SRTP_PROTECTION_PROFILE) *srtp_profiles;
 
+  // Defined compression algorithms for certificates.
+  STACK_OF(CertCompressionAlg) *cert_compression_algs;
+
   // Supported group values inherited by SSL structure
   size_t supported_group_list_len;
   uint16_t *supported_group_list;
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 606d1fc..c6799b6 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -510,6 +510,51 @@
   ssl->config->handoff = on;
 }
 
+int SSL_CTX_add_cert_compression_alg(SSL_CTX *ctx, uint16_t alg_id,
+                                     bssl::CertCompressFunc compress,
+                                     bssl::CertDecompressFunc decompress) {
+  assert(compress != nullptr || decompress != nullptr);
+
+  for (CertCompressionAlg *alg : ctx->cert_compression_algs) {
+    if (alg->alg_id == alg_id) {
+      return 0;
+    }
+  }
+
+  CertCompressionAlg *alg = reinterpret_cast<CertCompressionAlg *>(
+      OPENSSL_malloc(sizeof(CertCompressionAlg)));
+  if (alg == nullptr) {
+    goto err;
+  }
+
+  OPENSSL_memset(alg, 0, sizeof(CertCompressionAlg));
+  alg->alg_id = alg_id;
+  alg->compress = compress;
+  alg->decompress = decompress;
+
+  if (ctx->cert_compression_algs == nullptr) {
+    ctx->cert_compression_algs = sk_CertCompressionAlg_new_null();
+    if (ctx->cert_compression_algs == nullptr) {
+      goto err;
+    }
+  }
+
+  if (!sk_CertCompressionAlg_push(ctx->cert_compression_algs, alg)) {
+    goto err;
+  }
+
+  return 1;
+
+err:
+  OPENSSL_free(alg);
+  if (ctx->cert_compression_algs != nullptr &&
+      sk_CertCompressionAlg_num(ctx->cert_compression_algs) == 0) {
+    sk_CertCompressionAlg_free(ctx->cert_compression_algs);
+    ctx->cert_compression_algs = nullptr;
+  }
+  return 0;
+}
+
 }  // namespace bssl
 
 using namespace bssl;
@@ -672,6 +717,8 @@
   sk_CRYPTO_BUFFER_pop_free(ctx->client_CA, CRYPTO_BUFFER_free);
   ctx->x509_method->ssl_ctx_free(ctx);
   sk_SRTP_PROTECTION_PROFILE_free(ctx->srtp_profiles);
+  sk_CertCompressionAlg_pop_free(ctx->cert_compression_algs,
+                                 Delete<CertCompressionAlg>);
   OPENSSL_free(ctx->psk_identity_hint);
   OPENSSL_free(ctx->supported_group_list);
   OPENSSL_free(ctx->alpn_client_proto_list);
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index 40b7ff0..4687f52 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -2761,6 +2761,93 @@
   return true;
 }
 
+// Certificate compression
+
+static bool cert_compression_add_clienthello(SSL_HANDSHAKE *hs, CBB *out) {
+  return true;
+}
+
+static bool cert_compression_parse_serverhello(SSL_HANDSHAKE *hs,
+                                               uint8_t *out_alert,
+                                               CBS *contents) {
+  if (contents == nullptr) {
+    return true;
+  }
+
+  // The server may not echo this extension. Any server to client negotiation is
+  // advertised in the CertificateRequest message.
+  return false;
+}
+
+static bool cert_compression_parse_clienthello(SSL_HANDSHAKE *hs,
+                                               uint8_t *out_alert,
+                                               CBS *contents) {
+  if (contents == nullptr) {
+    return true;
+  }
+
+  const size_t num_algs =
+      sk_CertCompressionAlg_num(hs->ssl->ctx->cert_compression_algs);
+
+  CBS alg_ids;
+  if (!CBS_get_u8_length_prefixed(contents, &alg_ids) ||
+      CBS_len(contents) != 0 ||
+      CBS_len(&alg_ids) == 0 ||
+      CBS_len(&alg_ids) % 2 == 1) {
+    return false;
+  }
+
+  const size_t num_given_alg_ids = CBS_len(&alg_ids) / 2;
+  Array<uint16_t> given_alg_ids;
+  if (!given_alg_ids.Init(num_given_alg_ids)) {
+    return false;
+  }
+
+  size_t best_index = num_algs;
+  size_t given_alg_idx = 0;
+
+  while (CBS_len(&alg_ids) > 0) {
+    uint16_t alg_id;
+    if (!CBS_get_u16(&alg_ids, &alg_id)) {
+      return false;
+    }
+
+    given_alg_ids[given_alg_idx++] = alg_id;
+
+    for (size_t i = 0; i < num_algs; i++) {
+      const auto *alg =
+          sk_CertCompressionAlg_value(hs->ssl->ctx->cert_compression_algs, i);
+      if (alg->alg_id == alg_id && alg->compress != nullptr) {
+        if (i < best_index) {
+          best_index = i;
+        }
+        break;
+      }
+    }
+  }
+
+  qsort(given_alg_ids.data(), given_alg_ids.size(), sizeof(uint16_t),
+        compare_uint16_t);
+  for (size_t i = 1; i < num_given_alg_ids; i++) {
+    if (given_alg_ids[i - 1] == given_alg_ids[i]) {
+      return false;
+    }
+  }
+
+  if (best_index < num_algs &&
+      ssl_protocol_version(hs->ssl) >= TLS1_3_VERSION) {
+    hs->cert_compression_negotiated = true;
+    hs->cert_compression_alg_id =
+        sk_CertCompressionAlg_value(hs->ssl->ctx->cert_compression_algs,
+                                    best_index)->alg_id;
+  }
+
+  return true;
+}
+
+static bool cert_compression_add_serverhello(SSL_HANDSHAKE *hs, CBB *out) {
+  return true;
+}
 
 // kExtensions contains all the supported extensions.
 static const struct tls_extension kExtensions[] = {
@@ -2945,6 +3032,14 @@
     ext_token_binding_parse_clienthello,
     ext_token_binding_add_serverhello,
   },
+  {
+    TLSEXT_TYPE_cert_compression,
+    NULL,
+    cert_compression_add_clienthello,
+    cert_compression_parse_serverhello,
+    cert_compression_parse_clienthello,
+    cert_compression_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 87da27a..32d224c 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -1346,6 +1346,54 @@
                              ssl_ctx.get());
   }
 
+  if (config->install_cert_compression_algs &&
+      (!SSL_CTX_add_cert_compression_alg(
+           ssl_ctx.get(), 0xff02,
+           [](SSL *ssl, CBB *out, bssl::Span<const uint8_t> in) -> bool {
+             if (!CBB_add_u8(out, 1) ||
+                 !CBB_add_u8(out, 2) ||
+                 !CBB_add_u8(out, 3) ||
+                 !CBB_add_u8(out, 4) ||
+                 !CBB_add_bytes(out, in.data(), in.size())) {
+               return false;
+             }
+             return true;
+           },
+           [](SSL *ssl, bssl::UniquePtr<CRYPTO_BUFFER> *out,
+              size_t uncompressed_len, bssl::Span<const uint8_t> in) -> bool {
+             if (in.size() < 4 || in[0] != 1 || in[1] != 2 || in[2] != 3 ||
+                 in[3] != 4 || uncompressed_len != in.size() - 4) {
+               return false;
+             }
+             const bssl::Span<const uint8_t> uncompressed(in.subspan(4));
+             out->reset(CRYPTO_BUFFER_new(uncompressed.data(),
+                                          uncompressed.size(), nullptr));
+             return true;
+           }) ||
+       !SSL_CTX_add_cert_compression_alg(
+           ssl_ctx.get(), 0xff01,
+           [](SSL *ssl, CBB *out, bssl::Span<const uint8_t> in) -> bool {
+             if (in.size() < 2 || in[0] != 0 || in[1] != 0) {
+               return false;
+             }
+             return CBB_add_bytes(out, in.data() + 2, in.size() - 2);
+           },
+           [](SSL *ssl, bssl::UniquePtr<CRYPTO_BUFFER> *out,
+              size_t uncompressed_len, bssl::Span<const uint8_t> in) -> bool {
+             if (uncompressed_len != 2 + in.size()) {
+               return false;
+             }
+             std::unique_ptr<uint8_t[]> buf(new uint8_t[2 + in.size()]);
+             buf[0] = 0;
+             buf[1] = 0;
+             OPENSSL_memcpy(&buf[2], in.data(), in.size());
+             out->reset(CRYPTO_BUFFER_new(buf.get(), 2 + in.size(), nullptr));
+             return true;
+           }))) {
+    fprintf(stderr, "SSL_CTX_add_cert_compression_alg failed.\n");
+    abort();
+  }
+
   return ssl_ctx;
 }
 
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 5d3345d..66b4295 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -82,26 +82,27 @@
 
 // TLS handshake message types.
 const (
-	typeHelloRequest        uint8 = 0
-	typeClientHello         uint8 = 1
-	typeServerHello         uint8 = 2
-	typeHelloVerifyRequest  uint8 = 3
-	typeNewSessionTicket    uint8 = 4
-	typeEndOfEarlyData      uint8 = 5 // draft-ietf-tls-tls13-21
-	typeHelloRetryRequest   uint8 = 6 // draft-ietf-tls-tls13-16
-	typeEncryptedExtensions uint8 = 8 // draft-ietf-tls-tls13-16
-	typeCertificate         uint8 = 11
-	typeServerKeyExchange   uint8 = 12
-	typeCertificateRequest  uint8 = 13
-	typeServerHelloDone     uint8 = 14
-	typeCertificateVerify   uint8 = 15
-	typeClientKeyExchange   uint8 = 16
-	typeFinished            uint8 = 20
-	typeCertificateStatus   uint8 = 22
-	typeKeyUpdate           uint8 = 24  // draft-ietf-tls-tls13-16
-	typeNextProtocol        uint8 = 67  // Not IANA assigned
-	typeChannelID           uint8 = 203 // Not IANA assigned
-	typeMessageHash         uint8 = 254 // draft-ietf-tls-tls13-21
+	typeHelloRequest          uint8 = 0
+	typeClientHello           uint8 = 1
+	typeServerHello           uint8 = 2
+	typeHelloVerifyRequest    uint8 = 3
+	typeNewSessionTicket      uint8 = 4
+	typeEndOfEarlyData        uint8 = 5 // draft-ietf-tls-tls13-21
+	typeHelloRetryRequest     uint8 = 6 // draft-ietf-tls-tls13-16
+	typeEncryptedExtensions   uint8 = 8 // draft-ietf-tls-tls13-16
+	typeCertificate           uint8 = 11
+	typeServerKeyExchange     uint8 = 12
+	typeCertificateRequest    uint8 = 13
+	typeServerHelloDone       uint8 = 14
+	typeCertificateVerify     uint8 = 15
+	typeClientKeyExchange     uint8 = 16
+	typeFinished              uint8 = 20
+	typeCertificateStatus     uint8 = 22
+	typeKeyUpdate             uint8 = 24  // draft-ietf-tls-tls13-16
+	typeCompressedCertificate uint8 = 25  // Not IANA assigned
+	typeNextProtocol          uint8 = 67  // Not IANA assigned
+	typeChannelID             uint8 = 203 // Not IANA assigned
+	typeMessageHash           uint8 = 254 // draft-ietf-tls-tls13-21
 )
 
 // TLS compression types.
@@ -122,7 +123,8 @@
 	extensionPadding                    uint16 = 21
 	extensionExtendedMasterSecret       uint16 = 23
 	extensionTokenBinding               uint16 = 24
-	extensionQUICTransportParams        uint16 = 26
+	extensionQUICTransportParams        uint16 = 26 // conflicts with TLS-LTS
+	extensionCompressedCertAlgs         uint16 = 27
 	extensionSessionTicket              uint16 = 35
 	extensionPreSharedKey               uint16 = 41    // draft-ietf-tls-tls13-23
 	extensionEarlyData                  uint16 = 42    // draft-ietf-tls-tls13-23
@@ -330,6 +332,16 @@
 	Put(sessionId string, session *sessionState)
 }
 
+// CertCompressionAlg is a certificate compression algorithm, specified as a
+// pair of functions for compressing and decompressing certificates.
+type CertCompressionAlg struct {
+	// Compress returns a compressed representation of the input.
+	Compress func([]byte) []byte
+	// Decompress depresses the contents of in and writes the result to out, which
+	// will be the correct size. It returns true on success and false otherwise.
+	Decompress func(out, in []byte) bool
+}
+
 // A Config structure is used to configure a TLS client or server.
 // After one has been passed to a TLS function it must not be
 // modified. A Config may be reused; the tls package will also not
@@ -500,6 +512,8 @@
 	// transport parameters extension.
 	QUICTransportParams []byte
 
+	CertCompressionAlgs map[uint16]CertCompressionAlg
+
 	// Bugs specifies optional misbehaviour to be used for testing other
 	// implementations.
 	Bugs ProtocolBugs
@@ -1596,6 +1610,14 @@
 	// SetX25519HighBit, if true, causes X25519 key shares to set their
 	// high-order bit.
 	SetX25519HighBit bool
+
+	// DuplicateCompressedCertAlgs, if true, causes two, equal, certificate
+	// compression algorithm IDs to be sent.
+	DuplicateCompressedCertAlgs bool
+
+	// ExpectedCompressedCert specifies the compression algorithm ID that must be
+	// used on this connection, or zero if there are no special requirements.
+	ExpectedCompressedCert uint16
 }
 
 func (c *Config) serverInit() {
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 55e42a5..b6b6ffa 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -1368,9 +1368,11 @@
 		m = &certificateMsg{
 			hasRequestContext: c.vers >= VersionTLS13,
 		}
+	case typeCompressedCertificate:
+		m = new(compressedCertificateMsg)
 	case typeCertificateRequest:
 		m = &certificateRequestMsg{
-			vers: c.wireVersion,
+			vers:                  c.wireVersion,
 			hasSignatureAlgorithm: c.vers >= VersionTLS12,
 			hasRequestContext:     c.vers >= VersionTLS13,
 		}
@@ -1806,7 +1808,7 @@
 	if c.isDTLS && c.config.Bugs.SendSplitAlert {
 		c.conn.Write([]byte{
 			byte(recordTypeAlert), // type
-			0xfe, 0xff, // version
+			0xfe, 0xff,            // version
 			0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // sequence
 			0x0, 0x2, // length
 		})
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index 6f6f440..f367dc4 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -155,6 +155,15 @@
 		}
 	}
 
+	if c.config.Bugs.DuplicateCompressedCertAlgs {
+		hello.compressedCertAlgs = []uint16{1, 1}
+	} else if len(c.config.CertCompressionAlgs) > 0 {
+		hello.compressedCertAlgs = make([]uint16, 0, len(c.config.CertCompressionAlgs))
+		for id, _ := range c.config.CertCompressionAlgs {
+			hello.compressedCertAlgs = append(hello.compressedCertAlgs, uint16(id))
+		}
+	}
+
 	if c.noRenegotiationInfo() {
 		hello.secureRenegotiation = nil
 	}
@@ -864,12 +873,46 @@
 			}
 		}
 
-		certMsg, ok := msg.(*certificateMsg)
-		if !ok {
-			c.sendAlert(alertUnexpectedMessage)
-			return unexpectedMessageError(certMsg, msg)
+		var certMsg *certificateMsg
+
+		if compressedCertMsg, ok := msg.(*compressedCertificateMsg); ok {
+			hs.writeServerHash(compressedCertMsg.marshal())
+
+			alg, ok := c.config.CertCompressionAlgs[compressedCertMsg.algID]
+			if !ok {
+				c.sendAlert(alertBadCertificate)
+				return fmt.Errorf("tls: received certificate compressed with unknown algorithm %x", compressedCertMsg.algID)
+			}
+
+			decompressed := make([]byte, 4+int(compressedCertMsg.uncompressedLength))
+			if !alg.Decompress(decompressed[4:], compressedCertMsg.compressed) {
+				c.sendAlert(alertBadCertificate)
+				return fmt.Errorf("tls: failed to decompress certificate with algorithm %x", compressedCertMsg.algID)
+			}
+
+			certMsg = &certificateMsg{
+				hasRequestContext: true,
+			}
+
+			if !certMsg.unmarshal(decompressed) {
+				c.sendAlert(alertBadCertificate)
+				return errors.New("tls: failed to parse decompressed certificate")
+			}
+
+			if expected := c.config.Bugs.ExpectedCompressedCert; expected != 0 && expected != compressedCertMsg.algID {
+				return fmt.Errorf("tls: expected certificate compressed with algorithm %x, but message used %x", expected, compressedCertMsg.algID)
+			}
+		} else {
+			if certMsg, ok = msg.(*certificateMsg); !ok {
+				c.sendAlert(alertUnexpectedMessage)
+				return unexpectedMessageError(certMsg, msg)
+			}
+			hs.writeServerHash(certMsg.marshal())
+
+			if c.config.Bugs.ExpectedCompressedCert != 0 {
+				return errors.New("tls: uncompressed certificate received")
+			}
 		}
-		hs.writeServerHash(certMsg.marshal())
 
 		// Check for unsolicited extensions.
 		for i, cert := range certMsg.certificates {
diff --git a/ssl/test/runner/handshake_messages.go b/ssl/test/runner/handshake_messages.go
index 745c8f3..c92d979 100644
--- a/ssl/test/runner/handshake_messages.go
+++ b/ssl/test/runner/handshake_messages.go
@@ -296,6 +296,7 @@
 	emptyExtensions         bool
 	pad                     int
 	dummyPQPaddingLen       int
+	compressedCertAlgs      []uint16
 }
 
 func (m *clientHelloMsg) equal(i interface{}) bool {
@@ -349,7 +350,8 @@
 		m.omitExtensions == m1.omitExtensions &&
 		m.emptyExtensions == m1.emptyExtensions &&
 		m.pad == m1.pad &&
-		m.dummyPQPaddingLen == m1.dummyPQPaddingLen
+		m.dummyPQPaddingLen == m1.dummyPQPaddingLen &&
+		eqUint16s(m.compressedCertAlgs, m1.compressedCertAlgs)
 }
 
 func (m *clientHelloMsg) marshal() []byte {
@@ -586,6 +588,14 @@
 		body := extensions.addU16LengthPrefixed()
 		body.addBytes(make([]byte, l))
 	}
+	if len(m.compressedCertAlgs) > 0 {
+		extensions.addU16(extensionCompressedCertAlgs)
+		body := extensions.addU16LengthPrefixed()
+		algIDs := body.addU8LengthPrefixed()
+		for _, v := range m.compressedCertAlgs {
+			algIDs.addU16(v)
+		}
+	}
 	// The PSK extension must be last (draft-ietf-tls-tls13-18 section 4.2.6).
 	if len(m.pskIdentities) > 0 && !m.pskBinderFirst {
 		extensions.addU16(extensionPreSharedKey)
@@ -903,6 +913,24 @@
 				return false
 			}
 			m.dummyPQPaddingLen = len(body)
+		case extensionCompressedCertAlgs:
+			var algIDs byteReader
+			if !body.readU8LengthPrefixed(&algIDs) {
+				return false
+			}
+
+			seen := make(map[uint16]struct{})
+			for len(algIDs) > 0 {
+				var algID uint16
+				if !algIDs.readU16(&algID) {
+					return false
+				}
+				if _, ok := seen[algID]; ok {
+					return false
+				}
+				seen[algID] = struct{}{}
+				m.compressedCertAlgs = append(m.compressedCertAlgs, algID)
+			}
 		}
 
 		if isGREASEValue(extension) {
@@ -1671,6 +1699,48 @@
 	return true
 }
 
+type compressedCertificateMsg struct {
+	raw                []byte
+	algID              uint16
+	uncompressedLength uint32
+	compressed         []byte
+}
+
+func (m *compressedCertificateMsg) marshal() (x []byte) {
+	if m.raw != nil {
+		return m.raw
+	}
+
+	certMsg := newByteBuilder()
+	certMsg.addU8(typeCertificate)
+	certificate := certMsg.addU24LengthPrefixed()
+	certificate.addU16(m.algID)
+	certificate.addU24(int(m.uncompressedLength))
+	compressed := certificate.addU24LengthPrefixed()
+	compressed.addBytes(m.compressed)
+
+	m.raw = certMsg.finish()
+	return m.raw
+}
+
+func (m *compressedCertificateMsg) unmarshal(data []byte) bool {
+	m.raw = data
+	reader := byteReader(data[4:])
+
+	if !reader.readU16(&m.algID) ||
+		!reader.readU24(&m.uncompressedLength) ||
+		!reader.readU24LengthPrefixedBytes(&m.compressed) ||
+		len(reader) != 0 {
+		return false
+	}
+
+	if m.uncompressedLength >= 1<<17 {
+		return false
+	}
+
+	return true
+}
+
 type serverKeyExchangeMsg struct {
 	raw []byte
 	key []byte
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index da6fddc..52c2002 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -834,7 +834,7 @@
 		if config.ClientAuth >= RequestClientCert {
 			// Request a client certificate
 			certReq := &certificateRequestMsg{
-				vers: c.wireVersion,
+				vers:                  c.wireVersion,
 				hasSignatureAlgorithm: !config.Bugs.OmitCertificateRequestAlgorithms,
 				hasRequestContext:     true,
 				requestContext:        config.Bugs.SendRequestContext,
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 15122a1..e217ed8 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -14396,6 +14396,158 @@
 	}
 }
 
+func addCertCompressionTests() {
+	// shrinkingPrefix is the first two bytes of a Certificate message.
+	shrinkingPrefix := []byte{0, 0}
+	// expandingPrefix is just some arbitrary byte string. This has to match the
+	// value in the shim.
+	expandingPrefix := []byte{1, 2, 3, 4}
+
+	shinking := CertCompressionAlg{
+		Compress: func(uncompressed []byte) []byte {
+			if !bytes.HasPrefix(uncompressed, shrinkingPrefix) {
+				panic(fmt.Sprintf("cannot compress certificate message %x", uncompressed))
+			}
+			return uncompressed[len(shrinkingPrefix):]
+		},
+		Decompress: func(out []byte, compressed []byte) bool {
+			if len(out) != len(shrinkingPrefix)+len(compressed) {
+				return false
+			}
+
+			copy(out, shrinkingPrefix)
+			copy(out[len(shrinkingPrefix):], compressed)
+			return true
+		},
+	}
+
+	expanding := CertCompressionAlg{
+		Compress: func(uncompressed []byte) []byte {
+			ret := make([]byte, 0, len(expandingPrefix)+len(uncompressed))
+			ret = append(ret, expandingPrefix...)
+			return append(ret, uncompressed...)
+		},
+		Decompress: func(out []byte, compressed []byte) bool {
+			if !bytes.HasPrefix(compressed, expandingPrefix) {
+				return false
+			}
+			copy(out, compressed[len(expandingPrefix):])
+			return true
+		},
+	}
+
+	const (
+		shrinkingAlgId = 0xff01
+		expandingAlgId = 0xff02
+	)
+
+	for _, ver := range tlsVersions {
+		if ver.version < VersionTLS12 {
+			continue
+		}
+
+		// Duplicate compression algorithms is an error, even if nothing is
+		// configured.
+		testCases = append(testCases, testCase{
+			testType:     serverTest,
+			name:         "DuplicateCertCompressionExt-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				Bugs: ProtocolBugs{
+					DuplicateCompressedCertAlgs: true,
+				},
+			},
+			shouldFail:    true,
+			expectedError: ":ERROR_PARSING_EXTENSION:",
+		})
+
+		// With compression algorithms configured, an duplicate values should still
+		// be an error.
+		testCases = append(testCases, testCase{
+			testType:     serverTest,
+			name:         "DuplicateCertCompressionExt2-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			flags:        []string{"-install-cert-compression-algs"},
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				Bugs: ProtocolBugs{
+					DuplicateCompressedCertAlgs: true,
+				},
+			},
+			shouldFail:    true,
+			expectedError: ":ERROR_PARSING_EXTENSION:",
+		})
+
+		if ver.version < VersionTLS13 {
+			testCases = append(testCases, testCase{
+				testType: serverTest,
+				name:     "CertCompressionIgnoredBefore13-" + ver.name,
+				flags:    []string{"-install-cert-compression-algs"},
+				config: Config{
+					MinVersion:          ver.version,
+					MaxVersion:          ver.version,
+					CertCompressionAlgs: map[uint16]CertCompressionAlg{expandingAlgId: expanding},
+				},
+			})
+
+			continue
+		}
+
+		testCases = append(testCases, testCase{
+			testType:     serverTest,
+			name:         "CertCompressionExpands-" + 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:     serverTest,
+			name:         "CertCompressionShrinks-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			flags:        []string{"-install-cert-compression-algs"},
+			config: Config{
+				MinVersion:          ver.version,
+				MaxVersion:          ver.version,
+				CertCompressionAlgs: map[uint16]CertCompressionAlg{shrinkingAlgId: shinking},
+				Bugs: ProtocolBugs{
+					ExpectedCompressedCert: shrinkingAlgId,
+				},
+			},
+		})
+
+		// With both algorithms configured, the server should pick its most
+		// preferable. (Which is expandingAlgId.)
+		testCases = append(testCases, testCase{
+			testType:     serverTest,
+			name:         "CertCompressionPriority-" + ver.name,
+			tls13Variant: ver.tls13Variant,
+			flags:        []string{"-install-cert-compression-algs"},
+			config: Config{
+				MinVersion: ver.version,
+				MaxVersion: ver.version,
+				CertCompressionAlgs: map[uint16]CertCompressionAlg{
+					shrinkingAlgId: shinking,
+					expandingAlgId: expanding,
+				},
+				Bugs: ProtocolBugs{
+					ExpectedCompressedCert: expandingAlgId,
+				},
+			},
+		})
+	}
+}
+
 func worker(statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {
 	defer wg.Done()
 
@@ -14524,6 +14676,7 @@
 	addECDSAKeyUsageTests()
 	addExtraHandshakeTests()
 	addOmitExtensionsTests()
+	addCertCompressionTests()
 
 	testCases = append(testCases, convertToSplitHandshakeTests(testCases)...)
 
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index fdcb0a9..fa38688 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -141,6 +141,8 @@
   { "-set-ocsp-in-callback", &TestConfig::set_ocsp_in_callback },
   { "-decline-ocsp-callback", &TestConfig::decline_ocsp_callback },
   { "-fail-ocsp-callback", &TestConfig::fail_ocsp_callback },
+  { "-install-cert-compression-algs",
+    &TestConfig::install_cert_compression_algs },
 };
 
 const Flag<std::string> kStringFlags[] = {
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 6488a26..c98bf93 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -161,6 +161,7 @@
   bool set_ocsp_in_callback = false;
   bool decline_ocsp_callback = false;
   bool fail_ocsp_callback = false;
+  bool install_cert_compression_algs = false;
 };
 
 bool ParseConfig(int argc, char **argv, TestConfig *out_initial,
diff --git a/ssl/tls13_both.cc b/ssl/tls13_both.cc
index 4cd3209..993dd02 100644
--- a/ssl/tls13_both.cc
+++ b/ssl/tls13_both.cc
@@ -353,12 +353,26 @@
 
 int tls13_add_certificate(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
+  CERT *const cert = hs->config->cert;
+
   ScopedCBB cbb;
-  CBB body, certificate_list;
-  if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_CERTIFICATE) ||
-      // The request context is always empty in the handshake.
-      !CBB_add_u8(&body, 0) ||
-      !CBB_add_u24_length_prefixed(&body, &certificate_list)) {
+  CBB *body, body_storage, certificate_list;
+
+  if (hs->cert_compression_negotiated) {
+    if (!CBB_init(cbb.get(), 1024)) {
+      return false;
+    }
+    body = cbb.get();
+  } else {
+    body = &body_storage;
+    if (!ssl->method->init_message(ssl, cbb.get(), body, SSL3_MT_CERTIFICATE)) {
+      return false;
+    }
+  }
+
+  if (// The request context is always empty in the handshake.
+      !CBB_add_u8(body, 0) ||
+      !CBB_add_u24_length_prefixed(body, &certificate_list)) {
     OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
     return 0;
   }
@@ -367,7 +381,6 @@
     return ssl_add_message_cbb(ssl, cbb.get());
   }
 
-  CERT *cert = hs->config->cert;
   CRYPTO_BUFFER *leaf_buf = sk_CRYPTO_BUFFER_value(cert->chain.get(), 0);
   CBB leaf, extensions;
   if (!CBB_add_u24_length_prefixed(&certificate_list, &leaf) ||
@@ -378,33 +391,29 @@
     return 0;
   }
 
-  if (hs->scts_requested &&
-      hs->config->cert->signed_cert_timestamp_list != nullptr) {
+  if (hs->scts_requested && cert->signed_cert_timestamp_list != nullptr) {
     CBB contents;
     if (!CBB_add_u16(&extensions, TLSEXT_TYPE_certificate_timestamp) ||
         !CBB_add_u16_length_prefixed(&extensions, &contents) ||
         !CBB_add_bytes(
             &contents,
-            CRYPTO_BUFFER_data(
-                hs->config->cert->signed_cert_timestamp_list.get()),
-            CRYPTO_BUFFER_len(
-                hs->config->cert->signed_cert_timestamp_list.get())) ||
+            CRYPTO_BUFFER_data(cert->signed_cert_timestamp_list.get()),
+            CRYPTO_BUFFER_len(cert->signed_cert_timestamp_list.get())) ||
         !CBB_flush(&extensions)) {
       OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
       return 0;
     }
   }
 
-  if (hs->ocsp_stapling_requested && hs->config->cert->ocsp_response != NULL) {
+  if (hs->ocsp_stapling_requested && cert->ocsp_response != NULL) {
     CBB contents, ocsp_response;
     if (!CBB_add_u16(&extensions, TLSEXT_TYPE_status_request) ||
         !CBB_add_u16_length_prefixed(&extensions, &contents) ||
         !CBB_add_u8(&contents, TLSEXT_STATUSTYPE_ocsp) ||
         !CBB_add_u24_length_prefixed(&contents, &ocsp_response) ||
-        !CBB_add_bytes(
-            &ocsp_response,
-            CRYPTO_BUFFER_data(hs->config->cert->ocsp_response.get()),
-            CRYPTO_BUFFER_len(hs->config->cert->ocsp_response.get())) ||
+        !CBB_add_bytes(&ocsp_response,
+                       CRYPTO_BUFFER_data(cert->ocsp_response.get()),
+                       CRYPTO_BUFFER_len(cert->ocsp_response.get())) ||
         !CBB_flush(&extensions)) {
       OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
       return 0;
@@ -423,7 +432,43 @@
     }
   }
 
-  return ssl_add_message_cbb(ssl, cbb.get());
+  if (!hs->cert_compression_negotiated) {
+    return ssl_add_message_cbb(ssl, cbb.get());
+  }
+
+  Array<uint8_t> msg;
+  if (!CBBFinishArray(cbb.get(), &msg)) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return 0;
+  }
+
+  const CertCompressionAlg *alg = nullptr;
+  for (CertCompressionAlg *candidate : ssl->ctx->cert_compression_algs) {
+    if (candidate->alg_id == hs->cert_compression_alg_id) {
+      alg = candidate;
+      break;
+    }
+  }
+
+  if (alg == nullptr || alg->compress == nullptr) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return 0;
+  }
+
+  CBB compressed;
+  body = &body_storage;
+  if (!ssl->method->init_message(ssl, cbb.get(), body,
+                                 SSL3_MT_COMPRESSED_CERTIFICATE) ||
+      !CBB_add_u16(body, hs->cert_compression_alg_id) ||
+      !CBB_add_u24(body, msg.size()) ||
+      !CBB_add_u24_length_prefixed(body, &compressed) ||
+      !alg->compress(ssl, &compressed, msg) ||
+      !ssl_add_message_cbb(ssl, cbb.get())) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
+    return 0;
+  }
+
+  return 1;
 }
 
 enum ssl_private_key_result_t tls13_add_certificate_verify(SSL_HANDSHAKE *hs) {