go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/tokenserver/appengine/impl/machinetoken/machinetoken.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 machinetoken implements generation of LUCI machine tokens.
    16  package machinetoken
    17  
    18  import (
    19  	"context"
    20  	"crypto/x509"
    21  	"encoding/base64"
    22  	"fmt"
    23  	"strings"
    24  	"time"
    25  
    26  	"google.golang.org/protobuf/proto"
    27  
    28  	"go.chromium.org/luci/common/clock"
    29  	"go.chromium.org/luci/common/retry/transient"
    30  	"go.chromium.org/luci/server/auth/signing"
    31  
    32  	tokenserver "go.chromium.org/luci/tokenserver/api"
    33  	"go.chromium.org/luci/tokenserver/api/admin/v1"
    34  	"go.chromium.org/luci/tokenserver/appengine/impl/utils/tokensigning"
    35  )
    36  
    37  // tokenSigningContext is used to make sure machine token is not misused in
    38  // place of some other token.
    39  //
    40  // See SigningContext in utils/tokensigning.Signer.
    41  //
    42  // TODO(vadimsh): Enable it. Requires some temporary hacks to accept old and
    43  // new tokens at the same time.
    44  const tokenSigningContext = ""
    45  
    46  // MintParams is passed to Mint.
    47  //
    48  // The name of the machine (to put into the machine token) is extracted from
    49  // the certificate.
    50  type MintParams struct {
    51  	// Cert is the certificate used when authenticating the token requester.
    52  	//
    53  	// It's serial number will be put in the token.
    54  	Cert *x509.Certificate
    55  
    56  	// Config is a chunk of configuration related to the machine domain.
    57  	//
    58  	// It describes parameters for the token. Fetched from luci-config as part of
    59  	// CA configuration.
    60  	Config *admin.CertificateAuthorityConfig
    61  
    62  	// Signer produces RSA-SHA256 signatures using a token server key.
    63  	//
    64  	// Usually it is using SignBytes GAE API.
    65  	Signer signing.Signer
    66  }
    67  
    68  // MachineFQDN extracts the machine's FQDN from the certificate.
    69  //
    70  // It uses either Common Name or X509v3 Subject Alternative Name DNS field
    71  // (depending on the presence of the later). If SAN DNS field is present, it
    72  // will be used, should it be consistent with the Common Name.
    73  //
    74  // The extracted FQDN is convert to lower case.
    75  //
    76  // Returns an error if CN field is empty.
    77  func (p *MintParams) MachineFQDN() (string, error) {
    78  	cn := strings.ToLower(p.Cert.Subject.CommonName)
    79  	if cn == "" {
    80  		return "", fmt.Errorf("unsupported cert, Subject CN field is required")
    81  	}
    82  
    83  	for _, altName := range p.Cert.DNSNames {
    84  		if dns := strings.ToLower(altName); strings.HasPrefix(dns, cn+".") {
    85  			return dns, nil
    86  		}
    87  	}
    88  	return cn, nil
    89  }
    90  
    91  // Validate checks that token minting parameters are allowed.
    92  func (p *MintParams) Validate() error {
    93  	// Check FDQN.
    94  	fqdn, err := p.MachineFQDN()
    95  	if err != nil {
    96  		return err
    97  	}
    98  	chunks := strings.SplitN(fqdn, ".", 2)
    99  	if len(chunks) != 2 {
   100  		return fmt.Errorf("not a valid FQDN %q", fqdn)
   101  	}
   102  	domain := chunks[1] // e.g. "us-central1-a.c.project-id.internal"
   103  
   104  	// Check DomainConfig for given domain.
   105  	domainCfg := domainConfig(p.Config, domain)
   106  	if domainCfg == nil {
   107  		return fmt.Errorf("the domain %q is not listed in the config", domain)
   108  	}
   109  	if domainCfg.MachineTokenLifetime <= 0 {
   110  		return fmt.Errorf("machine tokens for machines in domain %q are not allowed", domain)
   111  	}
   112  
   113  	// We don't support negative SNs.
   114  	sn := p.Cert.SerialNumber
   115  	if sn.Sign() <= 0 {
   116  		return fmt.Errorf("invalid certificate serial number: %s", sn)
   117  	}
   118  	return nil
   119  }
   120  
   121  // domainConfig returns DomainConfig (part of *.cfg file) for a given domain.
   122  //
   123  // It enumerates all domains specified in the config finding first domain that
   124  // is equal to 'domain' or has it as a subdomain.
   125  //
   126  // Returns nil if requested domain is not represented in the config.
   127  func domainConfig(cfg *admin.CertificateAuthorityConfig, domain string) *admin.DomainConfig {
   128  	for _, domainCfg := range cfg.KnownDomains {
   129  		for _, domainInCfg := range domainCfg.Domain {
   130  			if domainInCfg == domain || strings.HasSuffix(domain, "."+domainInCfg) {
   131  				return domainCfg
   132  			}
   133  		}
   134  	}
   135  	return nil
   136  }
   137  
   138  // Mint generates a new machine token proto, signs and serializes it.
   139  //
   140  // Returns its body as a proto, and as a signed base64-encoded final token.
   141  func Mint(c context.Context, params *MintParams) (*tokenserver.MachineTokenBody, string, error) {
   142  	if err := params.Validate(); err != nil {
   143  		return nil, "", err
   144  	}
   145  	fqdn, err := params.MachineFQDN()
   146  	if err != nil {
   147  		panic("impossible") // checked in Validate already
   148  	}
   149  	chunks := strings.SplitN(fqdn, ".", 2)
   150  	if len(chunks) != 2 {
   151  		panic("impossible") // checked in Validate already
   152  	}
   153  	cfg := domainConfig(params.Config, chunks[1])
   154  	if cfg == nil {
   155  		panic("impossible") // checked in Validate already
   156  	}
   157  
   158  	srvInfo, err := params.Signer.ServiceInfo(c)
   159  	if err != nil {
   160  		return nil, "", transient.Tag.Apply(err)
   161  	}
   162  
   163  	body := &tokenserver.MachineTokenBody{
   164  		MachineFqdn: fqdn,
   165  		IssuedBy:    srvInfo.ServiceAccountName,
   166  		IssuedAt:    uint64(clock.Now(c).Unix()),
   167  		Lifetime:    uint64(cfg.MachineTokenLifetime),
   168  		CaId:        params.Config.UniqueId,
   169  		CertSn:      params.Cert.SerialNumber.Bytes(),
   170  	}
   171  	signed, err := SignToken(c, params.Signer, body)
   172  	if err != nil {
   173  		return nil, "", err
   174  	}
   175  	return body, signed, nil
   176  }
   177  
   178  // SignToken signs and serializes the machine subtoken.
   179  //
   180  // It doesn't do any validation. Assumes the prepared subtoken is valid.
   181  //
   182  // Produces base64-encoded token or a transient error.
   183  func SignToken(c context.Context, signer signing.Signer, body *tokenserver.MachineTokenBody) (string, error) {
   184  	s := tokensigning.Signer{
   185  		Signer:         signer,
   186  		SigningContext: tokenSigningContext,
   187  		Encoding:       base64.RawStdEncoding,
   188  		Wrap: func(w *tokensigning.Unwrapped) proto.Message {
   189  			return &tokenserver.MachineTokenEnvelope{
   190  				TokenBody: w.Body,
   191  				RsaSha256: w.RsaSHA256Sig,
   192  				KeyId:     w.KeyID,
   193  			}
   194  		},
   195  	}
   196  	return s.SignToken(c, body)
   197  }
   198  
   199  // InspectToken returns information about the machine token.
   200  //
   201  // Inspection.Envelope is either nil or *tokenserver.MachineTokenEnvelope.
   202  // Inspection.Body is either nil or *tokenserver.MachineTokenBody.
   203  func InspectToken(c context.Context, certs tokensigning.CertificatesSupplier, tok string) (*tokensigning.Inspection, error) {
   204  	i := tokensigning.Inspector{
   205  		Encoding:       base64.RawStdEncoding,
   206  		Certificates:   certs,
   207  		SigningContext: tokenSigningContext,
   208  		Envelope:       func() proto.Message { return &tokenserver.MachineTokenEnvelope{} },
   209  		Body:           func() proto.Message { return &tokenserver.MachineTokenBody{} },
   210  		Unwrap: func(e proto.Message) tokensigning.Unwrapped {
   211  			env := e.(*tokenserver.MachineTokenEnvelope)
   212  			return tokensigning.Unwrapped{
   213  				Body:         env.TokenBody,
   214  				RsaSHA256Sig: env.RsaSha256,
   215  				KeyID:        env.KeyId,
   216  			}
   217  		},
   218  		Lifespan: func(b proto.Message) tokensigning.Lifespan {
   219  			body := b.(*tokenserver.MachineTokenBody)
   220  			return tokensigning.Lifespan{
   221  				NotBefore: time.Unix(int64(body.IssuedAt), 0),
   222  				NotAfter:  time.Unix(int64(body.IssuedAt)+int64(body.Lifetime), 0),
   223  			}
   224  		},
   225  	}
   226  	return i.InspectToken(c, tok)
   227  }