-
Notifications
You must be signed in to change notification settings - Fork 152
Add config field to enable custom client fingerprints #224
Conversation
I should have hello-specific things in handshake_client
This seems very similar to #223. Do we need both approaches? |
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. |
@bvandersloot Can you resolve merge conflicts with #223? Then I'll review. |
Sad! |
Conflicts: ztools/ztls/common.go ztools/ztls/handshake_client.go
@dadrian: This should be ready for eyes. |
There was a problem hiding this 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.
ztools/ztls/handshake_client.go
Outdated
type NullExtension struct { | ||
} | ||
|
||
func (e NullExtension) Marshal() []byte { |
There was a problem hiding this comment.
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.
ztools/ztls/common.go
Outdated
@@ -226,6 +226,10 @@ type ClientSessionCache interface { | |||
Put(sessionKey string, cs *ClientSessionState) | |||
} | |||
|
|||
type ClientExtension interface { | |||
Marshal() (data []byte) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Changing.
ztools/ztls/common.go
Outdated
|
||
// If non-null specifies the contents of the client-hello | ||
// WARNING: Setting this may invalidate other fields in the Config object | ||
ClientFingerprint *ClientHelloConfiguration |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refactoring both to ClientFingerprintConfiguration
ztools/ztls/handshake_client.go
Outdated
} | ||
|
||
type ClientHelloConfiguration struct { | ||
//Version in the handshake header |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixing throughout.
ztools/ztls/handshake_client.go
Outdated
func (c *ClientHelloConfiguration) ValidateExtensions() error { | ||
for _, ext := range c.Extensions { | ||
switch ext.(type) { | ||
case PointFormatExtension: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
ztools/ztls/handshake_client.go
Outdated
config.ExtendedMasterSecret = false | ||
config.SignedCertificateTimestampExt = false | ||
for i, ext := range c.Extensions { | ||
switch ext.(type) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
adding explicit default: continue
ztools/ztls/handshake_client.go
Outdated
config.SignedCertificateTimestampExt = false | ||
for i, ext := range c.Extensions { | ||
switch ext.(type) { | ||
case SniExtension: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
ztools/ztls/handshake_client.go
Outdated
for i, ext := range c.Extensions { | ||
switch ext.(type) { | ||
case SniExtension: | ||
if ext.(SniExtension).Autopopulate { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What does autopopulate mean?
There was a problem hiding this comment.
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.
ztools/ztls/handshake_client.go
Outdated
case ExtendedMasterSecretExtension: | ||
config.ExtendedMasterSecret = true | ||
case SignatureAlgorithmExtension: | ||
supportedSKXSignatureAlgorithms = ext.(SignatureAlgorithmExtension).getStruct() |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:%s/getStruct/getStructuredAlgorithms/g
ztools/ztls/handshake_client.go
Outdated
return config | ||
} | ||
|
||
func (c *ClientHelloConfiguration) marshal(config *Config) ([]byte, error) { |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yep.
ztools/ztls/common.go
Outdated
@@ -226,6 +226,10 @@ type ClientSessionCache interface { | |||
Put(sessionKey string, cs *ClientSessionState) | |||
} | |||
|
|||
type ClientExtension interface { |
There was a problem hiding this comment.
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
Sorry about the somewhat vague comments, I'm still trying to work out a mental model of this in my head. |
No problem, there is some complexity here. |
This is ready for another pass @dadrian |
There was a problem hiding this 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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
ztools/ztls/handshake_client.go
Outdated
switch casted := ext.(type) { | ||
case SniExtension: | ||
if casted.Autopopulate { | ||
c.Extensions[i] = SniExtension{[]string{config.ServerName}, true} |
There was a problem hiding this comment.
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,
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea.
ztools/ztls/handshake_client.go
Outdated
if casted.Autopopulate { | ||
c.Extensions[i] = SniExtension{[]string{config.ServerName}, true} | ||
} | ||
if config.ServerName == "" && len(casted.Domains) > 0 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
ztools/ztls/handshake_client.go
Outdated
config.ExtendedMasterSecret = false | ||
config.SignedCertificateTimestampExt = false | ||
for i, ext := range c.Extensions { | ||
switch casted := ext.(type) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
.
ztools/ztls/handshake_client.go
Outdated
} else { | ||
_, err := io.ReadFull(config.rand(), head[6:38]) | ||
if err != nil { | ||
return nil, errors.New("tls: short read from Rand: " + err.Error()) |
There was a problem hiding this comment.
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
ztools/ztls/handshake_client.go
Outdated
ciphers[3+i*2] = uint8(suite) | ||
} | ||
|
||
compressions := make([]byte, len(c.CompressionMethods)+1) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
ztools/ztls/handshake_client.go
Outdated
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 { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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()...) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nope!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ztools/ztls/handshake_client.go
Outdated
length[1] = uint8(len(extensions)) | ||
extensions = append(length, extensions...) | ||
} | ||
hello := append(head, append(sessionID, append(ciphers, append(compressions, extensions...)...)...)...) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in a28b378
ztools/ztls/handshake_client.go
Outdated
extensions = append(length, extensions...) | ||
} | ||
hello := append(head, append(sessionID, append(ciphers, append(compressions, extensions...)...)...)...) | ||
hello[1] = uint8((len(hello) - 4) >> 16) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
Comments by @dadrian up to this point should be addressed by |
ztools/ztls/handshake_client.go
Outdated
ClientRandom []byte | ||
// except the top 4 bytes if InsertTimestamp is true | ||
ClientRandom []byte | ||
InsertTimestamp bool |
There was a problem hiding this comment.
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.
@dadrian Seems like we are converging. I fixed a few things and added one small feature. |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I understand now.
ztools/ztls/handshake_client.go
Outdated
} | ||
|
||
// first, let's check if a ClientFingerprintConfiguration template was provided by the config | ||
if c.config.ClientFingerprintConfiguration != nil { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 | ||
} |
There was a problem hiding this comment.
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?
ztools/ztls/handshake_client.go
Outdated
} | ||
} | ||
for i, ext := range c.config.ClientFingerprintConfiguration.Extensions { | ||
switch ext.(type) { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
ztools/ztls/handshake_client.go
Outdated
} | ||
hello = &clientHelloMsg{} | ||
if ok := hello.unmarshal(helloBytes); !ok { | ||
return errors.New("Incompatible Custom Client Fingerprint") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lowercase incompatible
No worries, I've had about the same latency between interactions. |
There was a problem hiding this 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I understand now.
ztools/ztls/handshake_extensions.go
Outdated
return []byte{} | ||
} | ||
|
||
type SniExtension struct { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should be SNIExtension
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing.
ztools/ztls/handshake_extensions.go
Outdated
} | ||
|
||
func (e SupportedCurvesExtension) CheckImplemented() error { | ||
return nil |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding
ztools/ztls/handshake_extensions.go
Outdated
func (e PointFormatExtension) CheckImplemented() error { | ||
for _, format := range e.Formats { | ||
if format != pointFormatUncompressed { | ||
return errors.New(fmt.Sprintf("Unsupported EC Point Format %d", format)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
replace with fmt.Errorf
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing.
ztools/ztls/handshake_extensions.go
Outdated
return nil | ||
} | ||
|
||
func (e HeartbeatExtension) CheckImplemented() error { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Filed.
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding.
@zakird Any further comments? |
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 |
There was a problem hiding this 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.
@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. |
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.