go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/utils/tokensigning/inspector.go (about)

     1  // Copyright 2017 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 tokensigning
    16  
    17  import (
    18  	"context"
    19  	"crypto/x509"
    20  	"encoding/base64"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"google.golang.org/protobuf/proto"
    25  
    26  	"go.chromium.org/luci/common/clock"
    27  	"go.chromium.org/luci/common/retry/transient"
    28  	"go.chromium.org/luci/server/auth/signing"
    29  )
    30  
    31  // Inspector knows how to inspect tokens produced by Signer.
    32  //
    33  // It is used by Inspect<something>Token RPCs (available only to admins). It
    34  // tries to return as much information as possible. In particular, it tries to
    35  // deserialize the token body even if the signature is no longer valid. This is
    36  // useful when debugging broken tokens.
    37  //
    38  // Since it is available only to admins, we assume the possibility of abuse is
    39  // small.
    40  type Inspector struct {
    41  	// Certificates returns certs bundle used to validate the token signature.
    42  	Certificates CertificatesSupplier
    43  
    44  	// Encoding is base64 encoding to used for token (or RawURLEncoding if nil).
    45  	Encoding *base64.Encoding
    46  
    47  	// SigningContext is prepended to the token blob before signature check.
    48  	//
    49  	// See SigningContext in Signer struct for more info.
    50  	SigningContext string
    51  
    52  	// Envelope returns an empty message of same type as produced by signer.Wrap.
    53  	Envelope func() proto.Message
    54  
    55  	// Body returns an empty messages corresponding to the token body type.
    56  	Body func() proto.Message
    57  
    58  	// Unwrap extracts information from envelope proto message.
    59  	//
    60  	// It must set Body, RsaSHA256Sig and KeyID fields.
    61  	Unwrap func(e proto.Message) Unwrapped
    62  
    63  	// Lifespan extracts a lifespan from the deserialized body of the token.
    64  	Lifespan func(e proto.Message) Lifespan
    65  }
    66  
    67  // CertificatesSupplier produces signing.PublicCertificates.
    68  type CertificatesSupplier interface {
    69  	// Certificates returns certs bundle used to validate the token signature.
    70  	Certificates(c context.Context) (*signing.PublicCertificates, error)
    71  }
    72  
    73  // Inspection is the result of token inspection.
    74  type Inspection struct {
    75  	Signed           bool          // true if the token is structurally valid and signed
    76  	NonExpired       bool          // true if the token hasn't expire yet (may be bogus for unsigned tokens)
    77  	InvalidityReason string        // human readable reason why the token is invalid or "" if it is valid
    78  	Envelope         proto.Message // deserialized token envelope
    79  	Body             proto.Message // deserialized token body
    80  }
    81  
    82  // InspectToken extracts as much information as possible from the token.
    83  //
    84  // Returns errors only if the inspection operation itself fails (i.e we can't
    85  // determine whether the token valid or not). If the given token is invalid,
    86  // returns Inspection object with details and nil error.
    87  func (i *Inspector) InspectToken(c context.Context, tok string) (*Inspection, error) {
    88  	res := &Inspection{}
    89  
    90  	enc := i.Encoding
    91  	if enc == nil {
    92  		enc = base64.RawURLEncoding
    93  	}
    94  
    95  	// Byte blob with serialized envelope.
    96  	blob, err := enc.DecodeString(tok)
    97  	if err != nil {
    98  		res.InvalidityReason = fmt.Sprintf("not base64 - %s", err)
    99  		return res, nil
   100  	}
   101  
   102  	// Deserialize the envelope into a proto message.
   103  	env := i.Envelope()
   104  	if err = proto.Unmarshal(blob, env); err != nil {
   105  		res.InvalidityReason = fmt.Sprintf("can't unmarshal the envelope - %s", err)
   106  		return res, nil
   107  	}
   108  	res.Envelope = env
   109  
   110  	// Convert opaque proto message into a struct we can work with.
   111  	unwrapped := i.Unwrap(res.Envelope)
   112  
   113  	// Try to deserialize the body, if possible.
   114  	body := i.Body()
   115  	if err = proto.Unmarshal(unwrapped.Body, body); err != nil {
   116  		res.InvalidityReason = fmt.Sprintf("can't unmarshal the token body - %s", err)
   117  		return res, nil
   118  	}
   119  	res.Body = body
   120  
   121  	var reasons []string
   122  
   123  	if reason := i.checkLifetime(c, body); reason != "" {
   124  		reasons = append(reasons, reason)
   125  	} else {
   126  		res.NonExpired = true
   127  	}
   128  
   129  	switch reason, err := i.checkSignature(c, &unwrapped); {
   130  	case err != nil:
   131  		return nil, err
   132  	case reason != "":
   133  		reasons = append(reasons, reason)
   134  	default:
   135  		res.Signed = true
   136  	}
   137  
   138  	res.InvalidityReason = strings.Join(reasons, "; ")
   139  	return res, nil
   140  }
   141  
   142  // checkLifetime checks that token has not expired yet.
   143  //
   144  // Returns "" if it hasn't expire yet, or an invalidity reason if it has.
   145  func (i *Inspector) checkLifetime(c context.Context, body proto.Message) string {
   146  	lifespan := i.Lifespan(body)
   147  	now := clock.Now(c)
   148  	switch {
   149  	case lifespan.NotAfter == lifespan.NotBefore:
   150  		return "can't extract the token lifespan"
   151  	case now.Before(lifespan.NotBefore):
   152  		return "not active yet"
   153  	case now.After(lifespan.NotAfter):
   154  		return "expired"
   155  	default:
   156  		return ""
   157  	}
   158  }
   159  
   160  // checkSignature verifies the signature of the token.
   161  //
   162  // Returns "" if the signature is correct, or an invalidity reason if it is not.
   163  func (i *Inspector) checkSignature(c context.Context, unwrapped *Unwrapped) (string, error) {
   164  	certsBundle, err := i.Certificates.Certificates(c)
   165  	if err != nil {
   166  		return "", transient.Tag.Apply(err)
   167  	}
   168  	cert, err := certsBundle.CertificateForKey(unwrapped.KeyID)
   169  	if err != nil {
   170  		return fmt.Sprintf("invalid signing key - %s", err), nil
   171  	}
   172  	withCtx := prependSigningContext(unwrapped.Body, i.SigningContext)
   173  	err = cert.CheckSignature(x509.SHA256WithRSA, withCtx, unwrapped.RsaSHA256Sig)
   174  	if err != nil {
   175  		return fmt.Sprintf("bad signature - %s", err), nil
   176  	}
   177  	return "", nil
   178  }