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>
This commit is contained in:
Adam Langley 2018-05-02 09:06:48 -07:00 committed by Adam Langley
parent c1e4f338b1
commit a307cb7d58
17 changed files with 654 additions and 55 deletions

View File

@ -4565,6 +4565,50 @@ BORINGSSL_MAKE_DELETER(SSL, SSL_free)
BORINGSSL_MAKE_DELETER(SSL_CTX, SSL_CTX_free) BORINGSSL_MAKE_DELETER(SSL_CTX, SSL_CTX_free)
BORINGSSL_MAKE_DELETER(SSL_SESSION, SSL_SESSION_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 { enum class OpenRecordResult {
kOK, kOK,
kDiscard, kDiscard,

View File

@ -311,6 +311,7 @@ OPENSSL_COMPILE_ASSERT(
#define SSL3_MT_CERTIFICATE_STATUS 22 #define SSL3_MT_CERTIFICATE_STATUS 22
#define SSL3_MT_SUPPLEMENTAL_DATA 23 #define SSL3_MT_SUPPLEMENTAL_DATA 23
#define SSL3_MT_KEY_UPDATE 24 #define SSL3_MT_KEY_UPDATE 24
#define SSL3_MT_COMPRESSED_CERTIFICATE 25
#define SSL3_MT_NEXT_PROTO 67 #define SSL3_MT_NEXT_PROTO 67
#define SSL3_MT_CHANNEL_ID 203 #define SSL3_MT_CHANNEL_ID 203
#define SSL3_MT_MESSAGE_HASH 254 #define SSL3_MT_MESSAGE_HASH 254

View File

@ -205,9 +205,15 @@ extern "C" {
// ExtensionType value from draft-ietf-tokbind-negotiation-10 // ExtensionType value from draft-ietf-tokbind-negotiation-10
#define TLSEXT_TYPE_token_binding 24 #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 #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 // ExtensionType value from RFC4507
#define TLSEXT_TYPE_session_ticket 35 #define TLSEXT_TYPE_session_ticket 35

View File

@ -147,7 +147,8 @@ SSL_HANDSHAKE::SSL_HANDSHAKE(SSL *ssl_arg)
extended_master_secret(false), extended_master_secret(false),
pending_private_key_op(false), pending_private_key_op(false),
grease_seeded(false), grease_seeded(false),
handback(false) { handback(false),
cert_compression_negotiated(false) {
assert(ssl); assert(ssl);
} }

View File

@ -1481,6 +1481,11 @@ struct SSL_HANDSHAKE {
// sent. // sent.
uint16_t negotiated_token_binding_version; 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 // server_params, in a TLS 1.2 server, stores the ServerKeyExchange
// parameters. It has client and server randoms prepended for signing // parameters. It has client and server randoms prepended for signing
// convenience. // convenience.
@ -1595,6 +1600,14 @@ struct SSL_HANDSHAKE {
// grease_seeded is true if |grease_seed| has been initialized. // grease_seeded is true if |grease_seed| has been initialized.
bool grease_seeded:1; 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. // client_version is the value sent or received in the ClientHello version.
uint16_t client_version = 0; uint16_t client_version = 0;
@ -1619,11 +1632,6 @@ struct SSL_HANDSHAKE {
// should be echoed in a ServerHello, or zero if no extension should be // should be echoed in a ServerHello, or zero if no extension should be
// echoed. // echoed.
uint16_t dummy_pq_padding_len = 0; 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); UniquePtr<SSL_HANDSHAKE> ssl_handshake_new(SSL *ssl);
@ -1987,6 +1995,14 @@ struct tlsext_ticket_key {
DECLARE_LHASH_OF(SSL_SESSION) DECLARE_LHASH_OF(SSL_SESSION)
struct CertCompressionAlg {
bssl::CertCompressFunc compress;
bssl::CertDecompressFunc decompress;
uint16_t alg_id;
};
DEFINE_STACK_OF(CertCompressionAlg);
namespace bssl { namespace bssl {
// SSLContext backs the public |SSL_CTX| type. Due to compatibility constraints, // SSLContext backs the public |SSL_CTX| type. Due to compatibility constraints,
@ -2192,6 +2208,9 @@ struct SSLContext {
// SRTP profiles we are willing to do from RFC 5764 // SRTP profiles we are willing to do from RFC 5764
STACK_OF(SRTP_PROTECTION_PROFILE) *srtp_profiles; 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 // Supported group values inherited by SSL structure
size_t supported_group_list_len; size_t supported_group_list_len;
uint16_t *supported_group_list; uint16_t *supported_group_list;

View File

@ -510,6 +510,51 @@ void SSL_set_handoff_mode(SSL *ssl, bool on) {
ssl->config->handoff = on; 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 } // namespace bssl
using namespace bssl; using namespace bssl;
@ -672,6 +717,8 @@ void SSL_CTX_free(SSL_CTX *ctx) {
sk_CRYPTO_BUFFER_pop_free(ctx->client_CA, CRYPTO_BUFFER_free); sk_CRYPTO_BUFFER_pop_free(ctx->client_CA, CRYPTO_BUFFER_free);
ctx->x509_method->ssl_ctx_free(ctx); ctx->x509_method->ssl_ctx_free(ctx);
sk_SRTP_PROTECTION_PROFILE_free(ctx->srtp_profiles); 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->psk_identity_hint);
OPENSSL_free(ctx->supported_group_list); OPENSSL_free(ctx->supported_group_list);
OPENSSL_free(ctx->alpn_client_proto_list); OPENSSL_free(ctx->alpn_client_proto_list);

View File

@ -2761,6 +2761,93 @@ static bool ext_quic_transport_params_add_serverhello(SSL_HANDSHAKE *hs,
return true; 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. // kExtensions contains all the supported extensions.
static const struct tls_extension kExtensions[] = { static const struct tls_extension kExtensions[] = {
@ -2945,6 +3032,14 @@ static const struct tls_extension kExtensions[] = {
ext_token_binding_parse_clienthello, ext_token_binding_parse_clienthello,
ext_token_binding_add_serverhello, 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)) #define kNumExtensions (sizeof(kExtensions) / sizeof(struct tls_extension))

View File

@ -1346,6 +1346,54 @@ static bssl::UniquePtr<SSL_CTX> SetupCtx(SSL_CTX *old_ctx,
ssl_ctx.get()); 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; return ssl_ctx;
} }

View File

@ -82,26 +82,27 @@ const (
// TLS handshake message types. // TLS handshake message types.
const ( const (
typeHelloRequest uint8 = 0 typeHelloRequest uint8 = 0
typeClientHello uint8 = 1 typeClientHello uint8 = 1
typeServerHello uint8 = 2 typeServerHello uint8 = 2
typeHelloVerifyRequest uint8 = 3 typeHelloVerifyRequest uint8 = 3
typeNewSessionTicket uint8 = 4 typeNewSessionTicket uint8 = 4
typeEndOfEarlyData uint8 = 5 // draft-ietf-tls-tls13-21 typeEndOfEarlyData uint8 = 5 // draft-ietf-tls-tls13-21
typeHelloRetryRequest uint8 = 6 // draft-ietf-tls-tls13-16 typeHelloRetryRequest uint8 = 6 // draft-ietf-tls-tls13-16
typeEncryptedExtensions uint8 = 8 // draft-ietf-tls-tls13-16 typeEncryptedExtensions uint8 = 8 // draft-ietf-tls-tls13-16
typeCertificate uint8 = 11 typeCertificate uint8 = 11
typeServerKeyExchange uint8 = 12 typeServerKeyExchange uint8 = 12
typeCertificateRequest uint8 = 13 typeCertificateRequest uint8 = 13
typeServerHelloDone uint8 = 14 typeServerHelloDone uint8 = 14
typeCertificateVerify uint8 = 15 typeCertificateVerify uint8 = 15
typeClientKeyExchange uint8 = 16 typeClientKeyExchange uint8 = 16
typeFinished uint8 = 20 typeFinished uint8 = 20
typeCertificateStatus uint8 = 22 typeCertificateStatus uint8 = 22
typeKeyUpdate uint8 = 24 // draft-ietf-tls-tls13-16 typeKeyUpdate uint8 = 24 // draft-ietf-tls-tls13-16
typeNextProtocol uint8 = 67 // Not IANA assigned typeCompressedCertificate uint8 = 25 // Not IANA assigned
typeChannelID uint8 = 203 // Not IANA assigned typeNextProtocol uint8 = 67 // Not IANA assigned
typeMessageHash uint8 = 254 // draft-ietf-tls-tls13-21 typeChannelID uint8 = 203 // Not IANA assigned
typeMessageHash uint8 = 254 // draft-ietf-tls-tls13-21
) )
// TLS compression types. // TLS compression types.
@ -122,7 +123,8 @@ const (
extensionPadding uint16 = 21 extensionPadding uint16 = 21
extensionExtendedMasterSecret uint16 = 23 extensionExtendedMasterSecret uint16 = 23
extensionTokenBinding uint16 = 24 extensionTokenBinding uint16 = 24
extensionQUICTransportParams uint16 = 26 extensionQUICTransportParams uint16 = 26 // conflicts with TLS-LTS
extensionCompressedCertAlgs uint16 = 27
extensionSessionTicket uint16 = 35 extensionSessionTicket uint16 = 35
extensionPreSharedKey uint16 = 41 // draft-ietf-tls-tls13-23 extensionPreSharedKey uint16 = 41 // draft-ietf-tls-tls13-23
extensionEarlyData uint16 = 42 // draft-ietf-tls-tls13-23 extensionEarlyData uint16 = 42 // draft-ietf-tls-tls13-23
@ -330,6 +332,16 @@ type ServerSessionCache interface {
Put(sessionId string, session *sessionState) 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. // 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 // 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 // modified. A Config may be reused; the tls package will also not
@ -500,6 +512,8 @@ type Config struct {
// transport parameters extension. // transport parameters extension.
QUICTransportParams []byte QUICTransportParams []byte
CertCompressionAlgs map[uint16]CertCompressionAlg
// Bugs specifies optional misbehaviour to be used for testing other // Bugs specifies optional misbehaviour to be used for testing other
// implementations. // implementations.
Bugs ProtocolBugs Bugs ProtocolBugs
@ -1596,6 +1610,14 @@ type ProtocolBugs struct {
// SetX25519HighBit, if true, causes X25519 key shares to set their // SetX25519HighBit, if true, causes X25519 key shares to set their
// high-order bit. // high-order bit.
SetX25519HighBit bool 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() { func (c *Config) serverInit() {

View File

@ -1368,9 +1368,11 @@ func (c *Conn) readHandshake() (interface{}, error) {
m = &certificateMsg{ m = &certificateMsg{
hasRequestContext: c.vers >= VersionTLS13, hasRequestContext: c.vers >= VersionTLS13,
} }
case typeCompressedCertificate:
m = new(compressedCertificateMsg)
case typeCertificateRequest: case typeCertificateRequest:
m = &certificateRequestMsg{ m = &certificateRequestMsg{
vers: c.wireVersion, vers: c.wireVersion,
hasSignatureAlgorithm: c.vers >= VersionTLS12, hasSignatureAlgorithm: c.vers >= VersionTLS12,
hasRequestContext: c.vers >= VersionTLS13, hasRequestContext: c.vers >= VersionTLS13,
} }
@ -1806,7 +1808,7 @@ func (c *Conn) Handshake() error {
if c.isDTLS && c.config.Bugs.SendSplitAlert { if c.isDTLS && c.config.Bugs.SendSplitAlert {
c.conn.Write([]byte{ c.conn.Write([]byte{
byte(recordTypeAlert), // type byte(recordTypeAlert), // type
0xfe, 0xff, // version 0xfe, 0xff, // version
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // sequence 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, // sequence
0x0, 0x2, // length 0x0, 0x2, // length
}) })

View File

@ -155,6 +155,15 @@ func (c *Conn) clientHandshake() error {
} }
} }
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() { if c.noRenegotiationInfo() {
hello.secureRenegotiation = nil hello.secureRenegotiation = nil
} }
@ -864,12 +873,46 @@ func (hs *clientHandshakeState) doTLS13Handshake() error {
} }
} }
certMsg, ok := msg.(*certificateMsg) var certMsg *certificateMsg
if !ok {
c.sendAlert(alertUnexpectedMessage) if compressedCertMsg, ok := msg.(*compressedCertificateMsg); ok {
return unexpectedMessageError(certMsg, msg) 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. // Check for unsolicited extensions.
for i, cert := range certMsg.certificates { for i, cert := range certMsg.certificates {

View File

@ -296,6 +296,7 @@ type clientHelloMsg struct {
emptyExtensions bool emptyExtensions bool
pad int pad int
dummyPQPaddingLen int dummyPQPaddingLen int
compressedCertAlgs []uint16
} }
func (m *clientHelloMsg) equal(i interface{}) bool { func (m *clientHelloMsg) equal(i interface{}) bool {
@ -349,7 +350,8 @@ func (m *clientHelloMsg) equal(i interface{}) bool {
m.omitExtensions == m1.omitExtensions && m.omitExtensions == m1.omitExtensions &&
m.emptyExtensions == m1.emptyExtensions && m.emptyExtensions == m1.emptyExtensions &&
m.pad == m1.pad && m.pad == m1.pad &&
m.dummyPQPaddingLen == m1.dummyPQPaddingLen m.dummyPQPaddingLen == m1.dummyPQPaddingLen &&
eqUint16s(m.compressedCertAlgs, m1.compressedCertAlgs)
} }
func (m *clientHelloMsg) marshal() []byte { func (m *clientHelloMsg) marshal() []byte {
@ -586,6 +588,14 @@ func (m *clientHelloMsg) marshal() []byte {
body := extensions.addU16LengthPrefixed() body := extensions.addU16LengthPrefixed()
body.addBytes(make([]byte, l)) 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). // The PSK extension must be last (draft-ietf-tls-tls13-18 section 4.2.6).
if len(m.pskIdentities) > 0 && !m.pskBinderFirst { if len(m.pskIdentities) > 0 && !m.pskBinderFirst {
extensions.addU16(extensionPreSharedKey) extensions.addU16(extensionPreSharedKey)
@ -903,6 +913,24 @@ func (m *clientHelloMsg) unmarshal(data []byte) bool {
return false return false
} }
m.dummyPQPaddingLen = len(body) 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) { if isGREASEValue(extension) {
@ -1671,6 +1699,48 @@ func (m *certificateMsg) unmarshal(data []byte) bool {
return true 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 { type serverKeyExchangeMsg struct {
raw []byte raw []byte
key []byte key []byte

View File

@ -834,7 +834,7 @@ ResendHelloRetryRequest:
if config.ClientAuth >= RequestClientCert { if config.ClientAuth >= RequestClientCert {
// Request a client certificate // Request a client certificate
certReq := &certificateRequestMsg{ certReq := &certificateRequestMsg{
vers: c.wireVersion, vers: c.wireVersion,
hasSignatureAlgorithm: !config.Bugs.OmitCertificateRequestAlgorithms, hasSignatureAlgorithm: !config.Bugs.OmitCertificateRequestAlgorithms,
hasRequestContext: true, hasRequestContext: true,
requestContext: config.Bugs.SendRequestContext, requestContext: config.Bugs.SendRequestContext,

View File

@ -14396,6 +14396,158 @@ func addOmitExtensionsTests() {
} }
} }
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) { func worker(statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {
defer wg.Done() defer wg.Done()
@ -14524,6 +14676,7 @@ func main() {
addECDSAKeyUsageTests() addECDSAKeyUsageTests()
addExtraHandshakeTests() addExtraHandshakeTests()
addOmitExtensionsTests() addOmitExtensionsTests()
addCertCompressionTests()
testCases = append(testCases, convertToSplitHandshakeTests(testCases)...) testCases = append(testCases, convertToSplitHandshakeTests(testCases)...)

View File

@ -141,6 +141,8 @@ const Flag<bool> kBoolFlags[] = {
{ "-set-ocsp-in-callback", &TestConfig::set_ocsp_in_callback }, { "-set-ocsp-in-callback", &TestConfig::set_ocsp_in_callback },
{ "-decline-ocsp-callback", &TestConfig::decline_ocsp_callback }, { "-decline-ocsp-callback", &TestConfig::decline_ocsp_callback },
{ "-fail-ocsp-callback", &TestConfig::fail_ocsp_callback }, { "-fail-ocsp-callback", &TestConfig::fail_ocsp_callback },
{ "-install-cert-compression-algs",
&TestConfig::install_cert_compression_algs },
}; };
const Flag<std::string> kStringFlags[] = { const Flag<std::string> kStringFlags[] = {

View File

@ -161,6 +161,7 @@ struct TestConfig {
bool set_ocsp_in_callback = false; bool set_ocsp_in_callback = false;
bool decline_ocsp_callback = false; bool decline_ocsp_callback = false;
bool fail_ocsp_callback = false; bool fail_ocsp_callback = false;
bool install_cert_compression_algs = false;
}; };
bool ParseConfig(int argc, char **argv, TestConfig *out_initial, bool ParseConfig(int argc, char **argv, TestConfig *out_initial,

View File

@ -353,12 +353,26 @@ int tls13_process_finished(SSL_HANDSHAKE *hs, const SSLMessage &msg,
int tls13_add_certificate(SSL_HANDSHAKE *hs) { int tls13_add_certificate(SSL_HANDSHAKE *hs) {
SSL *const ssl = hs->ssl; SSL *const ssl = hs->ssl;
CERT *const cert = hs->config->cert;
ScopedCBB cbb; ScopedCBB cbb;
CBB body, certificate_list; CBB *body, body_storage, certificate_list;
if (!ssl->method->init_message(ssl, cbb.get(), &body, SSL3_MT_CERTIFICATE) ||
// The request context is always empty in the handshake. if (hs->cert_compression_negotiated) {
!CBB_add_u8(&body, 0) || if (!CBB_init(cbb.get(), 1024)) {
!CBB_add_u24_length_prefixed(&body, &certificate_list)) { 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); OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
return 0; return 0;
} }
@ -367,7 +381,6 @@ int tls13_add_certificate(SSL_HANDSHAKE *hs) {
return ssl_add_message_cbb(ssl, cbb.get()); 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); CRYPTO_BUFFER *leaf_buf = sk_CRYPTO_BUFFER_value(cert->chain.get(), 0);
CBB leaf, extensions; CBB leaf, extensions;
if (!CBB_add_u24_length_prefixed(&certificate_list, &leaf) || if (!CBB_add_u24_length_prefixed(&certificate_list, &leaf) ||
@ -378,33 +391,29 @@ int tls13_add_certificate(SSL_HANDSHAKE *hs) {
return 0; return 0;
} }
if (hs->scts_requested && if (hs->scts_requested && cert->signed_cert_timestamp_list != nullptr) {
hs->config->cert->signed_cert_timestamp_list != nullptr) {
CBB contents; CBB contents;
if (!CBB_add_u16(&extensions, TLSEXT_TYPE_certificate_timestamp) || if (!CBB_add_u16(&extensions, TLSEXT_TYPE_certificate_timestamp) ||
!CBB_add_u16_length_prefixed(&extensions, &contents) || !CBB_add_u16_length_prefixed(&extensions, &contents) ||
!CBB_add_bytes( !CBB_add_bytes(
&contents, &contents,
CRYPTO_BUFFER_data( CRYPTO_BUFFER_data(cert->signed_cert_timestamp_list.get()),
hs->config->cert->signed_cert_timestamp_list.get()), CRYPTO_BUFFER_len(cert->signed_cert_timestamp_list.get())) ||
CRYPTO_BUFFER_len(
hs->config->cert->signed_cert_timestamp_list.get())) ||
!CBB_flush(&extensions)) { !CBB_flush(&extensions)) {
OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR); OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
return 0; 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; CBB contents, ocsp_response;
if (!CBB_add_u16(&extensions, TLSEXT_TYPE_status_request) || if (!CBB_add_u16(&extensions, TLSEXT_TYPE_status_request) ||
!CBB_add_u16_length_prefixed(&extensions, &contents) || !CBB_add_u16_length_prefixed(&extensions, &contents) ||
!CBB_add_u8(&contents, TLSEXT_STATUSTYPE_ocsp) || !CBB_add_u8(&contents, TLSEXT_STATUSTYPE_ocsp) ||
!CBB_add_u24_length_prefixed(&contents, &ocsp_response) || !CBB_add_u24_length_prefixed(&contents, &ocsp_response) ||
!CBB_add_bytes( !CBB_add_bytes(&ocsp_response,
&ocsp_response, CRYPTO_BUFFER_data(cert->ocsp_response.get()),
CRYPTO_BUFFER_data(hs->config->cert->ocsp_response.get()), CRYPTO_BUFFER_len(cert->ocsp_response.get())) ||
CRYPTO_BUFFER_len(hs->config->cert->ocsp_response.get())) ||
!CBB_flush(&extensions)) { !CBB_flush(&extensions)) {
OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR); OPENSSL_PUT_ERROR(SSL, ERR_R_INTERNAL_ERROR);
return 0; return 0;
@ -423,7 +432,43 @@ int tls13_add_certificate(SSL_HANDSHAKE *hs) {
} }
} }
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) { enum ssl_private_key_result_t tls13_add_certificate_verify(SSL_HANDSHAKE *hs) {