Merge pull request #862 from jlhawn/ng_auth_package

Refactor token verification to support x5c header
This commit is contained in:
Olivier Gambier 2014-12-18 15:18:07 -08:00
commit e50fcc0ab9
2 changed files with 114 additions and 103 deletions

View file

@ -9,7 +9,6 @@ import (
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"os" "os"
"strconv"
"strings" "strings"
"github.com/docker/libtrust" "github.com/docker/libtrust"
@ -97,16 +96,16 @@ func (ac *authChallenge) Status() int {
// the WWW-Authenticate response challenge header. // the WWW-Authenticate response challenge header.
// See https://tools.ietf.org/html/rfc6750#section-3 // See https://tools.ietf.org/html/rfc6750#section-3
func (ac *authChallenge) challengeParams() string { func (ac *authChallenge) challengeParams() string {
str := fmt.Sprintf("Bearer realm=%s,service=%s", strconv.Quote(ac.realm), strconv.Quote(ac.service)) str := fmt.Sprintf("Bearer realm=%q,service=%q", ac.realm, ac.service)
if scope := ac.accessSet.scopeParam(); scope != "" { if scope := ac.accessSet.scopeParam(); scope != "" {
str = fmt.Sprintf("%s,scope=%s", str, strconv.Quote(scope)) str = fmt.Sprintf("%s,scope=%q", str, scope)
} }
if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken { if ac.err == ErrInvalidToken || ac.err == ErrMalformedToken {
str = fmt.Sprintf("%s,error=%s", str, strconv.Quote("invalid_token")) str = fmt.Sprintf("%s,error=%q", str, "invalid_token")
} else if ac.err == ErrInsufficientScope { } else if ac.err == ErrInsufficientScope {
str = fmt.Sprintf("%s,error=%s", str, strconv.Quote("insufficient_scope")) str = fmt.Sprintf("%s,error=%q", str, "insufficient_scope")
} }
return str return str

View file

@ -53,25 +53,11 @@ type ClaimSet struct {
// Header describes the header section of a JSON Web Token. // Header describes the header section of a JSON Web Token.
type Header struct { type Header struct {
Type string `json:"typ"` Type string `json:"typ"`
SigningAlg string `json:"alg"` SigningAlg string `json:"alg"`
KeyID string `json:"kid,omitempty"` KeyID string `json:"kid,omitempty"`
RawJWK json.RawMessage `json:"jwk"` X5c []string `json:"x5c,omitempty"`
SigningKey libtrust.PublicKey `json:"-"` RawJWK json.RawMessage `json:"jwk,omitempty"`
}
// CheckSigningKey parses the `jwk` field of a JOSE header and sets the
// SigningKey field if it is valid.
func (h *Header) CheckSigningKey() (err error) {
if len(h.RawJWK) == 0 {
// No signing key was specified.
return
}
h.SigningKey, err = libtrust.UnmarshalPublicKeyJWK([]byte(h.RawJWK))
h.RawJWK = nil // Don't need this anymore!
return
} }
// Token describes a JSON Web Token. // Token describes a JSON Web Token.
@ -135,10 +121,6 @@ func NewToken(rawToken string) (*Token, error) {
return nil, ErrMalformedToken return nil, ErrMalformedToken
} }
if err = token.Header.CheckSigningKey(); err != nil {
return nil, ErrMalformedToken
}
if err = json.Unmarshal(claimsJSON, token.Claims); err != nil { if err = json.Unmarshal(claimsJSON, token.Claims); err != nil {
return nil, ErrMalformedToken return nil, ErrMalformedToken
} }
@ -174,108 +156,86 @@ func (t *Token) Verify(verifyOpts VerifyOptions) error {
return ErrInvalidToken return ErrInvalidToken
} }
// If the token header has a SigningKey field, verify the signature // Verify that the signing key is trusted.
// using that key and its included x509 certificate chain if necessary. signingKey, err := t.VerifySigningKey(verifyOpts)
// If the Header's SigningKey field is nil, try using the KeyID field. if err != nil {
signingKey := t.Header.SigningKey log.Error(err)
return ErrInvalidToken
if signingKey == nil {
// Find the key in the given collection of trusted keys.
trustedKey, ok := verifyOpts.TrustedKeys[t.Header.KeyID]
if !ok {
log.Errorf("token signed by untrusted key with ID: %q", t.Header.KeyID)
return ErrInvalidToken
}
signingKey = trustedKey
} }
// First verify the signature of the token using the key which signed it. // Finally, verify the signature of the token using the key which signed it.
if err := signingKey.Verify(strings.NewReader(t.Raw), t.Header.SigningAlg, t.Signature); err != nil { if err := signingKey.Verify(strings.NewReader(t.Raw), t.Header.SigningAlg, t.Signature); err != nil {
log.Errorf("unable to verify token signature: %s", err) log.Errorf("unable to verify token signature: %s", err)
return ErrInvalidToken return ErrInvalidToken
} }
// Next, check if the signing key is one of the trusted keys. return nil
if _, isTrustedKey := verifyOpts.TrustedKeys[signingKey.KeyID()]; isTrustedKey {
// We're done! The token was signed by
// a trusted key and has been verified!
return nil
}
// Otherwise, we need to check the sigining keys included certificate chain.
return t.verifyCertificateChain(signingKey, verifyOpts.Roots)
} }
// verifyCertificateChain attempts to verify the token using the "x5c" field // VerifySigningKey attempts to get the key which was used to sign this token.
// of the given leafKey which was used to sign it. Returns a nil error if // The token header should contain either of these 3 fields:
// the key's certificate chain is valid and rooted an one of the given roots. // `x5c` - The x509 certificate chain for the signing key. Needs to be
func (t *Token) verifyCertificateChain(leafKey libtrust.PublicKey, roots *x509.CertPool) error { // verified.
// In this case, the token signature is valid, but the key that signed it // `jwk` - The JSON Web Key representation of the signing key.
// is not in our set of trusted keys. So, we'll need to check if the // May contain its own `x5c` field which needs to be verified.
// token's signing key included an x509 certificate chain that can be // `kid` - The unique identifier for the key. This library interprets it
// verified up to one of our trusted roots. // as a libtrust fingerprint. The key itself can be looked up in
x5cVal, ok := leafKey.GetExtendedField("x5c").([]interface{}) // the trustedKeys field of the given verify options.
if !ok || x5cVal == nil { // Each of these methods are tried in that order of preference until the
log.Error("unable to verify token signature: signed by untrusted key with no valid certificate chain") // signing key is found or an error is returned.
return ErrInvalidToken func (t *Token) VerifySigningKey(verifyOpts VerifyOptions) (signingKey libtrust.PublicKey, err error) {
// First attempt to get an x509 certificate chain from the header.
var (
x5c = t.Header.X5c
rawJWK = t.Header.RawJWK
keyID = t.Header.KeyID
)
switch {
case len(x5c) > 0:
signingKey, err = parseAndVerifyCertChain(x5c, verifyOpts.Roots)
case len(rawJWK) > 0:
signingKey, err = parseAndVerifyRawJWK(rawJWK, verifyOpts)
case len(keyID) > 0:
signingKey = verifyOpts.TrustedKeys[keyID]
if signingKey == nil {
err = fmt.Errorf("token signed by untrusted key with ID: %q", keyID)
}
default:
err = errors.New("unable to get token signing key")
} }
// Ensure each item is of the correct type. return
x5c := make([]string, len(x5cVal)) }
for i, val := range x5cVal {
certString, ok := val.(string) func parseAndVerifyCertChain(x5c []string, roots *x509.CertPool) (leafKey libtrust.PublicKey, err error) {
if !ok || len(certString) == 0 { if len(x5c) == 0 {
log.Error("unable to verify token signature: signed by untrusted key with malformed certificate chain") return nil, errors.New("empty x509 certificate chain")
return ErrInvalidToken
}
x5c[i] = certString
} }
// Ensure the first element is encoded correctly. // Ensure the first element is encoded correctly.
leafCertDer, err := base64.StdEncoding.DecodeString(x5c[0]) leafCertDer, err := base64.StdEncoding.DecodeString(x5c[0])
if err != nil { if err != nil {
log.Errorf("unable to decode signing key leaf cert: %s", err) return nil, fmt.Errorf("unable to decode leaf certificate: %s", err)
return ErrInvalidToken
} }
// And that it is a valid x509 certificate. // And that it is a valid x509 certificate.
leafCert, err := x509.ParseCertificate(leafCertDer) leafCert, err := x509.ParseCertificate(leafCertDer)
if err != nil { if err != nil {
log.Errorf("unable to parse signing key leaf cert: %s", err) return nil, fmt.Errorf("unable to parse leaf certificate: %s", err)
return ErrInvalidToken
} }
// Verify that the public key in the leaf cert *is* the signing key. // The rest of the certificate chain are intermediate certificates.
leafCryptoKey, ok := leafCert.PublicKey.(crypto.PublicKey)
if !ok {
log.Error("unable to get signing key leaf cert public key value")
return ErrInvalidToken
}
leafPubKey, err := libtrust.FromCryptoPublicKey(leafCryptoKey)
if err != nil {
log.Errorf("unable to make libtrust public key from signing key leaf cert: %s", err)
return ErrInvalidToken
}
if leafPubKey.KeyID() != leafKey.KeyID() {
log.Error("token signing key ID and leaf certificate public key ID do not match")
return ErrInvalidToken
}
// The rest of the x5c array are intermediate certificates.
intermediates := x509.NewCertPool() intermediates := x509.NewCertPool()
for i := 1; i < len(x5c); i++ { for i := 1; i < len(x5c); i++ {
intermediateCertDer, err := base64.StdEncoding.DecodeString(x5c[i]) intermediateCertDer, err := base64.StdEncoding.DecodeString(x5c[i])
if err != nil { if err != nil {
log.Errorf("unable to decode signing key intermediate cert: %s", err) return nil, fmt.Errorf("unable to decode intermediate certificate: %s", err)
return ErrInvalidToken
} }
intermediateCert, err := x509.ParseCertificate(intermediateCertDer) intermediateCert, err := x509.ParseCertificate(intermediateCertDer)
if err != nil { if err != nil {
log.Errorf("unable to parse signing key intermediate cert: %s", err) return nil, fmt.Errorf("unable to parse intermediate certificate: %s", err)
return ErrInvalidToken
} }
intermediates.AddCert(intermediateCert) intermediates.AddCert(intermediateCert)
@ -290,12 +250,64 @@ func (t *Token) verifyCertificateChain(leafKey libtrust.PublicKey, roots *x509.C
// TODO: this call returns certificate chains which we ignore for now, but // TODO: this call returns certificate chains which we ignore for now, but
// we should check them for revocations if we have the ability later. // we should check them for revocations if we have the ability later.
if _, err = leafCert.Verify(verifyOpts); err != nil { if _, err = leafCert.Verify(verifyOpts); err != nil {
log.Errorf("unable to verify signing key certificate: %s", err) return nil, fmt.Errorf("unable to verify certificate chain: %s", err)
return ErrInvalidToken
} }
// The signing key's x509 chain is valid! // Get the public key from the leaf certificate.
return nil leafCryptoKey, ok := leafCert.PublicKey.(crypto.PublicKey)
if !ok {
return nil, errors.New("unable to get leaf cert public key value")
}
leafKey, err = libtrust.FromCryptoPublicKey(leafCryptoKey)
if err != nil {
return nil, fmt.Errorf("unable to make libtrust public key from leaf certificate: %s", err)
}
return
}
func parseAndVerifyRawJWK(rawJWK json.RawMessage, verifyOpts VerifyOptions) (pubKey libtrust.PublicKey, err error) {
pubKey, err = libtrust.UnmarshalPublicKeyJWK([]byte(rawJWK))
if err != nil {
return nil, fmt.Errorf("unable to decode raw JWK value: %s", err)
}
// Check to see if the key includes a certificate chain.
x5cVal, ok := pubKey.GetExtendedField("x5c").([]interface{})
if !ok {
// The JWK should be one of the trusted root keys.
if _, trusted := verifyOpts.TrustedKeys[pubKey.KeyID()]; !trusted {
return nil, errors.New("untrusted JWK with no certificate chain")
}
// The JWK is one of the trusted keys.
return
}
// Ensure each item in the chain is of the correct type.
x5c := make([]string, len(x5cVal))
for i, val := range x5cVal {
certString, ok := val.(string)
if !ok || len(certString) == 0 {
return nil, errors.New("malformed certificate chain")
}
x5c[i] = certString
}
// Ensure that the x509 certificate chain can
// be verified up to one of our trusted roots.
leafKey, err := parseAndVerifyCertChain(x5c, verifyOpts.Roots)
if err != nil {
return nil, fmt.Errorf("could not verify JWK certificate chain: %s", err)
}
// Verify that the public key in the leaf cert *is* the signing key.
if pubKey.KeyID() != leafKey.KeyID() {
return nil, errors.New("leaf certificate public key ID does not match JWK key ID")
}
return
} }
// accessSet returns a set of actions available for the resource // accessSet returns a set of actions available for the resource