Skip to content
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

Closed
wants to merge 9 commits into from

Conversation

tsusanka
Copy link
Contributor

@tsusanka tsusanka commented Aug 8, 2017

This is not yet finished, this PR is more of a "checkpoint".

TODO:

  • saving the credentials to use them until they're valid
  • test edge cases
  • TLS 1.3 support
  • incorporate into keyless

cc @grittygrease

Copy link

@grittygrease grittygrease left a 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

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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?

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.

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

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.

import "bytes"
import (
"bytes"
)

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 {

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

@tsusanka tsusanka Aug 9, 2017

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

"errors"
"time"
)

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.

Copy link
Contributor Author

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?

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"

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

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.

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 understand. The Certificate struct will then contain marshalled DC and the associated PrivateKey. Both received from the delcred sub package.

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.

@@ -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.

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.

Copy link
Contributor Author

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?

@grittygrease
Copy link

Config.Clone and other ancillary functions are also missing the new data.

@grittygrease
Copy link

Also note that the tests are failing.

@tsusanka
Copy link
Contributor Author

tsusanka commented Aug 9, 2017

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.

@tsusanka tsusanka force-pushed the tsusanka/delegated-cred branch 3 times, most recently from 5e023e6 to acc9ee8 Compare August 15, 2017 12:54
@tsusanka tsusanka force-pushed the tsusanka/delegated-cred branch from acc9ee8 to 3df9bf9 Compare August 15, 2017 13:20
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
Copy link
Contributor Author

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 (?)

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.

@tsusanka
Copy link
Contributor Author

I think this is ready for another round of code review.

  • All code is in the same tls package, however, the logic is clearly separated using the GetCertificate function as discussed
  • I've squashed all commits into just a few, it was a mess
  • Tests are fixed
  • I've implemented the support for PKCS1, it's trivial to remove it in case we decide to

@@ -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

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.

}

func NewDelCredGetCertificateFunction(cert *Certificate) GetCertificate {

Choose a reason for hiding this comment

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

Unnecessary space?

PublicKey interface{}
}

func NewDelCredGetCertificateFunction(cert *Certificate) GetCertificate {

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.

Copy link
Contributor Author

@tsusanka tsusanka Aug 15, 2017

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?

func NewDelCredGetCertificateFunction(cert *Certificate) GetCertificate {

return func(clientHelloInfo *ClientHelloInfo) (*Certificate, error) {

Choose a reason for hiding this comment

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

Space

}

func selectVersion(versions []uint16) uint16 {

Choose a reason for hiding this comment

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

Newline

if scheme == ECDSAWithP256AndSHA256 {
privateKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
credential.PublicKey = privateKey.(crypto.Signer).Public()

Choose a reason for hiding this comment

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

newline


copy(cred[7:7+publicKeyLength], publicKeyBytes)

cred[publicKeyLength+7] = uint8(scheme >> 8) // hash

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

finishedHash finishedHash
masterSecret []byte
session *ClientSessionState
delCredPublicKey crypto.PublicKey

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.

@tsusanka tsusanka force-pushed the tsusanka/delegated-cred branch 2 times, most recently from c853649 to 8bbddcd Compare August 22, 2017 10:55
)

// After only renewThreshold of credential's validTime is left a new one is created
const renewThreshold = 0.2
Copy link
Contributor Author

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

@tsusanka tsusanka force-pushed the tsusanka/delegated-cred branch from 8bbddcd to 94d5a8b Compare August 22, 2017 12:28
Copy link

@grittygrease grittygrease left a 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)

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?

Copy link
Contributor Author

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()),
}

renewChan chan struct{}
}

var cachedCredentials map[SignatureScheme]*cachedCredential

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.

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 point! So I'm just including it as a not exported CredentialsConfig variable.

go func() {
cachedCredentials[config.scheme], err = newCredential(config)
if config.renewChan != nil {
config.renewChan <- struct{}{}

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?

Copy link
Contributor Author

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.

@tsusanka tsusanka closed this Aug 25, 2017
@tsusanka
Copy link
Contributor Author

Closed in favour #32

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.

2 participants