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 }