174a68a0fb
Drops support for delegated credentials with TLS 1.2 and adds the protocol version and signature algorithm to the credential structure.
389 lines
12 KiB
Go
389 lines
12 KiB
Go
// Copyright 2018 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.
|
|
|
|
package tls
|
|
|
|
// Delegated credentials for TLS
|
|
// (https://tools.ietf.org/html/draft-ietf-tls-subcerts-02) is an IETF Internet
|
|
// draft and proposed TLS extension. This allows a backend server to delegate
|
|
// TLS termination to a trusted frontend. If the client supports this extension,
|
|
// then the frontend may use a "delegated credential" as the signing key in the
|
|
// handshake. A delegated credential is a short lived key pair delegated to the
|
|
// server by an entity trusted by the client. Once issued, credentials can't be
|
|
// revoked; in order to mitigate risk in case the frontend is compromised, the
|
|
// credential is only valid for a short time (days, hours, or even minutes).
|
|
//
|
|
// This implements draft 02. This draft doesn't specify an object identifier for
|
|
// the X.509 extension; we use one assigned by Cloudflare. In addition, IANA has
|
|
// not assigned an extension ID for this extension; we picked up one that's not
|
|
// yet taken.
|
|
//
|
|
// TODO(cjpatton) Only ECDSA is supported with delegated credentials for now;
|
|
// we'd like to suppoort for EcDSA signatures once these have better support
|
|
// upstream.
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/x509"
|
|
"encoding/asn1"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
dcMaxTTLSeconds = 60 * 60 * 24 * 7 // 7 days
|
|
dcMaxTTL = time.Duration(dcMaxTTLSeconds * time.Second)
|
|
dcMaxPublicKeyLen = 1 << 16 // Bytes
|
|
dcMaxSignatureLen = 1 << 16 // Bytes
|
|
)
|
|
|
|
var errNoDelegationUsage = errors.New("certificate not authorized for delegation")
|
|
|
|
// delegationUsageId is the DelegationUsage X.509 extension OID
|
|
//
|
|
// NOTE(cjpatton) This OID is a child of Cloudflare's IANA-assigned OID.
|
|
var delegationUsageId = asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 44363, 44}
|
|
|
|
// canDelegate returns true if a certificate can be used for delegated
|
|
// credentials.
|
|
func canDelegate(cert *x509.Certificate) bool {
|
|
// Check that the digitalSignature key usage is set.
|
|
if (cert.KeyUsage & x509.KeyUsageDigitalSignature) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Check that the certificate has the DelegationUsage extension and that
|
|
// it's non-critical (per the spec).
|
|
for _, extension := range cert.Extensions {
|
|
if extension.Id.Equal(delegationUsageId) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// credential stores the public components of a credential.
|
|
type credential struct {
|
|
// The serialized form of the credential.
|
|
raw []byte
|
|
|
|
// The amount of time for which the credential is valid. Specifically, the
|
|
// the credential expires `ValidTime` seconds after the `notBefore` of the
|
|
// delegation certificate. The delegator shall not issue delegated
|
|
// credentials that are valid for more than 7 days from the current time.
|
|
//
|
|
// When this data structure is serialized, this value is converted to a
|
|
// uint32 representing the duration in seconds.
|
|
validTime time.Duration
|
|
|
|
// The signature scheme associated with the delegated credential public key.
|
|
expectedCertVerifyAlgorithm SignatureScheme
|
|
|
|
// The version of TLS in which the credential will be used.
|
|
expectedVersion uint16
|
|
|
|
// The credential public key.
|
|
publicKey crypto.PublicKey
|
|
}
|
|
|
|
// isExpired returns true if the credential has expired. The end of the validity
|
|
// interval is defined as the delegator certificate's notBefore field (`start`)
|
|
// plus ValidTime seconds. This function simply checks that the current time
|
|
// (`now`) is before the end of the valdity interval.
|
|
func (cred *credential) isExpired(start, now time.Time) bool {
|
|
end := start.Add(cred.validTime)
|
|
return !now.Before(end)
|
|
}
|
|
|
|
// invalidTTL returns true if the credential's validity period is longer than the
|
|
// maximum permitted. This is defined by the certificate's notBefore field
|
|
// (`start`) plus the ValidTime, minus the current time (`now`).
|
|
func (cred *credential) invalidTTL(start, now time.Time) bool {
|
|
return cred.validTime > (now.Sub(start) + dcMaxTTL).Round(time.Second)
|
|
}
|
|
|
|
// marshalSubjectPublicKeyInfo returns a DER encoded SubjectPublicKeyInfo structure
|
|
// (as defined in the X.509 standard) for the credential.
|
|
func (cred *credential) marshalSubjectPublicKeyInfo() ([]byte, error) {
|
|
switch cred.expectedCertVerifyAlgorithm {
|
|
case ECDSAWithP256AndSHA256,
|
|
ECDSAWithP384AndSHA384,
|
|
ECDSAWithP521AndSHA512:
|
|
serializedPublicKey, err := x509.MarshalPKIXPublicKey(cred.publicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return serializedPublicKey, nil
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unsupported signature scheme: 0x%04x", cred.expectedCertVerifyAlgorithm)
|
|
}
|
|
}
|
|
|
|
// marshal encodes a credential in the wire format specified in
|
|
// https://tools.ietf.org/html/draft-ietf-tls-subcerts-02.
|
|
func (cred *credential) marshal() ([]byte, error) {
|
|
// The number of bytes comprising the DC parameters, which includes the
|
|
// validity time (4 bytes), the signature scheme of the public key (2 bytes), and
|
|
// the protocol version (2 bytes).
|
|
paramsLen := 8
|
|
|
|
// The first 4 bytes are the valid_time, scheme, and version fields.
|
|
serialized := make([]byte, paramsLen+2)
|
|
binary.BigEndian.PutUint32(serialized, uint32(cred.validTime/time.Second))
|
|
binary.BigEndian.PutUint16(serialized[4:], uint16(cred.expectedCertVerifyAlgorithm))
|
|
binary.BigEndian.PutUint16(serialized[6:], cred.expectedVersion)
|
|
|
|
// Encode the public key and assert that the encoding is no longer than 2^16
|
|
// bytes (per the spec).
|
|
serializedPublicKey, err := cred.marshalSubjectPublicKeyInfo()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(serializedPublicKey) > dcMaxPublicKeyLen {
|
|
return nil, errors.New("public key is too long")
|
|
}
|
|
|
|
// The next 2 bytes are the length of the public key field.
|
|
binary.BigEndian.PutUint16(serialized[paramsLen:], uint16(len(serializedPublicKey)))
|
|
|
|
// The remaining bytes are the public key itself.
|
|
serialized = append(serialized, serializedPublicKey...)
|
|
cred.raw = serialized
|
|
return serialized, nil
|
|
}
|
|
|
|
// unmarshalCredential decodes a credential and returns it.
|
|
func unmarshalCredential(serialized []byte) (*credential, error) {
|
|
// The number of bytes comprising the DC parameters.
|
|
paramsLen := 8
|
|
|
|
if len(serialized) < paramsLen+2 {
|
|
return nil, errors.New("credential is too short")
|
|
}
|
|
|
|
// Parse the valid_time, scheme, and version fields.
|
|
validTime := time.Duration(binary.BigEndian.Uint32(serialized)) * time.Second
|
|
scheme := SignatureScheme(binary.BigEndian.Uint16(serialized[4:]))
|
|
version := binary.BigEndian.Uint16(serialized[6:])
|
|
|
|
// Parse the SubjectPublicKeyInfo.
|
|
pk, err := x509.ParsePKIXPublicKey(serialized[paramsLen+2:])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, ok := pk.(*ecdsa.PublicKey); !ok {
|
|
return nil, fmt.Errorf("unsupported delegation key type: %T", pk)
|
|
}
|
|
|
|
return &credential{
|
|
raw: serialized,
|
|
validTime: validTime,
|
|
expectedCertVerifyAlgorithm: scheme,
|
|
expectedVersion: version,
|
|
publicKey: pk,
|
|
}, nil
|
|
}
|
|
|
|
// getCredentialLen returns the number of bytes comprising the serialized
|
|
// credential that starts at the beginning of the input slice. It returns an
|
|
// error if the input is too short to contain a credential.
|
|
func getCredentialLen(serialized []byte) (int, error) {
|
|
paramsLen := 8
|
|
if len(serialized) < paramsLen+2 {
|
|
return 0, errors.New("credential is too short")
|
|
}
|
|
// First several bytes are the valid_time, scheme, and version fields.
|
|
serialized = serialized[paramsLen:]
|
|
|
|
// The next 2 bytes are the length of the serialized public key.
|
|
serializedPublicKeyLen := int(binary.BigEndian.Uint16(serialized))
|
|
serialized = serialized[2:]
|
|
|
|
if len(serialized) < serializedPublicKeyLen {
|
|
return 0, errors.New("public key of credential is too short")
|
|
}
|
|
|
|
return paramsLen + 2 + serializedPublicKeyLen, nil
|
|
}
|
|
|
|
// delegatedCredential stores a credential and its delegation.
|
|
type delegatedCredential struct {
|
|
raw []byte
|
|
|
|
// The credential, which contains a public and its validity time.
|
|
cred *credential
|
|
|
|
// The signature scheme used to sign the credential.
|
|
algorithm SignatureScheme
|
|
|
|
// The credential's delegation.
|
|
signature []byte
|
|
}
|
|
|
|
// ensureCertificateHasLeaf parses the leaf certificate if needed.
|
|
func ensureCertificateHasLeaf(cert *Certificate) error {
|
|
var err error
|
|
if cert.Leaf == nil {
|
|
if len(cert.Certificate[0]) == 0 {
|
|
return errors.New("missing leaf certificate")
|
|
}
|
|
cert.Leaf, err = x509.ParseCertificate(cert.Certificate[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validate checks that that the signature is valid, that the credential hasn't
|
|
// expired, and that the TTL is valid. It also checks that certificate can be
|
|
// used for delegation.
|
|
func (dc *delegatedCredential) validate(cert *x509.Certificate, now time.Time) (bool, error) {
|
|
// Check that the cert can delegate.
|
|
if !canDelegate(cert) {
|
|
return false, errNoDelegationUsage
|
|
}
|
|
|
|
if dc.cred.isExpired(cert.NotBefore, now) {
|
|
return false, errors.New("credential has expired")
|
|
}
|
|
|
|
if dc.cred.invalidTTL(cert.NotBefore, now) {
|
|
return false, errors.New("credential TTL is invalid")
|
|
}
|
|
|
|
// Prepare the credential for verification.
|
|
rawCred, err := dc.cred.marshal()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
hash := getHash(dc.algorithm)
|
|
in := prepareDelegation(hash, rawCred, cert.Raw, dc.algorithm)
|
|
|
|
// TODO(any) This code overlaps significantly with verifyHandshakeSignature()
|
|
// in ../auth.go. This should be refactored.
|
|
switch dc.algorithm {
|
|
case ECDSAWithP256AndSHA256,
|
|
ECDSAWithP384AndSHA384,
|
|
ECDSAWithP521AndSHA512:
|
|
pk, ok := cert.PublicKey.(*ecdsa.PublicKey)
|
|
if !ok {
|
|
return false, errors.New("expected ECDSA public key")
|
|
}
|
|
sig := new(ecdsaSignature)
|
|
if _, err = asn1.Unmarshal(dc.signature, sig); err != nil {
|
|
return false, err
|
|
}
|
|
return ecdsa.Verify(pk, in, sig.R, sig.S), nil
|
|
|
|
default:
|
|
return false, fmt.Errorf(
|
|
"unsupported signature scheme: 0x%04x", dc.algorithm)
|
|
}
|
|
}
|
|
|
|
// unmarshalDelegatedCredential decodes a DelegatedCredential structure.
|
|
func unmarshalDelegatedCredential(serialized []byte) (*delegatedCredential, error) {
|
|
// Get the length of the serialized credential that begins at the start of
|
|
// the input slice.
|
|
serializedCredentialLen, err := getCredentialLen(serialized)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse the credential.
|
|
cred, err := unmarshalCredential(serialized[:serializedCredentialLen])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse the signature scheme.
|
|
serialized = serialized[serializedCredentialLen:]
|
|
if len(serialized) < 4 {
|
|
return nil, errors.New("delegated credential is too short")
|
|
}
|
|
scheme := SignatureScheme(binary.BigEndian.Uint16(serialized))
|
|
|
|
// Parse the signature length.
|
|
serialized = serialized[2:]
|
|
serializedSignatureLen := binary.BigEndian.Uint16(serialized)
|
|
|
|
// Prase the signature.
|
|
serialized = serialized[2:]
|
|
if len(serialized) < int(serializedSignatureLen) {
|
|
return nil, errors.New("signature of delegated credential is too short")
|
|
}
|
|
sig := serialized[:serializedSignatureLen]
|
|
|
|
return &delegatedCredential{
|
|
raw: serialized,
|
|
cred: cred,
|
|
algorithm: scheme,
|
|
signature: sig,
|
|
}, nil
|
|
}
|
|
|
|
// getCurve maps the SignatureScheme to its corresponding elliptic.Curve.
|
|
func getCurve(scheme SignatureScheme) elliptic.Curve {
|
|
switch scheme {
|
|
case ECDSAWithP256AndSHA256:
|
|
return elliptic.P256()
|
|
case ECDSAWithP384AndSHA384:
|
|
return elliptic.P384()
|
|
case ECDSAWithP521AndSHA512:
|
|
return elliptic.P521()
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// getHash maps the SignatureScheme to its corresponding hash function.
|
|
//
|
|
// TODO(any) This function overlaps with hashForSignatureScheme in 13.go.
|
|
func getHash(scheme SignatureScheme) crypto.Hash {
|
|
switch scheme {
|
|
case ECDSAWithP256AndSHA256:
|
|
return crypto.SHA256
|
|
case ECDSAWithP384AndSHA384:
|
|
return crypto.SHA384
|
|
case ECDSAWithP521AndSHA512:
|
|
return crypto.SHA512
|
|
default:
|
|
return 0 // Unknown hash function
|
|
}
|
|
}
|
|
|
|
// prepareDelegation returns a hash of the message that the delegator is to
|
|
// sign. The inputs are the credential (`cred`), the DER-encoded delegator
|
|
// certificate (`delegatorCert`) and the signature scheme of the delegator
|
|
// (`delegatorAlgorithm`).
|
|
func prepareDelegation(hash crypto.Hash, cred, delegatorCert []byte, delegatorAlgorithm SignatureScheme) []byte {
|
|
h := hash.New()
|
|
|
|
// The header.
|
|
h.Write(bytes.Repeat([]byte{0x20}, 64))
|
|
h.Write([]byte("TLS, server delegated credentials"))
|
|
h.Write([]byte{0x00})
|
|
|
|
// The delegation certificate.
|
|
h.Write(delegatorCert)
|
|
|
|
// The credential.
|
|
h.Write(cred)
|
|
|
|
// The delegator signature scheme.
|
|
var serializedScheme [2]byte
|
|
binary.BigEndian.PutUint16(serializedScheme[:], uint16(delegatorAlgorithm))
|
|
h.Write(serializedScheme[:])
|
|
|
|
return h.Sum(nil)
|
|
}
|