blob: 6c8f02e3c4cd8fd2e2c4312fe4e17604d340bfa5 [file]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package machinetoken implements generation of LUCI machine tokens.
package machinetoken
import (
"crypto/x509"
"encoding/base64"
"fmt"
"math"
"math/big"
"strings"
"time"
"github.com/golang/protobuf/proto"
"golang.org/x/net/context"
"github.com/luci/luci-go/common/clock"
"github.com/luci/luci-go/common/retry/transient"
"github.com/luci/luci-go/server/auth/signing"
"github.com/luci/luci-go/tokenserver/api"
"github.com/luci/luci-go/tokenserver/api/admin/v1"
"github.com/luci/luci-go/tokenserver/appengine/impl/utils/tokensigning"
)
// tokenSigningContext is used to make sure machine token is not misused in
// place of some other token.
//
// See SigningContext in utils/tokensigning.Signer.
//
// TODO(vadimsh): Enable it. Requires some temporary hacks to accept old and
// new tokens at the same time.
const tokenSigningContext = ""
var maxUint64 = big.NewInt(0).SetUint64(math.MaxUint64)
// MintParams is passed to Mint.
type MintParams struct {
// FQDN is a full name of a host to mint a token for.
//
// Must be in lowercase.
FQDN string
// Cert is the certificate used when authenticating the token requester.
//
// It's serial number will be put in the token.
Cert *x509.Certificate
// Config is a chunk of configuration related to the machine domain.
//
// It describes parameters for the token. Fetched from luci-config as part of
// CA configuration.
Config *admin.CertificateAuthorityConfig
// Signer produces RSA-SHA256 signatures using a token server key.
//
// Usually it is using SignBytes GAE API.
Signer signing.Signer
}
// Validate checks that token minting parameters are allowed.
func (p *MintParams) Validate() error {
// Check FDQN.
if p.FQDN != strings.ToLower(p.FQDN) {
return fmt.Errorf("expecting FQDN in lowercase, got %q", p.FQDN)
}
chunks := strings.SplitN(p.FQDN, ".", 2)
if len(chunks) != 2 {
return fmt.Errorf("not a valid FQDN %q", p.FQDN)
}
domain := chunks[1] // e.g. "us-central1-a.c.project-id.internal"
// Check DomainConfig for given domain.
domainCfg := domainConfig(p.Config, domain)
if domainCfg == nil {
return fmt.Errorf("the domain %q is not whitelisted in the config", domain)
}
if domainCfg.MachineTokenLifetime <= 0 {
return fmt.Errorf("machine tokens for machines in domain %q are not allowed", domain)
}
// Make sure cert serial number fits into uint64. We don't support negative or
// giant SNs.
sn := p.Cert.SerialNumber
if sn.Sign() <= 0 || sn.Cmp(maxUint64) >= 0 {
return fmt.Errorf("invalid certificate serial number: %s", sn)
}
return nil
}
// domainConfig returns DomainConfig (part of *.cfg file) for a given domain.
//
// It enumerates all domains specified in the config finding first domain that
// is equal to 'domain' or has it as a subdomain.
//
// Returns nil if requested domain is not represented in the config.
func domainConfig(cfg *admin.CertificateAuthorityConfig, domain string) *admin.DomainConfig {
for _, domainCfg := range cfg.KnownDomains {
for _, domainInCfg := range domainCfg.Domain {
if domainInCfg == domain || strings.HasSuffix(domain, "."+domainInCfg) {
return domainCfg
}
}
}
return nil
}
// Mint generates a new machine token proto, signs and serializes it.
//
// Returns its body as a proto, and as a signed base64-encoded final token.
func Mint(c context.Context, params *MintParams) (*tokenserver.MachineTokenBody, string, error) {
if err := params.Validate(); err != nil {
return nil, "", err
}
chunks := strings.SplitN(params.FQDN, ".", 2)
if len(chunks) != 2 {
panic("impossible") // checked in Validate already
}
cfg := domainConfig(params.Config, chunks[1])
if cfg == nil {
panic("impossible") // checked in Validate already
}
srvInfo, err := params.Signer.ServiceInfo(c)
if err != nil {
return nil, "", transient.Tag.Apply(err)
}
body := &tokenserver.MachineTokenBody{
MachineFqdn: params.FQDN,
IssuedBy: srvInfo.ServiceAccountName,
IssuedAt: uint64(clock.Now(c).Unix()),
Lifetime: uint64(cfg.MachineTokenLifetime),
CaId: params.Config.UniqueId,
CertSn: params.Cert.SerialNumber.Uint64(), // already validated, fits uint64
}
signed, err := SignToken(c, params.Signer, body)
if err != nil {
return nil, "", err
}
return body, signed, nil
}
// SignToken signs and serializes the machine subtoken.
//
// It doesn't do any validation. Assumes the prepared subtoken is valid.
//
// Produces base64-encoded token or a transient error.
func SignToken(c context.Context, signer signing.Signer, body *tokenserver.MachineTokenBody) (string, error) {
s := tokensigning.Signer{
Signer: signer,
SigningContext: tokenSigningContext,
Encoding: base64.RawStdEncoding,
Wrap: func(w *tokensigning.Unwrapped) proto.Message {
return &tokenserver.MachineTokenEnvelope{
TokenBody: w.Body,
RsaSha256: w.RsaSHA256Sig,
KeyId: w.KeyID,
}
},
}
return s.SignToken(c, body)
}
// InspectToken returns information about the machine token.
//
// Inspection.Envelope is either nil or *tokenserver.MachineTokenEnvelope.
// Inspection.Body is either nil or *tokenserver.MachineTokenBody.
func InspectToken(c context.Context, certs tokensigning.CertificatesSupplier, tok string) (*tokensigning.Inspection, error) {
i := tokensigning.Inspector{
Encoding: base64.RawStdEncoding,
Certificates: certs,
SigningContext: tokenSigningContext,
Envelope: func() proto.Message { return &tokenserver.MachineTokenEnvelope{} },
Body: func() proto.Message { return &tokenserver.MachineTokenBody{} },
Unwrap: func(e proto.Message) tokensigning.Unwrapped {
env := e.(*tokenserver.MachineTokenEnvelope)
return tokensigning.Unwrapped{
Body: env.TokenBody,
RsaSHA256Sig: env.RsaSha256,
KeyID: env.KeyId,
}
},
Lifespan: func(b proto.Message) tokensigning.Lifespan {
body := b.(*tokenserver.MachineTokenBody)
return tokensigning.Lifespan{
NotBefore: time.Unix(int64(body.IssuedAt), 0),
NotAfter: time.Unix(int64(body.IssuedAt)+int64(body.Lifetime), 0),
}
},
}
return i.InspectToken(c, tok)
}