go.chromium.org/luci@v0.0.0-20250314024836-d9a61d0730e6/tokenserver/client/tokenclient.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 client
    16  
    17  import (
    18  	"context"
    19  	"crypto/x509"
    20  	"fmt"
    21  
    22  	"google.golang.org/grpc"
    23  	"google.golang.org/grpc/codes"
    24  	"google.golang.org/grpc/status"
    25  	"google.golang.org/protobuf/proto"
    26  	"google.golang.org/protobuf/types/known/timestamppb"
    27  
    28  	"go.chromium.org/luci/common/clock"
    29  	"go.chromium.org/luci/common/retry/transient"
    30  	"go.chromium.org/luci/grpc/grpcutil"
    31  
    32  	"go.chromium.org/luci/tokenserver/api/minter/v1"
    33  )
    34  
    35  // Client can make signed requests to the token server.
    36  type Client struct {
    37  	// Client is interface to use for raw RPC calls to the token server.
    38  	//
    39  	// Use minter.NewTokenMinterClient to create it. Note that transport-level
    40  	// authentication is not needed.
    41  	Client TokenMinterClient
    42  
    43  	// Signer knows how to sign requests using some private key.
    44  	Signer Signer
    45  }
    46  
    47  // TokenMinterClient is subset of minter.TokenMinterClient this package uses.
    48  type TokenMinterClient interface {
    49  	// MintMachineToken generates a new token for an authenticated machine.
    50  	MintMachineToken(context.Context, *minter.MintMachineTokenRequest, ...grpc.CallOption) (*minter.MintMachineTokenResponse, error)
    51  }
    52  
    53  // Signer knows how to sign requests using some private key.
    54  type Signer interface {
    55  	// Algo returns an algorithm that the signer implements.
    56  	Algo(ctx context.Context) (x509.SignatureAlgorithm, error)
    57  
    58  	// Certificate returns ASN.1 DER blob with the certificate of the signer.
    59  	Certificate(ctx context.Context) ([]byte, error)
    60  
    61  	// Sign signs a blob using the private key.
    62  	Sign(ctx context.Context, blob []byte) ([]byte, error)
    63  }
    64  
    65  // RPCError is optionally returned for recognized RPC errors.
    66  //
    67  // Use typecast to distinguish recognized and unrecognized errors.
    68  type RPCError struct {
    69  	error
    70  
    71  	GrpcCode       codes.Code       // grpc-level status code
    72  	ErrorCode      minter.ErrorCode // protocol-level status code
    73  	ServiceVersion string           // version of the backend, if known
    74  }
    75  
    76  // MintMachineToken signs the request using the signer and sends it.
    77  //
    78  // It will update in-place the following fields of the request:
    79  //   - Certificate will be set to ASN1 cert corresponding to the signer key.
    80  //   - SignatureAlgorithm will be set to the algorithm used to sign the request.
    81  //   - IssuedAt will be set to the current time.
    82  //
    83  // The rest of the fields must be already populated by the caller and will be
    84  // sent to the server as is.
    85  //
    86  // Returns:
    87  //   - TokenResponse on success.
    88  //   - Non-transient error on fatal errors.
    89  //   - Transient error on transient errors.
    90  //
    91  // You can sniff error for RPCError type to grab more error details.
    92  func (c *Client) MintMachineToken(ctx context.Context, req *minter.MachineTokenRequest, opts ...grpc.CallOption) (*minter.MachineTokenResponse, error) {
    93  	// Fill in SignatureAlgorithm.
    94  	algo, err := c.Signer.Algo(ctx)
    95  	if err != nil {
    96  		return nil, err
    97  	}
    98  	switch algo {
    99  	case x509.SHA256WithRSA:
   100  		req.SignatureAlgorithm = minter.SignatureAlgorithm_SHA256_RSA_ALGO
   101  	default:
   102  		return nil, fmt.Errorf("unsupported signing algorithm - %s", algo)
   103  	}
   104  
   105  	// Fill in Certificate and IssuedAt.
   106  	if req.Certificate, err = c.Signer.Certificate(ctx); err != nil {
   107  		return nil, err
   108  	}
   109  	req.IssuedAt = timestamppb.New(clock.Now(ctx))
   110  
   111  	// Serialize and sign.
   112  	tokenRequest, err := proto.Marshal(req)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	signature, err := c.Signer.Sign(ctx, tokenRequest)
   117  	if err != nil {
   118  		return nil, err
   119  	}
   120  
   121  	// Make an RPC call (with retries done by pRPC client).
   122  	resp, err := c.Client.MintMachineToken(ctx, &minter.MintMachineTokenRequest{
   123  		SerializedTokenRequest: tokenRequest,
   124  		Signature:              signature,
   125  	}, opts...)
   126  
   127  	// Fatal pRPC-level error or transient error in case retries didn't help.
   128  	if err != nil {
   129  		code := status.Code(err)
   130  		err = RPCError{
   131  			error:    err,
   132  			GrpcCode: code,
   133  		}
   134  		if grpcutil.IsTransientCode(code) {
   135  			err = transient.Tag.Apply(err)
   136  		}
   137  		return nil, err
   138  	}
   139  
   140  	// The response still may indicate a fatal error.
   141  	if resp.ErrorCode != minter.ErrorCode_SUCCESS {
   142  		details := resp.ErrorMessage
   143  		if details == "" {
   144  			details = "no detailed error message"
   145  		}
   146  		return nil, RPCError{
   147  			error:          fmt.Errorf("token server error %s - %s", resp.ErrorCode, details),
   148  			GrpcCode:       codes.OK,
   149  			ErrorCode:      resp.ErrorCode,
   150  			ServiceVersion: resp.ServiceVersion,
   151  		}
   152  	}
   153  
   154  	// Must not happen. But better return an error than nil-panic if it does.
   155  	if resp.TokenResponse == nil {
   156  		return nil, fmt.Errorf("token server didn't return a token")
   157  	}
   158  
   159  	return resp.TokenResponse, nil
   160  }