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) {