...

Source file src/github.com/docker/distribution/contrib/token-server/token.go

Documentation: github.com/docker/distribution/contrib/token-server

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"crypto"
     6  	"crypto/rand"
     7  	"encoding/base64"
     8  	"encoding/json"
     9  	"fmt"
    10  	"io"
    11  	"regexp"
    12  	"strings"
    13  	"time"
    14  
    15  	dcontext "github.com/docker/distribution/context"
    16  	"github.com/docker/distribution/registry/auth"
    17  	"github.com/docker/distribution/registry/auth/token"
    18  	"github.com/docker/libtrust"
    19  )
    20  
    21  // ResolveScopeSpecifiers converts a list of scope specifiers from a token
    22  // request's `scope` query parameters into a list of standard access objects.
    23  func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Access {
    24  	requestedAccessSet := make(map[auth.Access]struct{}, 2*len(scopeSpecs))
    25  
    26  	for _, scopeSpecifier := range scopeSpecs {
    27  		// There should be 3 parts, separated by a `:` character.
    28  		parts := strings.SplitN(scopeSpecifier, ":", 3)
    29  
    30  		if len(parts) != 3 {
    31  			dcontext.GetLogger(ctx).Infof("ignoring unsupported scope format %s", scopeSpecifier)
    32  			continue
    33  		}
    34  
    35  		resourceType, resourceName, actions := parts[0], parts[1], parts[2]
    36  
    37  		resourceType, resourceClass := splitResourceClass(resourceType)
    38  		if resourceType == "" {
    39  			continue
    40  		}
    41  
    42  		// Actions should be a comma-separated list of actions.
    43  		for _, action := range strings.Split(actions, ",") {
    44  			requestedAccess := auth.Access{
    45  				Resource: auth.Resource{
    46  					Type:  resourceType,
    47  					Class: resourceClass,
    48  					Name:  resourceName,
    49  				},
    50  				Action: action,
    51  			}
    52  
    53  			// Add this access to the requested access set.
    54  			requestedAccessSet[requestedAccess] = struct{}{}
    55  		}
    56  	}
    57  
    58  	requestedAccessList := make([]auth.Access, 0, len(requestedAccessSet))
    59  	for requestedAccess := range requestedAccessSet {
    60  		requestedAccessList = append(requestedAccessList, requestedAccess)
    61  	}
    62  
    63  	return requestedAccessList
    64  }
    65  
    66  var typeRegexp = regexp.MustCompile(`^([a-z0-9]+)(\([a-z0-9]+\))?$`)
    67  
    68  func splitResourceClass(t string) (string, string) {
    69  	matches := typeRegexp.FindStringSubmatch(t)
    70  	if len(matches) < 2 {
    71  		return "", ""
    72  	}
    73  	if len(matches) == 2 || len(matches[2]) < 2 {
    74  		return matches[1], ""
    75  	}
    76  	return matches[1], matches[2][1 : len(matches[2])-1]
    77  }
    78  
    79  // ResolveScopeList converts a scope list from a token request's
    80  // `scope` parameter into a list of standard access objects.
    81  func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access {
    82  	scopes := strings.Split(scopeList, " ")
    83  	return ResolveScopeSpecifiers(ctx, scopes)
    84  }
    85  
    86  func scopeString(a auth.Access) string {
    87  	if a.Class != "" {
    88  		return fmt.Sprintf("%s(%s):%s:%s", a.Type, a.Class, a.Name, a.Action)
    89  	}
    90  	return fmt.Sprintf("%s:%s:%s", a.Type, a.Name, a.Action)
    91  }
    92  
    93  // ToScopeList converts a list of access to a
    94  // scope list string
    95  func ToScopeList(access []auth.Access) string {
    96  	var s []string
    97  	for _, a := range access {
    98  		s = append(s, scopeString(a))
    99  	}
   100  	return strings.Join(s, ",")
   101  }
   102  
   103  // TokenIssuer represents an issuer capable of generating JWT tokens
   104  type TokenIssuer struct {
   105  	Issuer     string
   106  	SigningKey libtrust.PrivateKey
   107  	Expiration time.Duration
   108  }
   109  
   110  // CreateJWT creates and signs a JSON Web Token for the given subject and
   111  // audience with the granted access.
   112  func (issuer *TokenIssuer) CreateJWT(subject string, audience string, grantedAccessList []auth.Access) (string, error) {
   113  	// Make a set of access entries to put in the token's claimset.
   114  	resourceActionSets := make(map[auth.Resource]map[string]struct{}, len(grantedAccessList))
   115  	for _, access := range grantedAccessList {
   116  		actionSet, exists := resourceActionSets[access.Resource]
   117  		if !exists {
   118  			actionSet = map[string]struct{}{}
   119  			resourceActionSets[access.Resource] = actionSet
   120  		}
   121  		actionSet[access.Action] = struct{}{}
   122  	}
   123  
   124  	accessEntries := make([]*token.ResourceActions, 0, len(resourceActionSets))
   125  	for resource, actionSet := range resourceActionSets {
   126  		actions := make([]string, 0, len(actionSet))
   127  		for action := range actionSet {
   128  			actions = append(actions, action)
   129  		}
   130  
   131  		accessEntries = append(accessEntries, &token.ResourceActions{
   132  			Type:    resource.Type,
   133  			Class:   resource.Class,
   134  			Name:    resource.Name,
   135  			Actions: actions,
   136  		})
   137  	}
   138  
   139  	randomBytes := make([]byte, 15)
   140  	_, err := io.ReadFull(rand.Reader, randomBytes)
   141  	if err != nil {
   142  		return "", err
   143  	}
   144  	randomID := base64.URLEncoding.EncodeToString(randomBytes)
   145  
   146  	now := time.Now()
   147  
   148  	signingHash := crypto.SHA256
   149  	var alg string
   150  	switch issuer.SigningKey.KeyType() {
   151  	case "RSA":
   152  		alg = "RS256"
   153  	case "EC":
   154  		alg = "ES256"
   155  	default:
   156  		panic(fmt.Errorf("unsupported signing key type %q", issuer.SigningKey.KeyType()))
   157  	}
   158  
   159  	joseHeader := token.Header{
   160  		Type:       "JWT",
   161  		SigningAlg: alg,
   162  	}
   163  
   164  	if x5c := issuer.SigningKey.GetExtendedField("x5c"); x5c != nil {
   165  		joseHeader.X5c = x5c.([]string)
   166  	} else {
   167  		var jwkMessage json.RawMessage
   168  		jwkMessage, err = issuer.SigningKey.PublicKey().MarshalJSON()
   169  		if err != nil {
   170  			return "", err
   171  		}
   172  		joseHeader.RawJWK = &jwkMessage
   173  	}
   174  
   175  	exp := issuer.Expiration
   176  	if exp == 0 {
   177  		exp = 5 * time.Minute
   178  	}
   179  
   180  	claimSet := token.ClaimSet{
   181  		Issuer:     issuer.Issuer,
   182  		Subject:    subject,
   183  		Audience:   audience,
   184  		Expiration: now.Add(exp).Unix(),
   185  		NotBefore:  now.Unix(),
   186  		IssuedAt:   now.Unix(),
   187  		JWTID:      randomID,
   188  
   189  		Access: accessEntries,
   190  	}
   191  
   192  	var (
   193  		joseHeaderBytes []byte
   194  		claimSetBytes   []byte
   195  	)
   196  
   197  	if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil {
   198  		return "", fmt.Errorf("unable to encode jose header: %s", err)
   199  	}
   200  	if claimSetBytes, err = json.Marshal(claimSet); err != nil {
   201  		return "", fmt.Errorf("unable to encode claim set: %s", err)
   202  	}
   203  
   204  	encodedJoseHeader := joseBase64Encode(joseHeaderBytes)
   205  	encodedClaimSet := joseBase64Encode(claimSetBytes)
   206  	encodingToSign := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet)
   207  
   208  	var signatureBytes []byte
   209  	if signatureBytes, _, err = issuer.SigningKey.Sign(strings.NewReader(encodingToSign), signingHash); err != nil {
   210  		return "", fmt.Errorf("unable to sign jwt payload: %s", err)
   211  	}
   212  
   213  	signature := joseBase64Encode(signatureBytes)
   214  
   215  	return fmt.Sprintf("%s.%s", encodingToSign, signature), nil
   216  }
   217  
   218  func joseBase64Encode(data []byte) string {
   219  	return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=")
   220  }
   221  

View as plain text