Skip to content

DTLS 1.3 CertificateRequest + Certificate Messages#774

Merged
theodorsm merged 2 commits intopion:masterfrom
adrianosela:master
Jan 19, 2026
Merged

DTLS 1.3 CertificateRequest + Certificate Messages#774
theodorsm merged 2 commits intopion:masterfrom
adrianosela:master

Conversation

@adrianosela
Copy link
Copy Markdown
Contributor

DTLS 1.3 CertificateRequest + Certificate Messages

Closes #772

  • Implements TLS 1.3 Certificate Request message as CertificateRequest13 per RFC 8446 section 4.3.2
  • Implements TLS 1.3 Certificate message as Certificate13 per RFC 8446 section 4.4.2
  • Adds comprehensive unit tests for both

cc:// @theodorsm

@adrianosela adrianosela requested a review from theodorsm January 14, 2026 06:07
@codecov
Copy link
Copy Markdown

codecov bot commented Jan 14, 2026

Codecov Report

❌ Patch coverage is 81.66667% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 81.61%. Comparing base (9121462) to head (ffa13d6).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
pkg/protocol/handshake/message_certificate_13.go 77.46% 8 Missing and 8 partials ⚠️
...otocol/handshake/message_certificate_request_13.go 87.75% 3 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #774      +/-   ##
==========================================
+ Coverage   81.40%   81.61%   +0.21%     
==========================================
  Files         103      105       +2     
  Lines        5717     5837     +120     
==========================================
+ Hits         4654     4764     +110     
- Misses        684      689       +5     
- Partials      379      384       +5     
Flag Coverage Δ
go 81.61% <81.66%> (+0.21%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@adrianosela adrianosela force-pushed the master branch 3 times, most recently from ef35536 to fd492eb Compare January 14, 2026 07:11
Copy link
Copy Markdown
Member

@theodorsm theodorsm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, thank you a lot for such a fast implementation @adrianosela!! 💯

I have left some comments. I perfer the usage of cryptobyte like you do in the CertificateRequest implementation, it's a lot more readable and easy to catch errors. Additionally, would be cool if you could add some fuzzing tests for unmarshalling. I think it's a powerful way of catching bugs to have as much as possible of our raw bytes parsing fuzzed.

certificateList := []byte{}
for _, entry := range m.CertificateList {
// Add cert_data as a 3-byte length prefix
certDataLen := len(entry.CertificateData)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should check for boundaries <1..2^24-1>.

if certDataLen == 0 || certDataLen > 0xffffff {
    return nil, errInvalidCertificateEntry 
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For testing the upper bound, should I allocate a 16 MB slice... or is testing the zero case sufficient you think @theodorsm ?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think only testing for the zero case is sufficent.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actually max size should be 0xffffff - cert13CertLengthFieldSize. I forgot about the length prefix for the vector, sorry!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct me if I'm wrong, but I think 0xFFFFFF was correct. I think from reading TLS/DTLS specs, variable-length vectors <floor..ceiling> constrain the vector contents length, and the length field is encoded separately:

Variable-length vectors are defined by specifying a subrange of legal
lengths, inclusively, using the notation <floor..ceiling>. When
these are encoded, the actual length precedes the vector's contents
in the byte stream. The length will be in the form of a number
consuming as many bytes as required to hold the vector's specified
maximum (ceiling) length. A variable-length vector with an actual
length field of zero is referred to as an empty vector.

  1. https://datatracker.ietf.org/doc/html/rfc8446#section-3.4

from boringssl code: https://boringssl.googlesource.com/boringssl/%2B/master/ssl/tls13_both.cc#541

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah @JoTurk is right here. The n-byte length fields are independent from the vector whose length they describe.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoTurk, @adrianosela Yes, I agree that the length fields are independent, but to me the confusion arises from the parent vector: CertificateEntry certificate_list<0..2^24-1>.
As the vector holds a list of vectors, then the length prefix of any vector in this list is included in the 2^24-1 boundary no? It then seems to be that the max length for any certificate data must be 0xffffff - cert13CertLengthFieldSize? I would say this problem/confusion is avoided when using the cryptobyte library as it does to the upper bound checking for us. For example, this would return an error:

	certificateData := make([]byte, 0xffffff)
	for i := range certificateData {
		certificateData[i] = byte(i)
	}
	var builder cryptobyte.Builder
	builder.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
		b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
			fmt.Printf("Length of certData: %x\n", len(certificateData))
			b.AddBytes(certificateData)
			fmt.Printf("Length of certData vector with prefix: %x\n", len(b.BytesOrPanic()))
		})
		fmt.Printf("Length of certificate_list vector with prefix: %x\n", len(b.BytesOrPanic()))
	})
	_, err := builder.Bytes()
	if err != nil {
		fmt.Println(err.Error())
	}

