-
Notifications
You must be signed in to change notification settings - Fork 50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Delegated Credentials (WORK IN PROGRESS) #29
Conversation
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 needs a major structural overhaul.
common.go
Outdated
@@ -93,6 +94,7 @@ const ( | |||
extensionNextProtoNeg uint16 = 13172 // not IANA assigned | |||
extensionRenegotiationInfo uint16 = 0xff01 | |||
extensionShortHeaders uint16 = 0xff03 // Experimental | |||
extensionDelegatedCredential uint16 = 99 // not IANA assigned yet - https://tools.ietf.org/html/draft-rescorla-tls-subcerts-01 |
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.
Please use something in the 0xff experimental range for now.
common.go
Outdated
@@ -614,6 +616,9 @@ type Config struct { | |||
// this Config is returned by a GetConfigForClient callback. It's used | |||
// by serverInit in order to copy session ticket keys if needed. | |||
originalConfig *Config | |||
|
|||
// states if Delegated Credentials should be used | |||
useDelegatedCredentials 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 should be a public element so that a client can set its value when setting up a new TLS connection.
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're right, however, how come I still can access it? https://github.com/tsusanka/tls-tris/blob/tsusanka/delegated-cred/handshake_delegated_cred_test.go#L254
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 can still access it because you're in the same package. However, external packages won't be able to set this value.
common.go
Outdated
func (hs *serverHandshakeState) isCertificateValidForDelegationUsage() bool { | ||
|
||
for _, extension := range hs.cert.Leaf.Extensions { | ||
if extension.Id.Equal(asn1.ObjectIdentifier{2, 5, 29, 99}) { // TODO: change this to constant? |
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.
Yes, this should be a constant.
handshake_client.go
Outdated
hs.delegatedCredential = dc | ||
// continue handshake with the credentials | ||
// I'm still not sure this a good practise, it might be confusing | ||
certs[0].PublicKey = dc.PublicKey |
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.
Probably not what you want to do. The logic below for checking the public key type should still check the public key. The delegated credential should be checked separately.
handshake_messages.go
Outdated
import "bytes" | ||
import ( | ||
"bytes" | ||
) |
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 necessary
@@ -356,6 +369,24 @@ Curves: | |||
} | |||
} | |||
|
|||
if hs.clientHello.delegatedCredentials { |
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 logic should most likely be moved fully into GetCertificate. A cleaner design would be to simply create a new PrivateKey in the certificate object that corresponds to the delegated credential private key. That way almost all logic on the server side would be unchanged, aside from the addition of a new opaque delegated credentials object in the serverhello.
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.
Thanks. I'm a little confused, how to use the GetCertificate function. If the c.GetCertificate is not null it is called to retrieve the certificate.
So, my goal is to implement a new GetCertificate function which I then "override" how the certificate is retrieved, right? The function will add the delegated credential and its PrivateKey. However, where this code should be? Also in the subpackage? Where do I then set the config to use this function?
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.
Ah, alright, I simply set the function in the config when spinning up TLS, right? Similar to this
handshake_delegated_cred.go
Outdated
"errors" | ||
"time" | ||
) | ||
|
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 tight integration with the handshake is not ideal. I think all the credential minting and verifying should be abstracted into its own package (byte slice + cert to PrivateKey and back). The only pieces that need to change on the client side are the sending of the empty extension, the extraction and validation of the delegated credential, and the CertificateVerify function.
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.
The only pieces that need to change on the client side are the sending of the empty extension, the extraction and validation of the delegated credential, and the CertificateVerify function.
I'm confused about the CertificateVerify part. Do you mean the CertificateVerify message? But that's used for client authentication, isn't it?
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.
Actually, in TLS 1.2 this is called the ServerKeyExchange.
common.go
Outdated
@@ -11,6 +11,7 @@ import ( | |||
"crypto/rand" | |||
"crypto/sha512" | |||
"crypto/x509" | |||
"encoding/asn1" |
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.
As mentioned below, this is not a great dependency to add. Factor out as much logic as possible into its own package and try to keep the changes to crypto/tls minimal.
common.go
Outdated
@@ -949,6 +954,9 @@ type Certificate struct { | |||
// OCSPStaple contains an optional OCSP response which will be served | |||
// to clients that request it. | |||
OCSPStaple []byte | |||
// DelegatedCredential contains a signed object containing a public | |||
// key for signing handshakes | |||
DelegatedCredential DelegatedCredential |
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 probably the wrong place for this type of structure. When the Certificate is returned from GetCertificate it should already have a serialized opaque delegated credential, not a structure that needs to be serialized.
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 understand. The Certificate struct will then contain marshalled DC and the associated PrivateKey. Both received from the delcred sub package.
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.
Perhaps consider reusing the existing PrivateKey struct.
handshake_delegated_cred_test.go
Outdated
@@ -0,0 +1,283 @@ | |||
// Copyright 2010 The Go Authors. All rights reserved. | |||
// Use of this source code is governed by a BSD-style | |||
// license that can be found in the LICENSE file. |
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.
Again, this is too tightly integrated with the handshake. Each individual function should be unit tested with respect to its functionality, the internal tls structures should not be involved.
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.
Okay. So I should add unit tests - e.g. check if the credential marshalled correctly etc. What is wrong with those higher-level tests though? They still provide value, don't they?
Config.Clone and other ancillary functions are also missing the new data. |
Also note that the tests are failing. |
Thanks for the review. I'm aware of the failing tests, it is some kind of race condition issue, I'll have a look into that once I fix the issues, you outlined above. |
5e023e6
to
acc9ee8
Compare
acc9ee8
to
3df9bf9
Compare
WIP - this is not complete 1.3 support and is not yet tested
@@ -33,7 +34,7 @@ type keyAgreement interface { | |||
|
|||
// This method may not be called if the server doesn't send a | |||
// ServerKeyExchange message. | |||
processServerKeyExchange(*Config, *clientHelloMsg, *serverHelloMsg, *x509.Certificate, *serverKeyExchangeMsg) error | |||
processServerKeyExchange(*Config, *clientHelloMsg, *serverHelloMsg, crypto.PublicKey, *serverKeyExchangeMsg) 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.
It might be possible to avoid this change by wrapping the DC's PublicKey into an x509.Certificate. However:
a) I'm not sure it'll work since we have only the PublicKey
b) all processServerKeyExchange functions use only the public key, so the dependency for x509.Certificate seems redundant
c) the function is not exported, so it should not be a BC break (?)
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 slick change. I can't see why it wouldn't work or why *x509.Certificate was used in the first place.
I think this is ready for another round of code review.
|
@@ -33,7 +34,7 @@ type keyAgreement interface { | |||
|
|||
// This method may not be called if the server doesn't send a | |||
// ServerKeyExchange message. | |||
processServerKeyExchange(*Config, *clientHelloMsg, *serverHelloMsg, *x509.Certificate, *serverKeyExchangeMsg) error | |||
processServerKeyExchange(*Config, *clientHelloMsg, *serverHelloMsg, crypto.PublicKey, *serverKeyExchangeMsg) 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.
This is a slick change. I can't see why it wouldn't work or why *x509.Certificate was used in the first place.
delegated_credentials.go
Outdated
} | ||
|
||
func NewDelCredGetCertificateFunction(cert *Certificate) GetCertificate { | ||
|
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.
Unnecessary space?
delegated_credentials.go
Outdated
PublicKey interface{} | ||
} | ||
|
||
func NewDelCredGetCertificateFunction(cert *Certificate) GetCertificate { |
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 abbreviation is a bit awkward. NewGetCertificateFromDelegatedCredential? That's also long but a bit better.
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.
Yeah, I don't really like it myself. I'm not sure the From
is accurate though. So how about simply NewDelegatedCredentialGetCertificate
?
delegated_credentials.go
Outdated
func NewDelCredGetCertificateFunction(cert *Certificate) GetCertificate { | ||
|
||
return func(clientHelloInfo *ClientHelloInfo) (*Certificate, 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.
Space
delegated_credentials.go
Outdated
} | ||
|
||
func selectVersion(versions []uint16) uint16 { | ||
|
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.
Newline
delegated_credentials.go
Outdated
if scheme == ECDSAWithP256AndSHA256 { | ||
privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
credential.PublicKey = privateKey.(crypto.Signer).Public() | ||
|
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.
newline
delegated_credentials.go
Outdated
|
||
copy(cred[7:7+publicKeyLength], publicKeyBytes) | ||
|
||
cred[publicKeyLength+7] = uint8(scheme >> 8) // hash |
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 will likely be different for TLS 1.3
handshake_client.go
Outdated
finishedHash finishedHash | ||
masterSecret []byte | ||
session *ClientSessionState | ||
delCredPublicKey crypto.PublicKey |
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.
Maybe this should be just connection PublicKey, and it is set to either the delegated cred key or c.peerCertificates[0].PublicKey. Then you may be able to reduce the number of if statements below.
c853649
to
8bbddcd
Compare
) | ||
|
||
// After only renewThreshold of credential's validTime is left a new one is created | ||
const renewThreshold = 0.2 |
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.
Maybe better to reverse this? Set it to 0.8
and call it renewAfterThreshold
8bbddcd
to
94d5a8b
Compare
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.
The caching logic needs to be moved out of the global namespace.
// SignatureScheme to ensure client's support. | ||
func createDelegatedCredential(config *CredentialsConfig) (credential DelegatedCredential, privateKey crypto.PrivateKey, renewAfter time.Time, err error) { | ||
validTill := config.timeFunc().Add(config.Validity) | ||
relativeToCert := validTill.Sub(config.Cert.Leaf.NotBefore) |
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.
The computation of the ValidTime is a rather complex dance. Is there a simpler way to compute it that is more readable?
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.
Yeah, I agree. I was thinking about it and I'm not sure there is a better way though. The code is basically the definition in itself:
Relative time in seconds from the beginning of the certificate's notBefore value after which the Delegated Credential is no longer valid
Btw why is this relative? Why not making it a simple datetime when the credential expires? The client needs to verify the certificate's validity anyway.
I can remove the relativeToCert
variable but it's not significantly better:
validTill := config.timeFunc().Add(config.Validity)
renewAfter = validTill.Add(-time.Duration(config.Validity.Seconds()*renewThreshold) * time.Second)
credential = DelegatedCredential{
ValidTime: int64(validTill.Sub(config.Cert.Leaf.NotBefore).Seconds()),
}
delegated_credentials.go
Outdated
renewChan chan struct{} | ||
} | ||
|
||
var cachedCredentials map[SignatureScheme]*cachedCredential |
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 package-global variable indexed by the SignatureScheme. This means it will be shared between all instantiations, even those with different certs. You most likely want to keep this local to the CredentialsConfig.
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 point! So I'm just including it as a not exported CredentialsConfig variable.
delegated_credentials.go
Outdated
go func() { | ||
cachedCredentials[config.scheme], err = newCredential(config) | ||
if config.renewChan != nil { | ||
config.renewChan <- 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.
Where is the renewChan read?
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.
Only in tests. I was reluctant to add the channel just because of tests, but I wanted to test that the new credential is created, which I can do only after the goroutine finishes. I was talking to Salmen about this and he advised me this.
Closed in favour #32 |
This is not yet finished, this PR is more of a "checkpoint".
TODO:
cc @grittygrease