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