// Output: 
Length of certData: ffffff
Length of certData vector with prefix: 1000002
Length of certificate_list vector with prefix: 1000005
cryptobyte: pending child length 16777218 exceeds 3-byte length prefix

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with @adrianosela points about the extension size.

Also in isolation we shouldn't check for the list bound here. because if we do, we'll return errInvalidCertificateEntry when an entry is only out of bounds because it causes the parent list to exceed its limit, in that case we should return errCertificateListTooLong, which is already checked at the end of the loop. So subtracting -3 or -5 here is really a logic / spec bug.

@adrianosela
Copy link
Copy Markdown
Contributor Author

adrianosela commented Jan 14, 2026

Sounds good! Thanks for the fast review! I'll get to it sometime soon / this week.

I'll request a review from you when its ready for another pass.

@adrianosela adrianosela force-pushed the master branch 6 times, most recently from b2b0743 to 51e9f64 Compare January 14, 2026 18:59
@adrianosela
Copy link
Copy Markdown
Contributor Author

@theodorsm ready for another pass.

Q: I put the fuzz tests in the same test file as the other tests. Or do you want them in fuzz_test.go?
Q: I would like to define the 1.3 specific errors in the files they are used, rather than errors.go, what do you think?

No strong opinion from me for either of these items.

@adrianosela adrianosela requested a review from theodorsm January 14, 2026 19:07
@theodorsm
Copy link
Copy Markdown
Member

theodorsm commented Jan 16, 2026

@adrianosela

Q: I put the fuzz tests in the same test file as the other tests. Or do you want them in fuzz_test.go?

I prefer having them in the test file. Having the fuzz cases as small as possible leads to better results (the fuzzer engine with it's coverage guidance have to dig through less paths) + we can seed them better and adjust fail cases if necessary.

Q: I would like to define the 1.3 specific errors in the files they are used, rather than errors.go, what do you think?

We currently have the errors for the other 1.3 extensions in the files they are used, let's continue with that. I think it's cleaner (aesthetically) to have all the errors in one file, if there is a naming conflict of naming between the version, we can suffix 13.

Copy link
Copy Markdown
Member

@theodorsm theodorsm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work! I have two more comments on the PR (one of them being my fault, sorry).

certificateList := []byte{}
for _, entry := range m.CertificateList {
// Add cert_data as a 3-byte length prefix
certDataLen := len(entry.CertificateData)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The actually max size should be 0xffffff - cert13CertLengthFieldSize. I forgot about the length prefix for the vector, sorry!

@adrianosela adrianosela force-pushed the master branch 2 times, most recently from 69cd542 to 87c0158 Compare January 16, 2026 17:49
@adrianosela
Copy link
Copy Markdown
Contributor Author

adrianosela commented Jan 16, 2026

Nice catch on the cert's bounds

e: @JoTurk highlighted the bounds are correct, and I concur.

certificateList := []byte{}
for _, entry := range m.CertificateList {
// Add cert_data as a 3-byte length prefix
certDataLen := len(entry.CertificateData)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JoTurk, @adrianosela Yes, I agree that the length fields are independent, but to me the confusion arises from the parent vector: CertificateEntry certificate_list<0..2^24-1>.
As the vector holds a list of vectors, then the length prefix of any vector in this list is included in the 2^24-1 boundary no? It then seems to be that the max length for any certificate data must be 0xffffff - cert13CertLengthFieldSize? I would say this problem/confusion is avoided when using the cryptobyte library as it does to the upper bound checking for us. For example, this would return an error:

	certificateData := make([]byte, 0xffffff)
	for i := range certificateData {
		certificateData[i] = byte(i)
	}
	var builder cryptobyte.Builder
	builder.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
		b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
			fmt.Printf("Length of certData: %x\n", len(certificateData))
			b.AddBytes(certificateData)
			fmt.Printf("Length of certData vector with prefix: %x\n", len(b.BytesOrPanic()))
		})
		fmt.Printf("Length of certificate_list vector with prefix: %x\n", len(b.BytesOrPanic()))
	})
	_, err := builder.Bytes()
	if err != nil {
		fmt.Println(err.Error())
	}

