go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/utils/tokensigning/signer.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  	"encoding/base64"
    20  
    21  	"google.golang.org/protobuf/proto"
    22  
    23  	"go.chromium.org/luci/common/retry/transient"
    24  	"go.chromium.org/luci/server/auth/signing"
    25  )
    26  
    27  // Signer knows how to sign protos and serialize/encode signed result.
    28  type Signer struct {
    29  	// Signer is the actual signer: it knows how to sign blobs.
    30  	Signer signing.Signer
    31  
    32  	// SigningContext is prepended to the token blob before it is signed.
    33  	//
    34  	// It exists to avoid cross-protocol attacks, when same key is used to sign
    35  	// different kinds of tokens. An attacker may get a token of kind A, and use
    36  	// it in place of a token of kind B. This may produce unexpected (possibly
    37  	// bad) results, especially for proto-serialized tokens (that all use small
    38  	// integers for message tags).
    39  	//
    40  	// By using different SigningContext strings per token kind we ensure tokens
    41  	// are recognized as correctly signed only when they are used in an
    42  	// appropriate context.
    43  	//
    44  	// SigningContext should be some arbitrary constant string that designates the
    45  	// usage of the token. We actually prepend SigningContext + '\x00' to the
    46  	// token blob.
    47  	//
    48  	// If SigningContext is "", this mechanism is completely skipped.
    49  	SigningContext string
    50  
    51  	// Encoding is base64 encoding to use (or RawURLEncoding if nil).
    52  	Encoding *base64.Encoding
    53  
    54  	// Wrap takes a blob with token body, the signature and signing key details
    55  	// and returns a proto to serialize/encode and return as the final signed
    56  	// token.
    57  	Wrap func(unwrapped *Unwrapped) proto.Message
    58  }
    59  
    60  // SignToken serializes the body, signs it and returns serialized envelope.
    61  //
    62  // Produces base64 URL-safe token or an error (possibly transient).
    63  func (s *Signer) SignToken(c context.Context, body proto.Message) (string, error) {
    64  	info, err := s.Signer.ServiceInfo(c)
    65  	if err != nil {
    66  		return "", transient.Tag.Apply(err)
    67  	}
    68  	blob, err := proto.Marshal(body)
    69  	if err != nil {
    70  		return "", err
    71  	}
    72  	withCtx := prependSigningContext(blob, s.SigningContext)
    73  	keyID, sig, err := s.Signer.SignBytes(c, withCtx)
    74  	if err != nil {
    75  		return "", transient.Tag.Apply(err)
    76  	}
    77  	tok, err := proto.Marshal(s.Wrap(&Unwrapped{
    78  		Body:         blob,
    79  		RsaSHA256Sig: sig,
    80  		SignerID:     info.ServiceAccountName,
    81  		KeyID:        keyID,
    82  	}))
    83  	if err != nil {
    84  		return "", err
    85  	}
    86  	enc := s.Encoding
    87  	if enc == nil {
    88  		enc = base64.RawURLEncoding
    89  	}
    90  	return enc.EncodeToString(tok), nil
    91  }