Skip to content
This repository has been archived by the owner on Jul 8, 2021. It is now read-only.

Add config field to enable custom client fingerprints #224

Merged
merged 30 commits into from
Feb 26, 2017

Conversation

bvandersloot
Copy link
Contributor

These changes enable a zTLS consumer to specify exactly the format of the ClientHello message.
This is useful for censorship-resistance, so zTLS clients can camouflage themselves as other traffic.

The mechanism this uses is an additional field in the Config struct, ClientFingerprint, that when non-null specifies the contents of the ClientHello message. When this field is nil, everything proceeds as normal.

I repeat: WHEN THIS CONFIG FIELD IS nil, THESE CODE PATHS ARE NOT HIT

The extensions supported out-of-the-box are:

But a consumer can also specify their own extensions and roll them in as well.
Custom Session Caches are also allowed.

@zakird
Copy link
Member

zakird commented Feb 4, 2017

This seems very similar to #223. Do we need both approaches?

@bvandersloot
Copy link
Contributor Author

We don't need both. Mine is only a modification to the zTLS library, and will not be accessible from the zGrab command line without further changes. My version also does not support sending arbitrary (potentially unimplemented, or even Google black-box) extensions over the wire out-of-the-box.

@dadrian
Copy link
Member

dadrian commented Feb 4, 2017

I'm currently more inclined to this approach than #223. That being said, let's move any broad discussion of goals, etc to #223, rather than having to split threads. If we end up with your approach, I'll come back and review the code here.

@dadrian
Copy link
Member

dadrian commented Feb 8, 2017

@bvandersloot Can you resolve merge conflicts with #223? Then I'll review.

@bvandersloot
Copy link
Contributor Author

Sad!

@bvandersloot
Copy link
Contributor Author

@dadrian: This should be ready for eyes.

Copy link
Member

@dadrian dadrian left a comment

Choose a reason for hiding this comment

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

Here's some comments to start.

type NullExtension struct {
}

