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;
}