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