func (e NullExtension) Marshal() []byte {
Copy link
Member

Choose a reason for hiding this comment

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

These should probably be pointer receivers.

@@ -226,6 +226,10 @@ type ClientSessionCache interface {
Put(sessionKey string, cs *ClientSessionState)
}

type ClientExtension interface {
Marshal() (data []byte)
Copy link
Member

Choose a reason for hiding this comment

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

You don't need a named result here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sure. Changing.


// If non-null specifies the contents of the client-hello
// WARNING: Setting this may invalidate other fields in the Config object
ClientFingerprint *ClientHelloConfiguration
Copy link
Member

Choose a reason for hiding this comment

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

Why are these named differently?

Copy link
Contributor Author

@bvandersloot bvandersloot Feb 9, 2017

Choose a reason for hiding this comment

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

Refactoring both to ClientFingerprintConfiguration

}

type ClientHelloConfiguration struct {
//Version in the handshake header
Copy link
Member

Choose a reason for hiding this comment

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

There should be a space between // and the rest of the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixing throughout.

func (c *ClientHelloConfiguration) ValidateExtensions() error {
for _, ext := range c.Extensions {
switch ext.(type) {
case PointFormatExtension:
Copy link
Member

Choose a reason for hiding this comment

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

Why are there only two extensions here? What happens in the default case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Most extensions don't need to make sure their details are implemented, so the default is no action. Validate may not be the best wording. Also, maybe this is best rolled into another function in the Extension interface, and calling it on all extensions here?

Copy link
Member

Choose a reason for hiding this comment

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

I agree, should probably be part of the extension interface.

config.ExtendedMasterSecret = false
config.SignedCertificateTimestampExt = false
for i, ext := range c.Extensions {
switch ext.(type) {
Copy link
Member

Choose a reason for hiding this comment

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

This does not have a default case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

adding explicit default: continue

config.SignedCertificateTimestampExt = false
for i, ext := range c.Extensions {
switch ext.(type) {
case SniExtension:
Copy link
Member

Choose a reason for hiding this comment

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

You should be able to do assignment in the switch statement, rather than having to recast inside of each case body.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I forgot you can do that!

for i, ext := range c.Extensions {
switch ext.(type) {
case SniExtension:
if ext.(SniExtension).Autopopulate {
Copy link
Member

Choose a reason for hiding this comment

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

What does autopopulate mean?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fill in the SNI extension per-request. That way a new Config is not needed per-domain.

case ExtendedMasterSecretExtension:
config.ExtendedMasterSecret = true
case SignatureAlgorithmExtension:
supportedSKXSignatureAlgorithms = ext.(SignatureAlgorithmExtension).getStruct()
Copy link
Member

Choose a reason for hiding this comment

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

getStruct is an incredibly vague function name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

:%s/getStruct/getStructuredAlgorithms/g

return config
}

func (c *ClientHelloConfiguration) marshal(config *Config) ([]byte, error) {
Copy link
Member

Choose a reason for hiding this comment

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

Why is this method on ClientHelloConfiguration? Why does it need a Config?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is making the on-the-wire version of the ClientHelloConfiguration. I have this distinct from the clientHelloMsg serializer to make clearer that they are very different cases. It needs the Config to specify the random number generator, and to see if the config is specifying ForceSuites.

Copy link
Member

@dadrian dadrian Feb 9, 2017

Choose a reason for hiding this comment

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

So a ClientHelloConfiguration still marshals into a TLS ClientHello? It doesn't go through the "normal" ClientHello?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep.

@@ -226,6 +226,10 @@ type ClientSessionCache interface {
Put(sessionKey string, cs *ClientSessionState)
}

type ClientExtension interface {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This also should not be in this file- moving it to be next to the definition of ClientFingerprintConfiguration

@dadrian
Copy link
Member

dadrian commented Feb 9, 2017

Sorry about the somewhat vague comments, I'm still trying to work out a mental model of this in my head.

@bvandersloot
Copy link
Contributor Author

No problem, there is some complexity here.

@bvandersloot
Copy link
Contributor Author

This is ready for another pass @dadrian

Copy link
Member

@dadrian dadrian left a comment

Choose a reason for hiding this comment

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

I've gone through up to marshal.

CacheKey CacheKeyGenerator
}

type ClientExtension interface {
Copy link
Member

Choose a reason for hiding this comment

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

Can you document how this interface is supposed to behave?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding some comments for each member function

switch casted := ext.(type) {
case SniExtension:
if casted.Autopopulate {
c.Extensions[i] = SniExtension{[]string{config.ServerName}, true}
Copy link
Member

Choose a reason for hiding this comment

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

You should use named fields here, instead of relying on ordering. This also prevents compilation from breaking if you add a field.

c.Extensions[i] = SniExtension{
   Domains: []string{config.ServerName},
   Autopopulate: true,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea.

if casted.Autopopulate {
c.Extensions[i] = SniExtension{[]string{config.ServerName}, true}
}
if config.ServerName == "" && len(casted.Domains) > 0 {
Copy link
Member

Choose a reason for hiding this comment

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

I'm a little confused by this logic. We only use casted.Domains if config.ServerName is empty? Can you add some comments to explain what's going on?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding a comment to clarify. This is to allow certificate verification to proceed using a SNI name.

config.ExtendedMasterSecret = false
config.SignedCertificateTimestampExt = false
for i, ext := range c.Extensions {
switch casted := ext.(type) {
Copy link
Member

Choose a reason for hiding this comment

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

This entire switch should be made a method in the ClientExtension interface.

WriteToConfig(*Config) error

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding. Calling for each ext.

} else {
_, err := io.ReadFull(config.rand(), head[6:38])
if err != nil {
return nil, errors.New("tls: short read from Rand: " + err.Error())
Copy link
Member

Choose a reason for hiding this comment

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

I'd probably use fmt.Errorf for this, but /shrug

ciphers[3+i*2] = uint8(suite)
}

compressions := make([]byte, len(c.CompressionMethods)+1)
Copy link
Member

Choose a reason for hiding this comment

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

Same comment about length verification

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Shouldn't be a problem with the checks below, but adding because safety is best.

Copy link
Member

Choose a reason for hiding this comment

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

👍

copy(compressions[1:], c.CompressionMethods)
if c.CompressionMethods[0] != 0 {
return nil, errors.New(fmt.Sprintf("tls: unimplemented compression method %d", c.CompressionMethods[0]))
} else if len(c.CompressionMethods) > 1 {
Copy link
Member

Choose a reason for hiding this comment

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

This shouldn't be an else. It should be a separate if

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Does the RFC specify to not send a compression method more than once?

Copy link
Member

Choose a reason for hiding this comment

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

So? The network is evil and wants to kill you.


var extensions []byte
for _, ext := range c.Extensions {
extensions = append(extensions, ext.Marshal()...)
Copy link
Member

Choose a reason for hiding this comment

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

Can ext.Marshal() ever fail?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope!

Copy link
Member

Choose a reason for hiding this comment

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

bold-strategy-cotton

length[1] = uint8(len(extensions))
extensions = append(length, extensions...)
}
hello := append(head, append(sessionID, append(ciphers, append(compressions, extensions...)...)...)...)
Copy link
Member

Choose a reason for hiding this comment

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

This is kind of comical.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Do you know a better way to append a bunch of slices together in golang?

Copy link
Member

Choose a reason for hiding this comment

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

Not really. Although, preallocating a slice and then appending in a loop of a [][]byte might read a little easier and be faster? Alternatively, you could try to preallocate everything before you serialize, and then just slice off the relevant chunks as you go.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed in a28b378

extensions = append(length, extensions...)
}
hello := append(head, append(sessionID, append(ciphers, append(compressions, extensions...)...)...)...)
hello[1] = uint8((len(hello) - 4) >> 16)
Copy link
Member

Choose a reason for hiding this comment

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

Save len(hello) - 4 to a variable first

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding. Also adding a check that len(hello) - 4 <= 1 << 24

@bvandersloot
Copy link
Contributor Author

Comments by @dadrian up to this point should be addressed by 37bbabb

ClientRandom []byte
// except the top 4 bytes if InsertTimestamp is true
ClientRandom []byte
InsertTimestamp bool
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems like a good thing to do, as far as camouflaging clients go. Android does this, apparently.

@bvandersloot
Copy link
Contributor Author

@dadrian Seems like we are converging. I fixed a few things and added one small feature.

Copy link
Member

@dadrian dadrian left a comment

Choose a reason for hiding this comment

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

Sorry I'm so slow on this. Almost there!

Marshal() []byte

// Function will return an error if zTLS does not implement the necessary features for this extension
CheckImplemented() error
Copy link
Member

Choose a reason for hiding this comment

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

Can you give an example of when this will return non-nil?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In the Hash and Signature Algorithm Extension, if a algorithm combination listed to be offered by the client is not actually in zTLS. Or even a non-defined value.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I understand now.

}

// first, let's check if a ClientFingerprintConfiguration template was provided by the config
if c.config.ClientFingerprintConfiguration != nil {
Copy link
Member

Choose a reason for hiding this comment

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

This can go in the above if

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Merging the above if into this.

candidateSession.vers <= c.config.ClientFingerprintConfiguration.HandshakeVersion
if versOk && cipherSuiteOk {
session = candidateSession
}
Copy link
Member

Choose a reason for hiding this comment

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

Is this not an error case otherwise? Do you have to match version to the previous session if resuming?

}
}
for i, ext := range c.config.ClientFingerprintConfiguration.Extensions {
switch ext.(type) {
Copy link
Member

Choose a reason for hiding this comment

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

This should probably also be part of the extension interface

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a special case only for session management.

}
hello = &clientHelloMsg{}
if ok := hello.unmarshal(helloBytes); !ok {
return errors.New("Incompatible Custom Client Fingerprint")
Copy link
Member

Choose a reason for hiding this comment

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

Lowercase incompatible

@bvandersloot
Copy link
Contributor Author

No worries, I've had about the same latency between interactions.

Copy link
Member

@dadrian dadrian left a comment

Choose a reason for hiding this comment

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

Almost done!

Marshal() []byte

// Function will return an error if zTLS does not implement the necessary features for this extension
CheckImplemented() error
Copy link
Member

Choose a reason for hiding this comment

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

Oh, I understand now.

return []byte{}
}

type SniExtension struct {
Copy link
Member

Choose a reason for hiding this comment

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

Should be SNIExtension

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changing.

}

func (e SupportedCurvesExtension) CheckImplemented() error {
return nil
Copy link
Member

Choose a reason for hiding this comment

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

We support all possible curves?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding

func (e PointFormatExtension) CheckImplemented() error {
for _, format := range e.Formats {
if format != pointFormatUncompressed {
return errors.New(fmt.Sprintf("Unsupported EC Point Format %d", format))
Copy link
Member

Choose a reason for hiding this comment

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

replace with fmt.Errorf

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changing.

return nil
}

func (e HeartbeatExtension) CheckImplemented() error {
Copy link
Member

Choose a reason for hiding this comment

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

File an issue that we don't actually support Heartbeat, only kind of.

Copy link
Member

Choose a reason for hiding this comment

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

We can't receive arbitrary Heartbeat messages at arbitrary times.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Filed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

#237

"fmt"
)

type NullExtension struct {
Copy link
Member

Choose a reason for hiding this comment

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

This file should all be pointer receivers, unless you have a good reason not to.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding.

@dadrian
Copy link
Member

dadrian commented Feb 15, 2017

@zakird Any further comments?

@zakird
Copy link
Member

zakird commented Feb 19, 2017

This mostly looks fine to me. However, this is a lot of code that touches a lot of things in a lot of places, and I have little confidence from reading it over that it works.

I'd like to see at least a few unit tests that confirm that what goes into a ClientFingerprintConfiguration leads to the expected Client Hello. I suspect you must have done this already somehow to see if any of this worked. Let's just get them into tests.

Copy link
Member

@zakird zakird left a comment

Choose a reason for hiding this comment

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

I'm going to request changes on this this until there are tests in place. This is a lot of code, and I have little confidence that this works without tests in place. In the longterm, I think this will also benefit you.

@bvandersloot
Copy link
Contributor Author

@zakird, I went with one large functional test that imitates a Firefox hello, rather than trying to unit test. This seems to match the granularity of existing tests. It passes and I manually inspected the hexdump test file and all of the configuration changes were reflected on the wire.

@zakird zakird merged commit bd9d4b8 into master Feb 26, 2017
@dadrian dadrian deleted the client-fingerprint branch May 3, 2017 14:49
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
3 participants