| package token |
| |
| import ( |
| "crypto" |
| "crypto/x509" |
| "encoding/base64" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "strings" |
| "time" |
| |
| "github.com/docker/libtrust" |
| log "github.com/sirupsen/logrus" |
| |
| "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"` |
| Class string `json:"class,omitempty"` |
| 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.Infof("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.Infof("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.Infof("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.Infof("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.Infof("token not to be used before %s - currently %s", NotBeforeWithLeeway, currentTime) |
| return ErrInvalidToken |
| } |
| |
| // Verify the token signature. |
| if len(t.Signature) == 0 { |
| log.Info("token has no signature") |
| return ErrInvalidToken |
| } |
| |
| // Verify that the signing key is trusted. |
| signingKey, err := t.VerifySigningKey(verifyOpts) |
| if err != nil { |
| log.Info(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.Infof("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) resources() []auth.Resource { |
| if t.Claims == nil { |
| return nil |
| } |
| |
| resourceSet := map[auth.Resource]struct{}{} |
| for _, resourceActions := range t.Claims.Access { |
| resource := auth.Resource{ |
| Type: resourceActions.Type, |
| Class: resourceActions.Class, |
| Name: resourceActions.Name, |
| } |
| resourceSet[resource] = struct{}{} |
| } |
| |
| resources := make([]auth.Resource, 0, len(resourceSet)) |
| for resource := range resourceSet { |
| resources = append(resources, resource) |
| } |
| |
| return resources |
| } |
| |
| func (t *Token) compactRaw() string { |
| return fmt.Sprintf("%s.%s", t.Raw, joseBase64UrlEncode(t.Signature)) |
| } |