token.go 5.3 KB

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