go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/delegation/checker.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package delegation
    16  
    17  import (
    18  	"context"
    19  	"encoding/base64"
    20  	"fmt"
    21  	"strings"
    22  
    23  	"google.golang.org/protobuf/proto"
    24  
    25  	"go.chromium.org/luci/auth/identity"
    26  	"go.chromium.org/luci/common/clock"
    27  	"go.chromium.org/luci/common/errors"
    28  	"go.chromium.org/luci/common/logging"
    29  	"go.chromium.org/luci/common/retry/transient"
    30  
    31  	"go.chromium.org/luci/server/auth/delegation/messages"
    32  	"go.chromium.org/luci/server/auth/internal/tracing"
    33  	"go.chromium.org/luci/server/auth/signing"
    34  )
    35  
    36  const (
    37  	// maxTokenSize is upper bound for expected size of a token (after base64
    38  	// decoding). Larger tokens will be ignored right away.
    39  	maxTokenSize = 8 * 1024
    40  
    41  	// allowedClockDriftSec is how much clock difference we accept, in seconds.
    42  	allowedClockDriftSec = int64(30)
    43  )
    44  
    45  var (
    46  	// ErrMalformedDelegationToken is returned when delegation token cannot be
    47  	// deserialized.
    48  	ErrMalformedDelegationToken = errors.New("auth: malformed delegation token")
    49  
    50  	// ErrUnsignedDelegationToken is returned if token's signature cannot be
    51  	// verified.
    52  	ErrUnsignedDelegationToken = errors.New("auth: unsigned delegation token")
    53  
    54  	// ErrForbiddenDelegationToken is returned if token is structurally correct,
    55  	// but some of its constraints prevents it from being used. For example, it is
    56  	// already expired or it was minted for some other services, etc. See logs for
    57  	// details.
    58  	ErrForbiddenDelegationToken = errors.New("auth: forbidden delegation token")
    59  )
    60  
    61  // CertificatesProvider is used by 'CheckToken', it is implemented by authdb.DB.
    62  //
    63  // It returns certificates of services trusted to sign tokens.
    64  type CertificatesProvider interface {
    65  	// GetCertificates returns a bundle with certificates of a trusted signer.
    66  	//
    67  	// Returns (nil, nil) if the given signer is not trusted.
    68  	//
    69  	// Returns errors (usually transient) if the bundle can't be fetched.
    70  	GetCertificates(ctx context.Context, id identity.Identity) (*signing.PublicCertificates, error)
    71  }
    72  
    73  // GroupsChecker is accepted by 'CheckToken', it is implemented by authdb.DB.
    74  type GroupsChecker interface {
    75  	// IsMember returns true if the given identity belongs to any of the groups.
    76  	//
    77  	// Unknown groups are considered empty. May return errors if underlying
    78  	// datastore has issues.
    79  	IsMember(ctx context.Context, id identity.Identity, groups []string) (bool, error)
    80  }
    81  
    82  // CheckTokenParams is passed to CheckToken.
    83  type CheckTokenParams struct {
    84  	Token                string               // the delegation token to check
    85  	PeerID               identity.Identity    // identity of the caller, as extracted from its credentials
    86  	CertificatesProvider CertificatesProvider // returns certificates with trusted keys
    87  	GroupsChecker        GroupsChecker        // knows how to do group lookups
    88  	OwnServiceIdentity   identity.Identity    // identity of the current service
    89  }
    90  
    91  // CheckToken verifies validity of a delegation token.
    92  //
    93  // If the token is valid, it returns the delegated identity (embedded in the
    94  // token).
    95  //
    96  // May return transient errors.
    97  func CheckToken(ctx context.Context, params CheckTokenParams) (_ identity.Identity, err error) {
    98  	ctx, span := tracing.Start(ctx, "go.chromium.org/luci/server/auth/delegation.CheckToken")
    99  	defer func() { tracing.End(span, err) }()
   100  
   101  	// base64-encoded token -> DelegationToken proto (with signed serialized
   102  	// subtoken).
   103  	tok, err := deserializeToken(params.Token)
   104  	if err != nil {
   105  		logging.Warningf(ctx, "auth: Failed to deserialize delegation token - %s", err)
   106  		return "", ErrMalformedDelegationToken
   107  	}
   108  
   109  	// Signed serialized subtoken -> Subtoken proto.
   110  	subtoken, err := unsealToken(ctx, tok, params.CertificatesProvider)
   111  	if err != nil {
   112  		if transient.Tag.In(err) {
   113  			logging.Warningf(ctx, "auth: Transient error when checking delegation token signature - %s", err)
   114  			return "", err
   115  		}
   116  		logging.Warningf(ctx, "auth: Failed to check delegation token signature - %s", err)
   117  		return "", ErrUnsignedDelegationToken
   118  	}
   119  
   120  	// Validate all constrains encoded in the token and derive the delegated
   121  	// identity.
   122  	return checkSubtoken(ctx, subtoken, &params)
   123  }
   124  
   125  // deserializeToken deserializes DelegationToken proto message.
   126  func deserializeToken(token string) (*messages.DelegationToken, error) {
   127  	blob, err := base64.RawURLEncoding.DecodeString(token)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	if len(blob) > maxTokenSize {
   132  		return nil, fmt.Errorf("the delegation token is too big (%d bytes)", len(blob))
   133  	}
   134  	tok := &messages.DelegationToken{}
   135  	if err = proto.Unmarshal(blob, tok); err != nil {
   136  		return nil, err
   137  	}
   138  	return tok, nil
   139  }
   140  
   141  // unsealToken verifies token's signature and deserializes the subtoken.
   142  //
   143  // May return transient errors.
   144  func unsealToken(ctx context.Context, tok *messages.DelegationToken, certsProvider CertificatesProvider) (*messages.Subtoken, error) {
   145  	// Grab the public keys of the service that signed the token, if we trust it.
   146  	signerID, err := identity.MakeIdentity(tok.SignerId)
   147  	if err != nil {
   148  		return nil, fmt.Errorf("bad signer_id %q - %s", tok.SignerId, err)
   149  	}
   150  	certs, err := certsProvider.GetCertificates(ctx, signerID)
   151  	switch {
   152  	case err != nil:
   153  		return nil, fmt.Errorf("failed to grab certificates of %q - %s", tok.SignerId, err)
   154  	case certs == nil:
   155  		return nil, fmt.Errorf("the signer %q is not trusted", tok.SignerId)
   156  	}
   157  
   158  	// Check the signature on the token.
   159  	err = certs.CheckSignature(tok.SigningKeyId, tok.SerializedSubtoken, tok.Pkcs1Sha256Sig)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	// The signature is correct! Deserialize the subtoken.
   165  	msg := &messages.Subtoken{}
   166  	if err = proto.Unmarshal(tok.SerializedSubtoken, msg); err != nil {
   167  		return nil, err
   168  	}
   169  
   170  	return msg, nil
   171  }
   172  
   173  // checkSubtoken validates the delegation subtoken.
   174  //
   175  // It extracts and returns original delegated_identity.
   176  func checkSubtoken(ctx context.Context, subtoken *messages.Subtoken, params *CheckTokenParams) (identity.Identity, error) {
   177  	if subtoken.Kind != messages.Subtoken_BEARER_DELEGATION_TOKEN {
   178  		logging.Warningf(ctx, "auth: Invalid delegation token kind - %s", subtoken.Kind)
   179  		return "", ErrForbiddenDelegationToken
   180  	}
   181  
   182  	// Do fast checks before heavy ones.
   183  	now := clock.Now(ctx).Unix()
   184  	if err := checkSubtokenExpiration(subtoken, now); err != nil {
   185  		logging.Warningf(ctx, "auth: Bad delegation token expiration - %s", err)
   186  		return "", ErrForbiddenDelegationToken
   187  	}
   188  	if err := checkSubtokenServices(subtoken, params.OwnServiceIdentity); err != nil {
   189  		logging.Warningf(ctx, "auth: Forbidden delegation token - %s", err)
   190  		return "", ErrForbiddenDelegationToken
   191  	}
   192  
   193  	// Do the audience check (may use group lookups).
   194  	if err := checkSubtokenAudience(ctx, subtoken, params.PeerID, params.GroupsChecker); err != nil {
   195  		if transient.Tag.In(err) {
   196  			logging.Warningf(ctx, "auth: Transient error when checking delegation token audience - %s", err)
   197  			return "", err
   198  		}
   199  		logging.Warningf(ctx, "auth: Bad delegation token audience - %s", err)
   200  		return "", ErrForbiddenDelegationToken
   201  	}
   202  
   203  	// Grab delegated identity.
   204  	ident, err := identity.MakeIdentity(subtoken.DelegatedIdentity)
   205  	if err != nil {
   206  		logging.Warningf(ctx, "auth: Invalid delegated_identity in the delegation token - %s", err)
   207  		return "", ErrMalformedDelegationToken
   208  	}
   209  
   210  	return ident, nil
   211  }
   212  
   213  // checkSubtokenExpiration checks 'CreationTime' and 'ValidityDuration' fields.
   214  func checkSubtokenExpiration(t *messages.Subtoken, now int64) error {
   215  	if t.CreationTime <= 0 {
   216  		return fmt.Errorf("invalid 'creation_time' field: %d", t.CreationTime)
   217  	}
   218  	dur := int64(t.ValidityDuration)
   219  	if dur <= 0 {
   220  		return fmt.Errorf("invalid validity_duration: %d", dur)
   221  	}
   222  	if t.CreationTime >= now+allowedClockDriftSec {
   223  		return fmt.Errorf("token is not active yet (created at %d)", t.CreationTime)
   224  	}
   225  	if t.CreationTime+dur < now {
   226  		return fmt.Errorf("token has expired %d sec ago", now-(t.CreationTime+dur))
   227  	}
   228  	return nil
   229  }
   230  
   231  // checkSubtokenServices makes sure the token is usable by the current service.
   232  func checkSubtokenServices(t *messages.Subtoken, serviceID identity.Identity) error {
   233  	// Empty services field is not allowed.
   234  	if len(t.Services) == 0 {
   235  		return fmt.Errorf("the token's services list is empty")
   236  	}
   237  	// Else, make sure we are in the 'services' list or it contains '*'.
   238  	for _, allowed := range t.Services {
   239  		if allowed == "*" || allowed == string(serviceID) {
   240  			return nil
   241  		}
   242  	}
   243  	return fmt.Errorf("token is not intended for %s", serviceID)
   244  }
   245  
   246  // checkSubtokenAudience makes sure the token is intended for use by given
   247  // identity.
   248  //
   249  // May return transient errors.
   250  func checkSubtokenAudience(ctx context.Context, t *messages.Subtoken, ident identity.Identity, checker GroupsChecker) error {
   251  	// Empty audience field is not allowed.
   252  	if len(t.Audience) == 0 {
   253  		return fmt.Errorf("the token's audience list is empty")
   254  	}
   255  	// Try to find a direct hit first, to avoid calling expensive group lookups.
   256  	// Collect the groups along the way for the check below.
   257  	groups := make([]string, 0, len(t.Audience))
   258  	for _, aud := range t.Audience {
   259  		if aud == "*" || aud == string(ident) {
   260  			return nil
   261  		}
   262  		if strings.HasPrefix(aud, "group:") {
   263  			groups = append(groups, strings.TrimPrefix(aud, "group:"))
   264  		}
   265  	}
   266  	// Search through groups now.
   267  	switch ok, err := checker.IsMember(ctx, ident, groups); {
   268  	case err != nil:
   269  		return err // transient error during group lookup
   270  	case ok:
   271  		return nil // success, 'ident' is in the target audience
   272  	}
   273  	return fmt.Errorf("%s is not allowed to use the token", ident)
   274  }