Skip to content

Commit

Permalink
Add DTLS 1.3 sequence number encryption
Browse files Browse the repository at this point in the history
Bug: 715
Change-Id: I87f8a08e9a2258dede21cffb1cfde5802608d30d
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/70667
Reviewed-by: Bob Beck <bbe@google.com>
Commit-Queue: Bob Beck <bbe@google.com>
  • Loading branch information
nharper authored and Boringssl LUCI CQ committed Aug 30, 2024
1 parent d8cd383 commit 7303079
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 45 deletions.
75 changes: 52 additions & 23 deletions ssl/dtls_record.cc
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ uint64_t reconstruct_seqnum(uint16_t wire_seq, uint64_t seq_mask,
return seqnum;
}

static bool parse_dtls13_record_header(SSL *ssl, CBS *in, size_t packet_size,
static bool parse_dtls13_record_header(SSL *ssl, CBS *in, Span<uint8_t> packet,
uint8_t type, CBS *out_body,
uint64_t *out_sequence,
uint16_t *out_epoch,
Expand All @@ -206,29 +206,23 @@ static bool parse_dtls13_record_header(SSL *ssl, CBS *in, size_t packet_size,
// Connection ID bit set, which we didn't negotiate.
return false;
}

// TODO(crbug.com/boringssl/715): Add a runner test that performs many
// key updates to verify epoch reconstruction works for epochs larger than
// 3.
*out_epoch = reconstruct_epoch(type, ssl->d1->r_epoch);
size_t seqlen = 1;
if ((type & 0x08) == 0x08) {
// 16-bit sequence number.
uint16_t seq;
if (!CBS_get_u16(in, &seq)) {
// The record header was incomplete or malformed.
return false;
}
*out_sequence =
reconstruct_seqnum(seq, 0xffff, ssl->d1->bitmap.max_seq_num);
} else {
// 8-bit sequence number.
uint8_t seq;
if (!CBS_get_u8(in, &seq)) {
// The record header was incomplete or malformed.
return false;
}
*out_sequence = reconstruct_seqnum(seq, 0xff, ssl->d1->bitmap.max_seq_num);
// If this bit is set, the sequence number is 16 bits long, otherwise it is
// 8 bits. The seqlen variable tracks the length of the sequence number in
// bytes.
seqlen = 2;
}
if (!CBS_skip(in, seqlen)) {
// The record header was incomplete or malformed.
return false;
}
*out_header_len = packet_size - CBS_len(in);
*out_header_len = packet.size() - CBS_len(in);
if ((type & 0x04) == 0x04) {
*out_header_len += 2;
// 16-bit length present
Expand All @@ -244,6 +238,26 @@ static bool parse_dtls13_record_header(SSL *ssl, CBS *in, size_t packet_size,
return false;
}
}

// Decrypt and reconstruct the sequence number:
uint8_t mask[AES_BLOCK_SIZE];
SSLAEADContext *aead = ssl->s3->aead_read_ctx.get();
if (!aead->GenerateRecordNumberMask(mask, *out_body)) {
// GenerateRecordNumberMask most likely failed because the record body was
// not long enough.
return false;
}
// Apply the mask to the sequence number as it exists in the header. The
// header (with the decrypted sequence number bytes) is used as the
// additional data for the AEAD function. Since we don't support Connection
// ID, the sequence number starts immediately after the type byte.
uint64_t seq = 0;
for (size_t i = 0; i < seqlen; i++) {
packet[i + 1] ^= mask[i];
seq = (seq << 8) | packet[i + 1];
}
*out_sequence = reconstruct_seqnum(seq, (1 << (seqlen * 8)) - 1,
ssl->d1->bitmap.max_seq_num);
return true;
}

Expand Down Expand Up @@ -321,9 +335,8 @@ enum ssl_open_record_t dtls_open_record(SSL *ssl, uint8_t *out_type,
// records use the old record header format.
if ((type & 0xe0) == 0x20 && !aead->is_null_cipher() &&
aead->ProtocolVersion() >= TLS1_3_VERSION) {
valid_record_header =
parse_dtls13_record_header(ssl, &cbs, in.size(), type, &body, &sequence,
&epoch, &record_header_len);
valid_record_header = parse_dtls13_record_header(
ssl, &cbs, in, type, &body, &sequence, &epoch, &record_header_len);
} else {
valid_record_header = parse_dtls_plaintext_record_header(
ssl, &cbs, in.size(), type, &body, &sequence, &epoch,
Expand Down Expand Up @@ -539,8 +552,24 @@ bool dtls_seal_record(SSL *ssl, uint8_t *out, size_t *out_len, size_t max_out,
return false;
}

// TODO(crbug.com/boringssl/715): Perform record number encryption (RFC 9147
// section 4.2.3).
// Perform record number encryption (RFC 9147 section 4.2.3).
if (dtls13_header) {
// Record number encryption uses bytes from the ciphertext as a sample to
// generate the mask used for encryption. For simplicity, pass in the whole
// ciphertext as the sample - GenerateRecordNumberMask will read only what
// it needs (and error if |sample| is too short).
Span<const uint8_t> sample =
MakeConstSpan(out + record_header_len, ciphertext_len);
// AES cipher suites require the mask be exactly AES_BLOCK_SIZE; ChaCha20
// cipher suites have no requirements on the mask size. We only need the
// first two bytes from the mask.
uint8_t mask[AES_BLOCK_SIZE];
if (!aead->GenerateRecordNumberMask(mask, sample)) {
return false;
}
out[1] ^= mask[0];
out[2] ^= mask[1];
}

(*seq)++;
*out_len = record_header_len + ciphertext_len;
Expand Down
64 changes: 64 additions & 0 deletions ssl/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
#include <utility>

#include <openssl/aead.h>
#include <openssl/aes.h>
#include <openssl/curve25519.h>
#include <openssl/err.h>
#include <openssl/hpke.h>
Expand Down Expand Up @@ -811,6 +812,16 @@ bool tls1_prf(const EVP_MD *digest, Span<uint8_t> out,

// Encryption layer.

class RecordNumberEncrypter {
public:
virtual ~RecordNumberEncrypter() = default;
static constexpr bool kAllowUniquePtr = true;

virtual size_t KeySize() = 0;
virtual bool SetKey(Span<const uint8_t> key) = 0;
virtual bool GenerateMask(Span<uint8_t> out, Span<const uint8_t> sample) = 0;
};

// SSLAEADContext contains information about an AEAD that is being used to
// encrypt an SSL connection.
class SSLAEADContext {
Expand Down Expand Up @@ -916,6 +927,17 @@ class SSLAEADContext {

bool GetIV(const uint8_t **out_iv, size_t *out_iv_len) const;

RecordNumberEncrypter *GetRecordNumberEncrypter() {
return rn_encrypter_.get();
}

// GenerateRecordNumberMask computes the mask used for DTLS 1.3 record number
// encryption (RFC 9147 section 4.2.3), writing it to |out|. The |out| buffer
// must be sized to AES_BLOCK_SIZE. The |sample| buffer must be at least 16
// bytes, as required by the AES and ChaCha20 cipher suites in RFC 9147. Extra
// bytes in |sample| will be ignored.
bool GenerateRecordNumberMask(Span<uint8_t> out, Span<const uint8_t> sample);

private:
// GetAdditionalData returns the additional data, writing into |storage| if
// necessary.
Expand All @@ -924,6 +946,8 @@ class SSLAEADContext {
uint64_t seqnum, size_t plaintext_len,
Span<const uint8_t> header);

void CreateRecordNumberEncrypter();

const SSL_CIPHER *cipher_;
ScopedEVP_AEAD_CTX ctx_;
// fixed_nonce_ contains any bytes of the nonce that are fixed for all
Expand All @@ -932,6 +956,7 @@ class SSLAEADContext {
uint8_t fixed_nonce_len_ = 0, variable_nonce_len_ = 0;
// version_ is the wire version that should be used with this AEAD.
uint16_t version_;
UniquePtr<RecordNumberEncrypter> rn_encrypter_;
// is_dtls_ is whether DTLS is being used with this AEAD.
bool is_dtls_;
// variable_nonce_included_in_record_ is true if the variable nonce
Expand All @@ -951,6 +976,45 @@ class SSLAEADContext {
bool ad_is_header_ : 1;
};

class AESRecordNumberEncrypter : public RecordNumberEncrypter {
public:
bool SetKey(Span<const uint8_t> key) override;
bool GenerateMask(Span<uint8_t> out, Span<const uint8_t> sample) override;

private:
AES_KEY key_;
};

class AES128RecordNumberEncrypter : public AESRecordNumberEncrypter {
public:
size_t KeySize() override;
};

class AES256RecordNumberEncrypter : public AESRecordNumberEncrypter {
public:
size_t KeySize() override;
};

class ChaChaRecordNumberEncrypter : public RecordNumberEncrypter {
public:
size_t KeySize() override;
bool SetKey(Span<const uint8_t> key) override;
bool GenerateMask(Span<uint8_t> out, Span<const uint8_t> sample) override;

private:
static const size_t kKeySize = 32;
uint8_t key_[kKeySize];
};

#if defined(BORINGSSL_UNSAFE_FUZZER_MODE)
class NullRecordNumberEncrypter : public RecordNumberEncrypter {
public:
size_t KeySize() override;
bool SetKey(Span<const uint8_t> key) override;
bool GenerateMask(Span<uint8_t> out, Span<const uint8_t> sample) override;
};
#endif // BORINGSSL_UNSAFE_FUZZER_MODE


// DTLS replay bitmap.

Expand Down
85 changes: 85 additions & 0 deletions ssl/ssl_aead_ctx.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <string.h>

#include <openssl/aead.h>
#include <openssl/chacha.h>
#include <openssl/err.h>
#include <openssl/rand.h>

Expand All @@ -44,6 +45,7 @@ SSLAEADContext::SSLAEADContext(uint16_t version_arg, bool is_dtls_arg,
omit_length_in_ad_(false),
ad_is_header_(false) {
OPENSSL_memset(fixed_nonce_, 0, sizeof(fixed_nonce_));
CreateRecordNumberEncrypter();
}

SSLAEADContext::~SSLAEADContext() {}
Expand Down Expand Up @@ -145,6 +147,23 @@ UniquePtr<SSLAEADContext> SSLAEADContext::Create(
return aead_ctx;
}

void SSLAEADContext::CreateRecordNumberEncrypter() {
if (!cipher_) {
return;
}
#if defined(BORINGSSL_UNSAFE_FUZZER_MODE)
rn_encrypter_ = MakeUnique<NullRecordNumberEncrypter>();
#else
if (cipher_->algorithm_enc == SSL_AES128GCM) {
rn_encrypter_ = MakeUnique<AES128RecordNumberEncrypter>();
} else if (cipher_->algorithm_enc == SSL_AES256GCM) {
rn_encrypter_ = MakeUnique<AES256RecordNumberEncrypter>();
} else if (cipher_->algorithm_enc == SSL_CHACHA20POLY1305) {
rn_encrypter_ = MakeUnique<ChaChaRecordNumberEncrypter>();
}
#endif // BORINGSSL_UNSAFE_FUZZER_MODE
}

UniquePtr<SSLAEADContext> SSLAEADContext::CreatePlaceholderForQUIC(
uint16_t version, const SSL_CIPHER *cipher) {
return MakeUnique<SSLAEADContext>(version, false, cipher);
Expand Down Expand Up @@ -427,4 +446,70 @@ bool SSLAEADContext::GetIV(const uint8_t **out_iv, size_t *out_iv_len) const {
EVP_AEAD_CTX_get_iv(ctx_.get(), out_iv, out_iv_len);
}

bool SSLAEADContext::GenerateRecordNumberMask(Span<uint8_t> out,
Span<const uint8_t> sample) {
if (!rn_encrypter_) {
return false;
}
return rn_encrypter_->GenerateMask(out, sample);
}

size_t AES128RecordNumberEncrypter::KeySize() { return 16; }

size_t AES256RecordNumberEncrypter::KeySize() { return 32; }

bool AESRecordNumberEncrypter::SetKey(Span<const uint8_t> key) {
return AES_set_encrypt_key(key.data(), key.size() * 8, &key_) == 0;
}

bool AESRecordNumberEncrypter::GenerateMask(Span<uint8_t> out,
Span<const uint8_t> sample) {
if (sample.size() < AES_BLOCK_SIZE || out.size() != AES_BLOCK_SIZE) {
return false;
}
AES_encrypt(sample.data(), out.data(), &key_);
return true;
}

size_t ChaChaRecordNumberEncrypter::KeySize() { return kKeySize; }

bool ChaChaRecordNumberEncrypter::SetKey(Span<const uint8_t> key) {
if (key.size() != kKeySize) {
return false;
}
OPENSSL_memcpy(key_, key.data(), key.size());
return true;
}

bool ChaChaRecordNumberEncrypter::GenerateMask(Span<uint8_t> out,
Span<const uint8_t> sample) {
Array<uint8_t> zeroes;
if (!zeroes.Init(out.size())) {
return false;
}
OPENSSL_memset(zeroes.data(), 0, zeroes.size());
// RFC 9147 section 4.2.3 uses the first 4 bytes of the sample as the counter
// and the next 12 bytes as the nonce. If we have less than 4+12=16 bytes in
// the sample, then we'll read past the end of the |sample| buffer.
if (sample.size() < 16) {
return false;
}
uint32_t counter = CRYPTO_load_u32_be(sample.data());
Span<const uint8_t> nonce = sample.subspan(4);
CRYPTO_chacha_20(out.data(), zeroes.data(), zeroes.size(), key_, nonce.data(),
counter);
return true;
}

#if defined(BORINGSSL_UNSAFE_FUZZER_MODE)
size_t NullRecordNumberEncrypter::KeySize() { return 0; }
bool NullRecordNumberEncrypter::SetKey(Span<const uint8_t> key) { return true; }

bool NullRecordNumberEncrypter::GenerateMask(Span<uint8_t> out,
Span<const uint8_t> sample) {
OPENSSL_memset(out.data(), 0, out.size());
return true;
}
#endif // BORINGSSL_UNSAFE_FUZZER_MODE

BSSL_NAMESPACE_END
Loading

0 comments on commit 7303079

Please sign in to comment.