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
22
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
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
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
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
80
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
94
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
104 type TokenIssuer struct {
105 Issuer string
106 SigningKey libtrust.PrivateKey
107 Expiration time.Duration
108 }
109
110
111
112 func (issuer *TokenIssuer) CreateJWT(subject string, audience string, grantedAccessList []auth.Access) (string, error) {
113
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