distribution/contrib/token-server/token.go
Stephen J Day 9c88801a12
context: remove definition of Context
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>
2017-08-11 15:53:31 -07:00

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), "=")
}