mirror of
https://github.com/tailscale/tailscale.git
synced 2025-09-21 13:41:46 +02:00
This is step 2 of ~4, breaking up #14720 into reviewable chunks, with the aim to make syspolicy be a build-time configurable feature. Step 1 was #16984. In this second step, the util/syspolicy/policyclient package is added with the policyclient.Client interface. This is the interface that's always present (regardless of build tags), and is what code around the tree uses to ask syspolicy/MDM questions. There are two implementations of policyclient.Client for now: 1) NoPolicyClient, which only returns default values. 2) the unexported, temporary 'globalSyspolicy', which is implemented in terms of the global functions we wish to later eliminate. This then starts to plumb around the policyclient.Client to most callers. Future changes will plumb it more. When the last of the global func callers are gone, then we can unexport the global functions and make a proper policyclient.Client type and constructor in the syspolicy package, removing the globalSyspolicy impl out of tsd. The final change will sprinkle build tags in a few more places and lock it in with dependency tests to make sure the dependencies don't later creep back in. Updates #16998 Updates #12614 Change-Id: Ib2c93d15c15c1f2b981464099177cd492d50391c Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
198 lines
5.1 KiB
Go
198 lines
5.1 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build windows
|
|
|
|
// darwin,cgo is also supported by certstore but untested, so it is not enabled.
|
|
|
|
package controlclient
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/tailscale/certstore"
|
|
"tailscale.com/tailcfg"
|
|
"tailscale.com/types/key"
|
|
"tailscale.com/util/syspolicy/pkey"
|
|
"tailscale.com/util/syspolicy/policyclient"
|
|
)
|
|
|
|
// getMachineCertificateSubject returns the exact name of a Subject that needs
|
|
// to be present in an identity's certificate chain to sign a RegisterRequest,
|
|
// formatted as per pkix.Name.String(). The Subject may be that of the identity
|
|
// itself, an intermediate CA or the root CA.
|
|
//
|
|
// If getMachineCertificateSubject() returns "" then no lookup will occur and
|
|
// each RegisterRequest will be unsigned.
|
|
//
|
|
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
|
func getMachineCertificateSubject(polc policyclient.Client) string {
|
|
machineCertSubject, _ := polc.GetString(pkey.MachineCertificateSubject, "")
|
|
return machineCertSubject
|
|
}
|
|
|
|
var (
|
|
errNoMatch = errors.New("no matching certificate")
|
|
errBadRequest = errors.New("malformed request")
|
|
)
|
|
|
|
func isSupportedCertificate(cert *x509.Certificate) bool {
|
|
return cert.PublicKeyAlgorithm == x509.RSA
|
|
}
|
|
|
|
func isSubjectInChain(subject string, chain []*x509.Certificate) bool {
|
|
if len(chain) == 0 || chain[0] == nil {
|
|
return false
|
|
}
|
|
|
|
for _, c := range chain {
|
|
if c == nil {
|
|
continue
|
|
}
|
|
if c.Subject.String() == subject {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func selectIdentityFromSlice(subject string, ids []certstore.Identity, now time.Time) (certstore.Identity, []*x509.Certificate) {
|
|
var bestCandidate struct {
|
|
id certstore.Identity
|
|
chain []*x509.Certificate
|
|
}
|
|
|
|
for _, id := range ids {
|
|
chain, err := id.CertificateChain()
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if len(chain) < 1 {
|
|
continue
|
|
}
|
|
|
|
if !isSupportedCertificate(chain[0]) {
|
|
continue
|
|
}
|
|
|
|
if now.Before(chain[0].NotBefore) || now.After(chain[0].NotAfter) {
|
|
// Certificate is not valid at this time
|
|
continue
|
|
}
|
|
|
|
if !isSubjectInChain(subject, chain) {
|
|
continue
|
|
}
|
|
|
|
// Select the most recently issued certificate. If there is a tie, pick
|
|
// one arbitrarily.
|
|
if len(bestCandidate.chain) > 0 && bestCandidate.chain[0].NotBefore.After(chain[0].NotBefore) {
|
|
continue
|
|
}
|
|
|
|
bestCandidate.id = id
|
|
bestCandidate.chain = chain
|
|
}
|
|
|
|
return bestCandidate.id, bestCandidate.chain
|
|
}
|
|
|
|
// findIdentity locates an identity from the Windows or Darwin certificate
|
|
// store. It returns the first certificate with a matching Subject anywhere in
|
|
// its certificate chain, so it is possible to search for the leaf certificate,
|
|
// intermediate CA or root CA. If err is nil then the returned identity will
|
|
// never be nil (if no identity is found, the error errNoMatch will be
|
|
// returned). If an identity is returned then its certificate chain is also
|
|
// returned.
|
|
func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x509.Certificate, error) {
|
|
ids, err := st.Identities()
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
selected, chain := selectIdentityFromSlice(subject, ids, clock.Now())
|
|
|
|
for _, id := range ids {
|
|
if id != selected {
|
|
id.Close()
|
|
}
|
|
}
|
|
|
|
if selected == nil {
|
|
return nil, nil, errNoMatch
|
|
}
|
|
|
|
return selected, chain, nil
|
|
}
|
|
|
|
// signRegisterRequest looks for a suitable machine identity from the local
|
|
// system certificate store, and if one is found, signs the RegisterRequest
|
|
// using that identity's public key. In addition to the signature, the full
|
|
// certificate chain is included so that the control server can validate the
|
|
// certificate from a copy of the root CA's certificate.
|
|
func signRegisterRequest(polc policyclient.Client, req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey key.MachinePublic) (err error) {
|
|
defer func() {
|
|
if err != nil {
|
|
err = fmt.Errorf("signRegisterRequest: %w", err)
|
|
}
|
|
}()
|
|
|
|
if req.Timestamp == nil {
|
|
return errBadRequest
|
|
}
|
|
|
|
machineCertificateSubject := getMachineCertificateSubject(polc)
|
|
if machineCertificateSubject == "" {
|
|
return errCertificateNotConfigured
|
|
}
|
|
|
|
st, err := certstore.Open(certstore.System)
|
|
if err != nil {
|
|
return fmt.Errorf("open cert store: %w", err)
|
|
}
|
|
defer st.Close()
|
|
|
|
id, chain, err := findIdentity(machineCertificateSubject, st)
|
|
if err != nil {
|
|
return fmt.Errorf("find identity: %w", err)
|
|
}
|
|
defer id.Close()
|
|
|
|
signer, err := id.Signer()
|
|
if err != nil {
|
|
return fmt.Errorf("create signer: %w", err)
|
|
}
|
|
|
|
cl := 0
|
|
for _, c := range chain {
|
|
cl += len(c.Raw)
|
|
}
|
|
req.DeviceCert = make([]byte, 0, cl)
|
|
for _, c := range chain {
|
|
req.DeviceCert = append(req.DeviceCert, c.Raw...)
|
|
}
|
|
|
|
req.SignatureType = tailcfg.SignatureV2
|
|
h, err := HashRegisterRequest(req.SignatureType, req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey)
|
|
if err != nil {
|
|
return fmt.Errorf("hash: %w", err)
|
|
}
|
|
|
|
req.Signature, err = signer.Sign(nil, h, &rsa.PSSOptions{
|
|
SaltLength: rsa.PSSSaltLengthEqualsHash,
|
|
Hash: crypto.SHA256,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("sign: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|