9c88801a12
Back in the before time, the best practices surrounding usage of Context weren't quite worked out. We defined our own type to make usage easier. As this packaged was used elsewhere, it make it more and more challenging to integrate with the forked `Context` type. Now that it is available in the standard library, we can just use that one directly. To make usage more consistent, we now use `dcontext` when referring to the distribution context package. Signed-off-by: Stephen J Day <stephen.day@docker.com>
220 lines
5.9 KiB
Go
220 lines
5.9 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
dcontext "github.com/docker/distribution/context"
|
|
"github.com/docker/distribution/registry/auth"
|
|
"github.com/docker/distribution/registry/auth/token"
|
|
"github.com/docker/libtrust"
|
|
)
|
|
|
|
// ResolveScopeSpecifiers converts a list of scope specifiers from a token
|
|
// request's `scope` query parameters into a list of standard access objects.
|
|
func ResolveScopeSpecifiers(ctx context.Context, scopeSpecs []string) []auth.Access {
|
|
requestedAccessSet := make(map[auth.Access]struct{}, 2*len(scopeSpecs))
|
|
|
|
for _, scopeSpecifier := range scopeSpecs {
|
|
// There should be 3 parts, separated by a `:` character.
|
|
parts := strings.SplitN(scopeSpecifier, ":", 3)
|
|
|
|
if len(parts) != 3 {
|
|
dcontext.GetLogger(ctx).Infof("ignoring unsupported scope format %s", scopeSpecifier)
|
|
continue
|
|
}
|
|
|
|
resourceType, resourceName, actions := parts[0], parts[1], parts[2]
|
|
|
|
resourceType, resourceClass := splitResourceClass(resourceType)
|
|
if resourceType == "" {
|
|
continue
|
|
}
|
|
|
|
// Actions should be a comma-separated list of actions.
|
|
for _, action := range strings.Split(actions, ",") {
|
|
requestedAccess := auth.Access{
|
|
Resource: auth.Resource{
|
|
Type: resourceType,
|
|
Class: resourceClass,
|
|
Name: resourceName,
|
|
},
|
|
Action: action,
|
|
}
|
|
|
|
// Add this access to the requested access set.
|
|
requestedAccessSet[requestedAccess] = struct{}{}
|
|
}
|
|
}
|
|
|
|
requestedAccessList := make([]auth.Access, 0, len(requestedAccessSet))
|
|
for requestedAccess := range requestedAccessSet {
|
|
requestedAccessList = append(requestedAccessList, requestedAccess)
|
|
}
|
|
|
|
return requestedAccessList
|
|
}
|
|
|
|
var typeRegexp = regexp.MustCompile(`^([a-z0-9]+)(\([a-z0-9]+\))?$`)
|
|
|
|
func splitResourceClass(t string) (string, string) {
|
|
matches := typeRegexp.FindStringSubmatch(t)
|
|
if len(matches) < 2 {
|
|
return "", ""
|
|
}
|
|
if len(matches) == 2 || len(matches[2]) < 2 {
|
|
return matches[1], ""
|
|
}
|
|
return matches[1], matches[2][1 : len(matches[2])-1]
|
|
}
|
|
|
|
// ResolveScopeList converts a scope list from a token request's
|
|
// `scope` parameter into a list of standard access objects.
|
|
func ResolveScopeList(ctx context.Context, scopeList string) []auth.Access {
|
|
scopes := strings.Split(scopeList, " ")
|
|
return ResolveScopeSpecifiers(ctx, scopes)
|
|
}
|
|
|
|
func scopeString(a auth.Access) string {
|
|
if a.Class != "" {
|
|
return fmt.Sprintf("%s(%s):%s:%s", a.Type, a.Class, a.Name, a.Action)
|
|
}
|
|
return fmt.Sprintf("%s:%s:%s", a.Type, a.Name, a.Action)
|
|
}
|
|
|
|
// ToScopeList converts a list of access to a
|
|
// scope list string
|
|
func ToScopeList(access []auth.Access) string {
|
|
var s []string
|
|
for _, a := range access {
|
|
s = append(s, scopeString(a))
|
|
}
|
|
return strings.Join(s, ",")
|
|
}
|
|
|
|
// TokenIssuer represents an issuer capable of generating JWT tokens
|
|
type TokenIssuer struct {
|
|
Issuer string
|
|
SigningKey libtrust.PrivateKey
|
|
Expiration time.Duration
|
|
}
|
|
|
|
// CreateJWT creates and signs a JSON Web Token for the given subject and
|
|
// audience with the granted access.
|
|
func (issuer *TokenIssuer) CreateJWT(subject string, audience string, grantedAccessList []auth.Access) (string, error) {
|
|
// Make a set of access entries to put in the token's claimset.
|
|
resourceActionSets := make(map[auth.Resource]map[string]struct{}, len(grantedAccessList))
|
|
for _, access := range grantedAccessList {
|
|
actionSet, exists := resourceActionSets[access.Resource]
|
|
if !exists {
|
|
actionSet = map[string]struct{}{}
|
|
resourceActionSets[access.Resource] = actionSet
|
|
}
|
|
actionSet[access.Action] = struct{}{}
|
|
}
|
|
|
|
accessEntries := make([]*token.ResourceActions, 0, len(resourceActionSets))
|
|
for resource, actionSet := range resourceActionSets {
|
|
actions := make([]string, 0, len(actionSet))
|
|
for action := range actionSet {
|
|
actions = append(actions, action)
|
|
}
|
|
|
|
accessEntries = append(accessEntries, &token.ResourceActions{
|
|
Type: resource.Type,
|
|
Class: resource.Class,
|
|
Name: resource.Name,
|
|
Actions: actions,
|
|
})
|
|
}
|
|
|
|
randomBytes := make([]byte, 15)
|
|
_, err := io.ReadFull(rand.Reader, randomBytes)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
randomID := base64.URLEncoding.EncodeToString(randomBytes)
|
|
|
|
now := time.Now()
|
|
|
|
signingHash := crypto.SHA256
|
|
var alg string
|
|
switch issuer.SigningKey.KeyType() {
|
|
case "RSA":
|
|
alg = "RS256"
|
|
case "EC":
|
|
alg = "ES256"
|
|
default:
|
|
panic(fmt.Errorf("unsupported signing key type %q", issuer.SigningKey.KeyType()))
|
|
}
|
|
|
|
joseHeader := token.Header{
|
|
Type: "JWT",
|
|
SigningAlg: alg,
|
|
}
|
|
|
|
if x5c := issuer.SigningKey.GetExtendedField("x5c"); x5c != nil {
|
|
joseHeader.X5c = x5c.([]string)
|
|
} else {
|
|
var jwkMessage json.RawMessage
|
|
jwkMessage, err = issuer.SigningKey.PublicKey().MarshalJSON()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
joseHeader.RawJWK = &jwkMessage
|
|
}
|
|
|
|
exp := issuer.Expiration
|
|
if exp == 0 {
|
|
exp = 5 * time.Minute
|
|
}
|
|
|
|
claimSet := token.ClaimSet{
|
|
Issuer: issuer.Issuer,
|
|
Subject: subject,
|
|
Audience: audience,
|
|
Expiration: now.Add(exp).Unix(),
|
|
NotBefore: now.Unix(),
|
|
IssuedAt: now.Unix(),
|
|
JWTID: randomID,
|
|
|
|
Access: accessEntries,
|
|
}
|
|
|
|
var (
|
|
joseHeaderBytes []byte
|
|
claimSetBytes []byte
|
|
)
|
|
|
|
if joseHeaderBytes, err = json.Marshal(joseHeader); err != nil {
|
|
return "", fmt.Errorf("unable to encode jose header: %s", err)
|
|
}
|
|
if claimSetBytes, err = json.Marshal(claimSet); err != nil {
|
|
return "", fmt.Errorf("unable to encode claim set: %s", err)
|
|
}
|
|
|
|
encodedJoseHeader := joseBase64Encode(joseHeaderBytes)
|
|
encodedClaimSet := joseBase64Encode(claimSetBytes)
|
|
encodingToSign := fmt.Sprintf("%s.%s", encodedJoseHeader, encodedClaimSet)
|
|
|
|
var signatureBytes []byte
|
|
if signatureBytes, _, err = issuer.SigningKey.Sign(strings.NewReader(encodingToSign), signingHash); err != nil {
|
|
return "", fmt.Errorf("unable to sign jwt payload: %s", err)
|
|
}
|
|
|
|
signature := joseBase64Encode(signatureBytes)
|
|
|
|
return fmt.Sprintf("%s.%s", encodingToSign, signature), nil
|
|
}
|
|
|
|
func joseBase64Encode(data []byte) string {
|
|
return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=")
|
|
}
|