1package main 2 3import ( 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. 23func 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 66var typeRegexp = regexp.MustCompile(`^([a-z0-9]+)(\([a-z0-9]+\))?$`) 67 68func 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. 81func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access { 82 scopes := strings.Split(scopeList, " ") 83 return ResolveScopeSpecifiers(ctx, scopes) 84} 85 86func 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 95func 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 104type 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. 112func (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 218func joseBase64Encode(data []byte) string { 219 return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=") 220} 221