Skip to content

Commit 90154a8

Browse files
committed
Implement certificate compression
Certificate compression is defined in RFC 8879: https://datatracker.ietf.org/doc/html/rfc8879 This implementation is client-side only, for server certificates.
1 parent 0b2885c commit 90154a8

13 files changed

+374
-50
lines changed

conn.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,6 +1057,10 @@ func (c *Conn) readHandshake() (interface{}, error) {
10571057
m = new(endOfEarlyDataMsg)
10581058
case typeKeyUpdate:
10591059
m = new(keyUpdateMsg)
1060+
// [UTLS SECTION BEGINS]
1061+
case typeCompressedCertificate:
1062+
m = new(compressedCertificateMsg)
1063+
// [UTLS SECTION ENDS]
10601064
default:
10611065
return nil, c.in.setErrorLocked(c.sendAlert(alertUnexpectedMessage))
10621066
}

go.mod

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module github.com/refraction-networking/utls
2+
3+
go 1.16
4+
5+
require (
6+
github.com/andybalholm/brotli v1.0.4
7+
github.com/klauspost/compress v1.13.6
8+
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
9+
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a
10+
)

go.sum

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY=
2+
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
3+
github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc=
4+
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
5+
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa h1:idItI2DDfCokpg0N51B2VtiLdJ4vAuXC9fnCb2gACo4=
6+
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
7+
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
8+
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a h1:c83jeVQW0KGKNaKBRfelNYNHaev+qawl9yaA825s8XE=
9+
golang.org/x/net v0.0.0-20211111160137-58aab5ef257a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
10+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
11+
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
12+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4=
13+
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
14+
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
15+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
16+
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
17+
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
18+
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=

handshake_client_tls13.go

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@ package tls
66

77
import (
88
"bytes"
9+
"compress/zlib"
910
"crypto"
1011
"crypto/hmac"
1112
"crypto/rsa"
1213
"errors"
1314
"fmt"
1415
"hash"
16+
"io"
1517
"sync/atomic"
1618
"time"
19+
20+
"github.com/andybalholm/brotli"
21+
"github.com/klauspost/compress/zstd"
1722
)
1823

1924
type clientHandshakeStateTLS13 struct {
@@ -484,6 +489,24 @@ func (hs *clientHandshakeStateTLS13) readServerCertificate() error {
484489
}
485490
}
486491