// Output: 
Length of certData: ffffff
Length of certData vector with prefix: 1000002
Length of certificate_list vector with prefix: 1000005
cryptobyte: pending child length 16777218 exceeds 3-byte length prefix

Implements TLS 1.3 Certificate Request and Certificate messages
per RFC 8446 Sections 4.3.2 and 4.4.2 respectively.

Adds comprehensive unit tests for both.
@adrianosela
Copy link
Copy Markdown
Contributor Author

So if I understand what you are saying: the current bounds check is correct per RFC i.e. each cert_data entry inside certificate_list can be <1..2^24-1>, however because we know that certificate_list itself must be <0..2^24-1> AND cert_data is a 3-byte-length prefixed vector, the actual largest value of that 3-byte-length prefix is 2^24-1-3.

Does that capture your point?


I agree with you, but the cases where either the 3-byte-length prefix or the data that comes after it is incorrectly (but valid per RFC) between 2^24-1-3 and 2^24-1 are already caught properly elsewhere in the code.

Also, if we are going to go with more strict bounds checking, 2^24-1-3 is not really the correct number because extensions take up space too (even when empty we'd have the two-byte-legnth prefix as zero bytes). So the more correct upper bound to check would be 2^24-1-3-2. But that would be a really arbitrary number because it only works for the case where there are no extensions. There might be N extensions, each of unknown length. So there isn't a good number to check for an upper bound other than the RFC's 2^24-1.

Also, we are not accounting for data from certificate_list that's already been read (and thus eaten up part of that 2^24-1's size)...

So, really we may as well check that it is <= 2^24-1-3-2-any_data_from_previous_cert_entries_in_certificate_list.


Personally, I think the bounds check per RFC's bounds (and as @JoTurk pointed out, in accordance with BoringSSLs implementation) are the way to go about this. This also keeps the code simpler, and not any less correct.

Malformed packets with cert_data too large will already be caught later when we validate the total certificate_list size:

	if len(certificateList) > maxUint24 {
		return nil, errCertificateListTooLong
	}

I've moved that check to inside the loop to fail faster. Would that address your concern?

@adrianosela adrianosela requested a review from theodorsm January 18, 2026 16:25
Adds unit tests that test the upper bounds of the produced
certificate_list's length when marshaling a MessageCertificate13
@adrianosela
Copy link
Copy Markdown
Contributor Author

We said earlier that we wouldnt add tests that allocate 16MB slices to test the upper bounds:

I think only testing for the zero case is sufficent.

But I think the conversation above merits adding such tests! Added as a new commit, let me know if you disagree and want me to drop it.

Copy link
Copy Markdown
Member

@theodorsm theodorsm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with the conclusion about the upper bound checking, and thanks for the discussion about it!

This PR looks really good now, especially with all the good tests.

@adrianosela thanks for working on this and the quick iterations 💯.

@theodorsm theodorsm merged commit 44160f0 into pion:master Jan 19, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

DTLS 1.3 Certificate messages

4 participants