db1bf93098
Adds a constant leeway (60 seconds) to the nbf and exp claim check to account for clock skew between the registry servers and the authentication server that generated the JWT. The leeway of 60 seconds is a bit arbitrary but based on the RFC recommendation and hub.docker.com logs/metrics where we don't see drifts of more than a second on our servers running ntpd. I didn't attempt to make the leeway configurable as it would add extra complexity to the PR and I am not sure how Distribution prefer to handle runtime flags like that. Also, I am simplifying the exp and nbf check for readability as the previous `NOT (A AND B)` with cmp operators was not very friendly. Ref: https://tools.ietf.org/html/rfc7519#section-4.1.5 Signed-off-by: Marcus Martins <marcus@docker.com>
354 lines
10 KiB
Go
354 lines
10 KiB
Go
package token
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
log "github.com/Sirupsen/logrus"
|
|
"github.com/docker/libtrust"
|
|
|
|
"github.com/docker/distribution/registry/auth"
|
|
)
|
|
|
|
const (
|
|
// TokenSeparator is the value which separates the header, claims, and
|
|
// signature in the compact serialization of a JSON Web Token.
|
|
TokenSeparator = "."
|
|
// Leeway is the Duration that will be added to NBF and EXP claim
|
|
// checks to account for clock skew as per https://tools.ietf.org/html/rfc7519#section-4.1.5
|
|
Leeway = 60 * time.Second
|
|
)
|
|
|
|
// Errors used by token parsing and verification.
|
|
var (
|
|
ErrMalformedToken = errors.New("malformed token")
|
|
ErrInvalidToken = errors.New("invalid token")
|
|
)
|
|
|
|
// ResourceActions stores allowed actions on a named and typed resource.
|
|
type ResourceActions struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Actions []string `json:"actions"`
|
|
}
|
|
|
|
// ClaimSet describes the main section of a JSON Web Token.
|
|
type ClaimSet struct {
|
|
// Public claims
|
|
Issuer string `json:"iss"`
|
|
Subject string `json:"sub"`
|
|
Audience string `json:"aud"`
|
|
Expiration int64 `json:"exp"`
|
|
NotBefore int64 `json:"nbf"`
|
|
IssuedAt int64 `json:"iat"`
|
|
JWTID string `json:"jti"`
|
|
|
|
// Private claims
|
|
Access []*ResourceActions `json:"access"`
|
|
}
|
|
|
|
// Header describes the header section of a JSON Web Token.
|
|
type Header struct {
|
|
Type string `json:"typ"`
|
|
SigningAlg string `json:"alg"`
|
|
KeyID string `json:"kid,omitempty"`
|
|
X5c []string `json:"x5c,omitempty"`
|
|
RawJWK *json.RawMessage `json:"jwk,omitempty"`
|
|
}
|
|
|
|
// Token describes a JSON Web Token.
|
|
type Token struct {
|
|
Raw string
|
|
Header *Header
|
|
Claims *ClaimSet
|
|
Signature []byte
|
|
}
|
|
|
|
// VerifyOptions is used to specify
|
|
// options when verifying a JSON Web Token.
|
|
type VerifyOptions struct {
|
|
TrustedIssuers []string
|
|
AcceptedAudiences []string
|
|
Roots *x509.CertPool
|
|
TrustedKeys map[string]libtrust.PublicKey
|
|
}
|
|
|
|
// NewToken parses the given raw token string
|
|
// and constructs an unverified JSON Web Token.
|
|
func NewToken(rawToken string) (*Token, error) {
|
|
parts := strings.Split(rawToken, TokenSeparator)
|
|
if len(parts) != 3 {
|
|
return nil, ErrMalformedToken
|
|
}
|
|
|
|
var (
|
|
rawHeader, rawClaims = parts[0], parts[1]
|
|
headerJSON, claimsJSON []byte
|
|
err error
|
|
)
|
|
|
|
defer func() {
|
|
if err != nil {
|
|
log.Errorf("error while unmarshalling raw token: %s", err)
|
|
}
|
|
}()
|
|
|
|
if headerJSON, err = joseBase64UrlDecode(rawHeader); err != nil {
|
|
err = fmt.Errorf("unable to decode header: %s", err)
|
|
return nil, ErrMalformedToken
|
|
}
|
|
|
|
if claimsJSON, err = joseBase64UrlDecode(rawClaims); err != nil {
|
|
err = fmt.Errorf("unable to decode claims: %s", err)
|
|
return nil, ErrMalformedToken
|
|
}
|
|
|
|
token := new(Token)
|
|
token.Header = new(Header)
|
|
token.Claims = new(ClaimSet)
|
|
|
|
token.Raw = strings.Join(parts[:2], TokenSeparator)
|
|
if token.Signature, err = joseBase64UrlDecode(parts[2]); err != nil {
|
|
err = fmt.Errorf("unable to decode signature: %s", err)
|
|
return nil, ErrMalformedToken
|
|
}
|
|
|
|
if err = json.Unmarshal(headerJSON, token.Header); err != nil {
|
|
return nil, ErrMalformedToken
|
|
}
|
|
|
|
if err = json.Unmarshal(claimsJSON, token.Claims); err != nil {
|
|
return nil, ErrMalformedToken
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// Verify attempts to verify this token using the given options.
|
|
// Returns a nil error if the token is valid.
|
|
func (t *Token) Verify(verifyOpts VerifyOptions) error {
|
|
// Verify that the Issuer claim is a trusted authority.
|
|
if !contains(verifyOpts.TrustedIssuers, t.Claims.Issuer) {
|
|
log.Errorf("token from untrusted issuer: %q", t.Claims.Issuer)
|
|
return ErrInvalidToken
|
|
}
|
|
|
|
// Verify that the Audience claim is allowed.
|
|
if !contains(verifyOpts.AcceptedAudiences, t.Claims.Audience) {
|
|
log.Errorf("token intended for another audience: %q", t.Claims.Audience)
|
|
return ErrInvalidToken
|
|
}
|
|
|
|
// Verify that the token is currently usable and not expired.
|
|
currentTime := time.Now()
|
|
|
|
ExpWithLeeway := time.Unix(t.Claims.Expiration, 0).Add(Leeway)
|
|
if currentTime.After(ExpWithLeeway) {
|
|
log.Errorf("token not to be used after %s - currently %s", ExpWithLeeway, currentTime)
|
|
return ErrInvalidToken
|
|
}
|
|
|
|
NotBeforeWithLeeway := time.Unix(t.Claims.NotBefore, 0).Add(-Leeway)
|
|
if currentTime.Before(NotBeforeWithLeeway) {
|
|
log.Errorf("token not to be used before %s - currently %s", NotBeforeWithLeeway, currentTime)
|
|
return ErrInvalidToken
|
|
}
|
|
|
|
// Verify the token signature.
|
|
if len(t.Signature) == 0 {
|
|
log.Error("token has no signature")
|
|
return ErrInvalidToken
|
|
}
|
|
|
|
// Verify that the signing key is trusted.
|
|
signingKey, err := t.VerifySigningKey(verifyOpts)
|
|
if err != nil {
|
|
log.Error(err)
|
|
return ErrInvalidToken
|
|
}
|
|
|
|
// 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 {
|
|
log.Errorf("unable to verify token signature: %s", err)
|
|
return ErrInvalidToken
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// VerifySigningKey attempts to get the key which was used to sign this token.
|
|
// The token header should contain either of these 3 fields:
|
|
// `x5c` - The x509 certificate chain for the signing key. Needs to be
|
|
// verified.
|
|
// `jwk` - The JSON Web Key representation of the signing key.
|
|
// May contain its own `x5c` field which needs to be verified.
|
|
// `kid` - The unique identifier for the key. This library interprets it
|
|
// as a libtrust fingerprint. The key itself can be looked up in
|
|
// the trustedKeys field of the given verify options.
|
|
// Each of these methods are tried in that order of preference until the
|
|
// signing key is found or an error is returned.
|
|
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 rawJWK != nil:
|
|
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")
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func parseAndVerifyCertChain(x5c []string, roots *x509.CertPool) (leafKey libtrust.PublicKey, err error) {
|
|
if len(x5c) == 0 {
|
|
return nil, errors.New("empty x509 certificate chain")
|
|
}
|
|
|
|
// Ensure the first element is encoded correctly.
|
|
leafCertDer, err := base64.StdEncoding.DecodeString(x5c[0])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode leaf certificate: %s", err)
|
|
}
|
|
|
|
// And that it is a valid x509 certificate.
|
|
leafCert, err := x509.ParseCertificate(leafCertDer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse leaf certificate: %s", err)
|
|
}
|
|
|
|
// The rest of the certificate chain are intermediate certificates.
|
|
intermediates := x509.NewCertPool()
|
|
for i := 1; i < len(x5c); i++ {
|
|
intermediateCertDer, err := base64.StdEncoding.DecodeString(x5c[i])
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode intermediate certificate: %s", err)
|
|
}
|
|
|
|
intermediateCert, err := x509.ParseCertificate(intermediateCertDer)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse intermediate certificate: %s", err)
|
|
}
|
|
|
|
intermediates.AddCert(intermediateCert)
|
|
}
|
|
|
|
verifyOpts := x509.VerifyOptions{
|
|
Intermediates: intermediates,
|
|
Roots: roots,
|
|
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
|
|
}
|
|
|
|
// TODO: this call returns certificate chains which we ignore for now, but
|
|
// we should check them for revocations if we have the ability later.
|
|
if _, err = leafCert.Verify(verifyOpts); err != nil {
|
|
return nil, fmt.Errorf("unable to verify certificate chain: %s", err)
|
|
}
|
|
|
|
// Get the public key from the leaf certificate.
|
|
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
|
|
// actions listed in the `access` section of this token.
|
|
func (t *Token) accessSet() accessSet {
|
|
if t.Claims == nil {
|
|
return nil
|
|
}
|
|
|
|
accessSet := make(accessSet, len(t.Claims.Access))
|
|
|
|
for _, resourceActions := range t.Claims.Access {
|
|
resource := auth.Resource{
|
|
Type: resourceActions.Type,
|
|
Name: resourceActions.Name,
|
|
}
|
|
|
|
set, exists := accessSet[resource]
|
|
if !exists {
|
|
set = newActionSet()
|
|
accessSet[resource] = set
|
|
}
|
|
|
|
for _, action := range resourceActions.Actions {
|
|
set.add(action)
|
|
}
|
|
}
|
|
|
|
return accessSet
|
|
}
|
|
|
|
func (t *Token) compactRaw() string {
|
|
return fmt.Sprintf("%s.%s", t.Raw, joseBase64UrlEncode(t.Signature))
|
|
}
|