492+
// [UTLS SECTION BEGINS]
493+
receivedCompressedCert := false
494+
// Check to see if we advertised any compression algorithms
495+
if hs.uconn != nil && len(hs.uconn.certCompressionAlgs) > 0 {
496+
// Check to see if the message is a compressed certificate message, otherwise move on.
497+
compressedCertMsg, ok := msg.(*compressedCertificateMsg)
498+
if ok {
499+
receivedCompressedCert = true
500+
hs.transcript.Write(compressedCertMsg.marshal())
501+
502+
msg, err = hs.decompressCert(*compressedCertMsg)
503+
if err != nil {
504+
return fmt.Errorf("tls: failed to decompress certificate message: %w", err)
505+
}
506+
}
507+
}
508+
// [UTLS SECTION ENDS]
509+
487510
certMsg, ok := msg.(*certificateMsgTLS13)
488511
if !ok {
489512
c.sendAlert(alertUnexpectedMessage)
@@ -493,7 +516,12 @@ func (hs *clientHandshakeStateTLS13) readServerCertificate() error {
493516
c.sendAlert(alertDecodeError)
494517
return errors.New("tls: received empty certificates message")
495518
}
496-
hs.transcript.Write(certMsg.marshal())
519+
// [UTLS SECTION BEGINS]
520+
// Previously, this was simply 'hs.transcript.Write(certMsg.marshal())' (without the if).
521+
if !receivedCompressedCert {
522+
hs.transcript.Write(certMsg.marshal())
523+
}
524+
// [UTLS SECTION ENDS]
497525

498526
c.scts = certMsg.certificate.SignedCertificateTimestamps
499527
c.ocspResponse = certMsg.certificate.OCSPStaple
@@ -690,6 +718,80 @@ func (hs *clientHandshakeStateTLS13) sendClientFinished() error {
690718
return nil
691719
}
692720

721+
// [UTLS SECTION BEGINS]
722+
func (hs *clientHandshakeStateTLS13) decompressCert(m compressedCertificateMsg) (*certificateMsgTLS13, error) {
723+
var (
724+
decompressed io.Reader
725+
compressed = bytes.NewReader(m.compressedCertificateMessage)
726+
c = hs.c
727+
)
728+
729+
// Check to see if the peer responded with an algorithm we advertised.
730+
supportedAlg := false
731+
for _, alg := range hs.uconn.certCompressionAlgs {
732+
if m.algorithm == uint16(alg) {
733+
supportedAlg = true
734+
}
735+
}
736+
if !supportedAlg {
737+
c.sendAlert(alertBadCertificate)
738+
return nil, fmt.Errorf("unadvertised algorithm (%d)", m.algorithm)
739+
}
740+
741+
switch CertCompressionAlgo(m.algorithm) {
742+
case CertCompressionBrotli:
743+
decompressed = brotli.NewReader(compressed)
744+
745+
case CertCompressionZlib:
746+
rc, err := zlib.NewReader(compressed)
747+
if err != nil {
748+
c.sendAlert(alertBadCertificate)
749+
return nil, fmt.Errorf("failed to open zlib reader: %w", err)
750+
}
751+
defer rc.Close()
752+
decompressed = rc
753+
754+
case CertCompressionZstd:
755+
rc, err := zstd.NewReader(compressed)
756+
if err != nil {
757+
c.sendAlert(alertBadCertificate)
758+
return nil, fmt.Errorf("failed to open zstd reader: %w", err)
759+
}
760+
defer rc.Close()
761+
decompressed = rc
762+
763+
default:
764+
c.sendAlert(alertBadCertificate)
765+
return nil, fmt.Errorf("unsupported algorithm (%d)", m.algorithm)
766+
}
767+
768+
rawMsg := make([]byte, m.uncompressedLength+4) // +4 for message type and uint24 length field
769+
rawMsg[0] = typeCertificate
770+
rawMsg[1] = uint8(m.uncompressedLength >> 16)
771+
rawMsg[2] = uint8(m.uncompressedLength >> 8)
772+
rawMsg[3] = uint8(m.uncompressedLength)
773+
774+
n, err := decompressed.Read(rawMsg[4:])
775+
if err != nil {
776+
c.sendAlert(alertBadCertificate)
777+
return nil, err
778+
}
779+
if n < len(rawMsg)-4 {
780+
// If, after decompression, the specified length does not match the actual length, the party
781+
// receiving the invalid message MUST abort the connection with the "bad_certificate" alert.
782+
// https://datatracker.ietf.org/doc/html/rfc8879#section-4
783+
c.sendAlert(alertBadCertificate)
784+
return nil, fmt.Errorf("decompressed len (%d) does not match specified len (%d)", n, m.uncompressedLength)
785+
}
786+
certMsg := new(certificateMsgTLS13)
787+
if !certMsg.unmarshal(rawMsg) {
788+
return nil, c.sendAlert(alertUnexpectedMessage)
789+
}
790+
return certMsg, nil
791+
}
792+
793+
// [UTLS SECTION ENDS]
794+
693795
func (c *Conn) handleNewSessionTicket(msg *newSessionTicketMsgTLS13) error {
694796
if !c.isClient {
695797
c.sendAlert(alertUnexpectedMessage)

handshake_messages_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var tests = []interface{}{
3636
&newSessionTicketMsgTLS13{},
3737
&certificateRequestMsgTLS13{},
3838
&certificateMsgTLS13{},
39+
&compressedCertificateMsg{}, // [UTLS]
3940
}
4041

4142
func TestMarshalUnmarshal(t *testing.T) {
@@ -420,6 +421,15 @@ func (*certificateMsgTLS13) Generate(rand *rand.Rand, size int) reflect.Value {
420421
return reflect.ValueOf(m)
421422
}
422423

424+
// [UTLS]
425+
func (*compressedCertificateMsg) Generate(rand *rand.Rand, size int) reflect.Value {
426+
m := &compressedCertificateMsg{}
427+
m.algorithm = uint16(rand.Intn(2 << 15))
428+
m.uncompressedLength = uint32(rand.Intn(2 << 23))
429+
m.compressedCertificateMessage = randomBytes(rand.Intn(500)+1, rand)
430+
return reflect.ValueOf(m)
431+
}
432+
423433
func TestRejectEmptySCTList(t *testing.T) {
424434
// RFC 6962, Section 3.3.1 specifies that empty SCT lists are invalid.
425435

og

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package tls
2+
3+
import (
4+
"golang.org/x/crypto/cryptobyte"
5+
)
6+
7+
// Only implemented client-side, for server certificates.
8+
// Alternate certificate message formats (https://datatracker.ietf.org/doc/html/rfc7250) are not
9+
// supported.
10+
// https://datatracker.ietf.org/doc/html/rfc8879
11+
type compressedCertificateMsg struct {
12+
raw []byte
13+
14+
algorithm uint16
15+
uncompressedLength uint32 // uint24
16+
compressedCertificateMessage []byte
17+
}
18+
19+
func (m *compressedCertificateMsg) marshal() []byte {
20+
if m.raw != nil {
21+
return m.raw
22+
}
23+
24+
var b cryptobyte.Builder
25+
b.AddUint8(typeCompressedCertificate)
26+
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
27+
b.AddUint16(m.algorithm)
28+
b.AddUint24(m.uncompressedLength)
29+
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
30+
b.AddBytes(m.compressedCertificateMessage)
31+
})
32+
})
33+
34+
m.raw = b.BytesOrPanic()
35+
return m.raw
36+
}
37+
38+
func (m *compressedCertificateMsg) unmarshal(data []byte) bool {
39+
*m = compressedCertificateMsg{raw: data}
40+
s := cryptobyte.String(data)
41+
42+
if !s.Skip(4) || // message type and uint24 length field
43+
!s.ReadUint16(&m.algorithm) ||
44+
!s.ReadUint24(&m.uncompressedLength) ||
45+
!readUint24LengthPrefixed(&s, &m.compressedCertificateMessage) {
46+
return false
47+
}
48+
return true
49+
}

pr

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package tls
2+
3+
import (
4+
"golang.org/x/crypto/cryptobyte"
5+
)
6+
7+
// Only implemented client-side, for server certificates.
8+
// Alternate certificate message formats (https://datatracker.ietf.org/doc/html/rfc7250) are not
9+
// supported.
10+
// https://datatracker.ietf.org/doc/html/rfc8879
11+
type compressedCertificateMsg struct {
12+
raw []byte
13+
14+
algorithm uint16
15+
uncompressedLength uint32 // uint24
16+
compressedCertificateMessage []byte
17+
}
18+
19+
func (m *compressedCertificateMsg) marshal() []byte {
20+
if m.raw != nil {
21+
return m.raw
22+
}
23+
24+
var b cryptobyte.Builder
25+
b.AddUint8(typeCompressedCertificate)
26+
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
27+
b.AddUint16(m.algorithm)
28+
b.AddUint24(m.uncompressedLength)
29+
b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) {
30+
b.AddBytes(m.compressedCertificateMessage)
31+
})
32+
})
33+
34+
m.raw = b.BytesOrPanic()
35+
return m.raw
36+
}
37+
38+
func (m *compressedCertificateMsg) unmarshal(data []byte) bool {
39+
*m = compressedCertificateMsg{raw: data}
40+
s := cryptobyte.String(data)
41+
42+
if !s.Skip(4) || // message type and uint24 length field
43+
!s.ReadUint16(&m.algorithm) ||
44+
!s.ReadUint24(&m.uncompressedLength) ||
45+
!readUint24LengthPrefixed(&s, &m.compressedCertificateMessage) {
46+
return false
47+
}
48+
return true
49+
}

