types/persist: add AttestationKey (#17281)
Extend Persist with AttestationKey to record a hardware-backed attestation key for the node's identity. Add a flag to tailscaled to allow users to control the use of hardware-backed keys to bind node identity to individual machines. Updates tailscale/corp#31269 Change-Id: Idcf40d730a448d85f07f1bebf387f086d4c58be3 Signed-off-by: Patrick O'Doherty <patrick@tailscale.com>
This commit is contained in:
committed by
GitHub
parent
a2dc517d7d
commit
e45557afc0
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tpm
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/feature"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/persist"
|
||||
)
|
||||
|
||||
func init() {
|
||||
feature.HookGenerateAttestationKeyIfEmpty.Set(generateAttestationKeyIfEmpty)
|
||||
}
|
||||
|
||||
// generateAttestationKeyIfEmpty generates a new hardware attestation key if
|
||||
// none exists. It returns true if a new key was generated and stored in
|
||||
// p.AttestationKey.
|
||||
func generateAttestationKeyIfEmpty(p *persist.Persist, logf logger.Logf) (bool, error) {
|
||||
// attempt to generate a new hardware attestation key if none exists
|
||||
var ak key.HardwareAttestationKey
|
||||
if p != nil {
|
||||
ak = p.AttestationKey
|
||||
}
|
||||
|
||||
if ak == nil || ak.IsZero() {
|
||||
var err error
|
||||
ak, err = key.NewHardwareAttestationKey()
|
||||
if err != nil {
|
||||
if !errors.Is(err, key.ErrUnsupported) {
|
||||
logf("failed to create hardware attestation key: %v", err)
|
||||
}
|
||||
} else if ak != nil {
|
||||
logf("using new hardware attestation key: %v", ak.Public())
|
||||
if p == nil {
|
||||
p = &persist.Persist{}
|
||||
}
|
||||
p.AttestationKey = ak
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
+33
-5
@@ -392,6 +392,23 @@ type LocalBackend struct {
|
||||
//
|
||||
// See tailscale/corp#29969.
|
||||
overrideExitNodePolicy bool
|
||||
|
||||
// hardwareAttested is whether backend should use a hardware-backed key to
|
||||
// bind the node identity to this device.
|
||||
hardwareAttested atomic.Bool
|
||||
}
|
||||
|
||||
// SetHardwareAttested enables hardware attestation key signatures in map
|
||||
// requests, if supported on this platform. SetHardwareAttested should be called
|
||||
// before Start.
|
||||
func (b *LocalBackend) SetHardwareAttested() {
|
||||
b.hardwareAttested.Store(true)
|
||||
}
|
||||
|
||||
// HardwareAttested reports whether hardware-backed attestation keys should be
|
||||
// used to bind the node's identity to this device.
|
||||
func (b *LocalBackend) HardwareAttested() bool {
|
||||
return b.hardwareAttested.Load()
|
||||
}
|
||||
|
||||
// HealthTracker returns the health tracker for the backend.
|
||||
@@ -2455,10 +2472,23 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
if b.reconcilePrefsLocked(newPrefs) {
|
||||
prefsChanged = true
|
||||
}
|
||||
|
||||
// neither UpdatePrefs or reconciliation should change Persist
|
||||
newPrefs.Persist = b.pm.CurrentPrefs().Persist().AsStruct()
|
||||
|
||||
if buildfeatures.HasTPM {
|
||||
if genKey, ok := feature.HookGenerateAttestationKeyIfEmpty.GetOk(); ok {
|
||||
newKey, err := genKey(newPrefs.Persist, b.logf)
|
||||
if err != nil {
|
||||
b.logf("failed to populate attestation key from TPM: %v", err)
|
||||
}
|
||||
if newKey {
|
||||
prefsChanged = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if prefsChanged {
|
||||
// Neither opts.UpdatePrefs nor prefs reconciliation
|
||||
// is allowed to modify Persist; retain the old value.
|
||||
newPrefs.Persist = b.pm.CurrentPrefs().Persist().AsStruct()
|
||||
if err := b.pm.SetPrefs(newPrefs.View(), cn.NetworkProfile()); err != nil {
|
||||
b.logf("failed to save updated and reconciled prefs: %v", err)
|
||||
}
|
||||
@@ -2491,8 +2521,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
|
||||
discoPublic := b.MagicConn().DiscoPublicKey()
|
||||
|
||||
var err error
|
||||
|
||||
isNetstack := b.sys.IsNetstackRouter()
|
||||
debugFlags := controlDebugFlags
|
||||
if isNetstack {
|
||||
|
||||
@@ -7030,6 +7030,27 @@ func TestDisplayMessageIPNBus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHardwareAttested(t *testing.T) {
|
||||
b := new(LocalBackend)
|
||||
|
||||
// default false
|
||||
if got := b.HardwareAttested(); got != false {
|
||||
t.Errorf("HardwareAttested() = %v, want false", got)
|
||||
}
|
||||
|
||||
// set true
|
||||
b.SetHardwareAttested()
|
||||
if got := b.HardwareAttested(); got != true {
|
||||
t.Errorf("HardwareAttested() = %v, want true after SetHardwareAttested()", got)
|
||||
}
|
||||
|
||||
// repeat calls are safe; still true
|
||||
b.SetHardwareAttested()
|
||||
if got := b.HardwareAttested(); got != true {
|
||||
t.Errorf("HardwareAttested() = %v, want true after second SetHardwareAttested()", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
OnImport: func(pkg string) {
|
||||
|
||||
@@ -19,7 +19,9 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnext"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/eventbus"
|
||||
)
|
||||
@@ -645,8 +647,8 @@ func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView)
|
||||
return pm.WriteState(k, []byte(profile.Key()))
|
||||
}
|
||||
|
||||
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
|
||||
bs, err := pm.store.ReadState(key)
|
||||
func (pm *profileManager) loadSavedPrefs(k ipn.StateKey) (ipn.PrefsView, error) {
|
||||
bs, err := pm.store.ReadState(k)
|
||||
if err == ipn.ErrStateNotExist || len(bs) == 0 {
|
||||
return defaultPrefs, nil
|
||||
}
|
||||
@@ -654,10 +656,18 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
|
||||
return ipn.PrefsView{}, err
|
||||
}
|
||||
savedPrefs := ipn.NewPrefs()
|
||||
|
||||
// if supported by the platform, create an empty hardware attestation key to use when deserializing
|
||||
// to avoid type exceptions from json.Unmarshaling into an interface{}.
|
||||
hw, _ := key.NewEmptyHardwareAttestationKey()
|
||||
savedPrefs.Persist = &persist.Persist{
|
||||
AttestationKey: hw,
|
||||
}
|
||||
|
||||
if err := ipn.PrefsFromBytes(bs, savedPrefs); err != nil {
|
||||
return ipn.PrefsView{}, fmt.Errorf("parsing saved prefs: %v", err)
|
||||
}
|
||||
pm.logf("using backend prefs for %q: %v", key, savedPrefs.Pretty())
|
||||
pm.logf("using backend prefs for %q: %v", k, savedPrefs.Pretty())
|
||||
|
||||
// Ignore any old stored preferences for https://login.tailscale.com
|
||||
// as the control server that would override the new default of
|
||||
|
||||
@@ -151,6 +151,7 @@ func TestProfileDupe(t *testing.T) {
|
||||
ID: tailcfg.UserID(user),
|
||||
LoginName: fmt.Sprintf("user%d@example.com", user),
|
||||
},
|
||||
AttestationKey: nil,
|
||||
}
|
||||
}
|
||||
user1Node1 := newPersist(1, 1)
|
||||
|
||||
@@ -709,6 +709,7 @@ func NewPrefs() *Prefs {
|
||||
// Provide default values for options which might be missing
|
||||
// from the json data for any reason. The json can still
|
||||
// override them to false.
|
||||
|
||||
p := &Prefs{
|
||||
// ControlURL is explicitly not set to signal that
|
||||
// it's not yet configured, which relaxes the CLI "up"
|
||||
|
||||
+1
-1
@@ -501,7 +501,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{o=, n=[B1VKl] u=""}}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{o=, n=[B1VKl] u="" ak=-}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
|
||||
Reference in New Issue
Block a user