u_common.go

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ const (
1919
utlsExtensionPadding uint16 = 21
2020
utlsExtensionExtendedMasterSecret uint16 = 23 // https://tools.ietf.org/html/rfc7627
2121

22+
// https://datatracker.ietf.org/doc/html/rfc8879#section-7.1
23+
utlsExtensionCompressCertificate uint16 = 27
24+
2225
// extensions with 'fake' prefix break connection, if server echoes them back
2326
fakeExtensionChannelID uint16 = 30032 // not IANA assigned
2427

25-
fakeCertCompressionAlgs uint16 = 0x001b
26-
fakeRecordSizeLimit uint16 = 0x001c
28+
fakeRecordSizeLimit uint16 = 0x001c
29+
30+
// https://datatracker.ietf.org/doc/html/rfc8879#section-7.2
31+
typeCompressedCertificate uint8 = 25
2732
)
2833

2934
const (
@@ -37,11 +42,11 @@ const (
3742
FAKE_OLD_TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256 = uint16(0xcc15) // we can try to craft these ciphersuites
3843
FAKE_TLS_DHE_RSA_WITH_AES_128_GCM_SHA256 = uint16(0x009e) // from existing pieces, if needed
3944

40-
FAKE_TLS_DHE_RSA_WITH_AES_128_CBC_SHA = uint16(0x0033)
41-
FAKE_TLS_DHE_RSA_WITH_AES_256_CBC_SHA = uint16(0x0039)
42-
FAKE_TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = uint16(0x009f)
43-
FAKE_TLS_RSA_WITH_RC4_128_MD5 = uint16(0x0004)
44-
FAKE_TLS_EMPTY_RENEGOTIATION_INFO_SCSV = uint16(0x00ff)
45+
FAKE_TLS_DHE_RSA_WITH_AES_128_CBC_SHA = uint16(0x0033)
46+
FAKE_TLS_DHE_RSA_WITH_AES_256_CBC_SHA = uint16(0x0039)
47+
FAKE_TLS_DHE_RSA_WITH_AES_256_GCM_SHA384 = uint16(0x009f)
48+
FAKE_TLS_RSA_WITH_RC4_128_MD5 = uint16(0x0004)
49+
FAKE_TLS_EMPTY_RENEGOTIATION_INFO_SCSV = uint16(0x00ff)
4550
)
4651

4752
// newest signatures
@@ -65,6 +70,7 @@ type CertCompressionAlgo uint16
6570
const (
6671
CertCompressionZlib CertCompressionAlgo = 0x0001
6772
CertCompressionBrotli CertCompressionAlgo = 0x0002
73+
CertCompressionZstd CertCompressionAlgo = 0x0003
6874
)
6975

7076
const (

u_conn.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ type UConn struct {
3232
greaseSeed [ssl_grease_last_index]uint16
3333

3434
omitSNIExtension bool
35+
36+
// certCompressionAlgs represents the set of advertised certificate compression
37+
// algorithms, as specified in the ClientHello. This is only relevant client-side, for the
38+
// server certificate. All other forms of certificate compression are unsupported.
39+
certCompressionAlgs []CertCompressionAlgo
3540
}
3641

3742
// UClient returns a new uTLS client, with behavior depending on clientHelloID.

u_fingerprinter.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,22 @@ func (f *Fingerprinter) FingerprintClientHello(data []byte) (*ClientHelloSpec, e
303303
case utlsExtensionPadding:
304304
clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsPaddingExtension{GetPaddingLen: BoringPaddingStyle})
305305

306-
case fakeExtensionChannelID, fakeCertCompressionAlgs, fakeRecordSizeLimit:
306+
case utlsExtensionCompressCertificate:
307+
methods := []CertCompressionAlgo{}
308+
methodsRaw := new(cryptobyte.String)
309+
if !extData.ReadUint8LengthPrefixed(methodsRaw) {
310+
return nil, errors.New("unable to read cert compression algorithms extension data")
311+
}
312+
for !methodsRaw.Empty() {
313+
var method uint16
314+
if !methodsRaw.ReadUint16(&method) {
315+
return nil, errors.New("unable to read cert compression algorithms extension data")
316+
}
317+
methods = append(methods, CertCompressionAlgo(method))
318+
}
319+
clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &UtlsCompressCertExtension{methods})
320+
321+
case fakeExtensionChannelID, fakeRecordSizeLimit:
307322
clientHelloSpec.Extensions = append(clientHelloSpec.Extensions, &GenericExtension{extension, extData})
308323

309324
case extensionPreSharedKey:

0 commit comments

Comments
 (0)