Browse Source

Revert "control/controlclient: back out HW key attestation (#17664)" (#17732)

This reverts commit a760cbe33f4bed64b63c6118808d02b2771ff785.

Signed-off-by: Andrew Lytvynov <[email protected]>
Andrew Lytvynov 4 months ago
parent
commit
db7dcd516f

+ 22 - 0
control/controlclient/direct.go

@@ -7,6 +7,8 @@ import (
 	"bytes"
 	"cmp"
 	"context"
+	"crypto"
+	"crypto/sha256"
 	"encoding/binary"
 	"encoding/json"
 	"errors"
@@ -946,6 +948,26 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
 		ConnectionHandleForTest: connectionHandleForTest,
 	}
 
+	// If we have a hardware attestation key, sign the node key with it and send
+	// the key & signature in the map request.
+	if buildfeatures.HasTPM {
+		if k := persist.AsStruct().AttestationKey; k != nil && !k.IsZero() {
+			hwPub := key.HardwareAttestationPublicFromPlatformKey(k)
+			request.HardwareAttestationKey = hwPub
+
+			t := c.clock.Now()
+			msg := fmt.Sprintf("%d|%s", t.Unix(), nodeKey.String())
+			digest := sha256.Sum256([]byte(msg))
+			sig, err := k.Sign(nil, digest[:], crypto.SHA256)
+			if err != nil {
+				c.logf("failed to sign node key with hardware attestation key: %v", err)
+			} else {
+				request.HardwareAttestationKeySignature = sig
+				request.HardwareAttestationKeySignatureTimestamp = t
+			}
+		}
+	}
+
 	var extraDebugFlags []string
 	if buildfeatures.HasAdvertiseRoutes && hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
 		ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {

+ 48 - 0
ipn/ipnlocal/hwattest.go

@@ -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
+}

+ 1 - 0
ipn/ipnlocal/local.go

@@ -1190,6 +1190,7 @@ func stripKeysFromPrefs(p ipn.PrefsView) ipn.PrefsView {
 	p2.Persist.PrivateNodeKey = key.NodePrivate{}
 	p2.Persist.OldPrivateNodeKey = key.NodePrivate{}
 	p2.Persist.NetworkLockKey = key.NLPrivate{}
+	p2.Persist.AttestationKey = nil
 	return p2.View()
 }
 

+ 10 - 0
ipn/ipnlocal/profiles.go

@@ -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"
 )
@@ -654,6 +656,14 @@ func (pm *profileManager) loadSavedPrefs(k 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)
 	}

+ 1 - 0
ipn/ipnlocal/profiles_test.go

@@ -151,6 +151,7 @@ func TestProfileDupe(t *testing.T) {
 				ID:        tailcfg.UserID(user),
 				LoginName: fmt.Sprintf("user%[email protected]", user),
 			},
+			AttestationKey: nil,
 		}
 	}
 	user1Node1 := newPersist(1, 1)

+ 1 - 1
ipn/prefs_test.go

@@ -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{

+ 16 - 2
types/persist/persist.go

@@ -26,6 +26,7 @@ type Persist struct {
 	UserProfile       tailcfg.UserProfile
 	NetworkLockKey    key.NLPrivate
 	NodeID            tailcfg.StableNodeID
+	AttestationKey    key.HardwareAttestationKey `json:",omitempty"`
 
 	// DisallowedTKAStateIDs stores the tka.State.StateID values which
 	// this node will not operate network lock on. This is used to
@@ -84,11 +85,20 @@ func (p *Persist) Equals(p2 *Persist) bool {
 		return false
 	}
 
+	var pub, p2Pub key.HardwareAttestationPublic
+	if p.AttestationKey != nil && !p.AttestationKey.IsZero() {
+		pub = key.HardwareAttestationPublicFromPlatformKey(p.AttestationKey)
+	}
+	if p2.AttestationKey != nil && !p2.AttestationKey.IsZero() {
+		p2Pub = key.HardwareAttestationPublicFromPlatformKey(p2.AttestationKey)
+	}
+
 	return p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
 		p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
 		p.UserProfile.Equal(&p2.UserProfile) &&
 		p.NetworkLockKey.Equal(p2.NetworkLockKey) &&
 		p.NodeID == p2.NodeID &&
+		pub.Equal(p2Pub) &&
 		reflect.DeepEqual(nilIfEmpty(p.DisallowedTKAStateIDs), nilIfEmpty(p2.DisallowedTKAStateIDs))
 }
 
@@ -96,12 +106,16 @@ func (p *Persist) Pretty() string {
 	var (
 		ok, nk key.NodePublic
 	)
+	akString := "-"
 	if !p.OldPrivateNodeKey.IsZero() {
 		ok = p.OldPrivateNodeKey.Public()
 	}
 	if !p.PrivateNodeKey.IsZero() {
 		nk = p.PublicNodeKey()
 	}
-	return fmt.Sprintf("Persist{o=%v, n=%v u=%#v}",
-		ok.ShortString(), nk.ShortString(), p.UserProfile.LoginName)
+	if p.AttestationKey != nil && !p.AttestationKey.IsZero() {
+		akString = fmt.Sprintf("%v", p.AttestationKey.Public())
+	}
+	return fmt.Sprintf("Persist{o=%v, n=%v u=%#v ak=%s}",
+		ok.ShortString(), nk.ShortString(), p.UserProfile.LoginName, akString)
 }

+ 4 - 0
types/persist/persist_clone.go

@@ -19,6 +19,9 @@ func (src *Persist) Clone() *Persist {
 	}
 	dst := new(Persist)
 	*dst = *src
+	if src.AttestationKey != nil {
+		dst.AttestationKey = src.AttestationKey.Clone()
+	}
 	dst.DisallowedTKAStateIDs = append(src.DisallowedTKAStateIDs[:0:0], src.DisallowedTKAStateIDs...)
 	return dst
 }
@@ -31,5 +34,6 @@ var _PersistCloneNeedsRegeneration = Persist(struct {
 	UserProfile           tailcfg.UserProfile
 	NetworkLockKey        key.NLPrivate
 	NodeID                tailcfg.StableNodeID
+	AttestationKey        key.HardwareAttestationKey
 	DisallowedTKAStateIDs []string
 }{})

+ 1 - 1
types/persist/persist_test.go

@@ -21,7 +21,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
 }
 
 func TestPersistEqual(t *testing.T) {
-	persistHandles := []string{"PrivateNodeKey", "OldPrivateNodeKey", "UserProfile", "NetworkLockKey", "NodeID", "DisallowedTKAStateIDs"}
+	persistHandles := []string{"PrivateNodeKey", "OldPrivateNodeKey", "UserProfile", "NetworkLockKey", "NodeID", "AttestationKey", "DisallowedTKAStateIDs"}
 	if have := fieldsOf(reflect.TypeFor[Persist]()); !reflect.DeepEqual(have, persistHandles) {
 		t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
 			have, persistHandles)

+ 6 - 4
types/persist/persist_view.go

@@ -89,10 +89,11 @@ func (v *PersistView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
 func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey }
 
 // needed to request key rotation
-func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
-func (v PersistView) UserProfile() tailcfg.UserProfile   { return v.ж.UserProfile }
-func (v PersistView) NetworkLockKey() key.NLPrivate      { return v.ж.NetworkLockKey }
-func (v PersistView) NodeID() tailcfg.StableNodeID       { return v.ж.NodeID }
+func (v PersistView) OldPrivateNodeKey() key.NodePrivate   { return v.ж.OldPrivateNodeKey }
+func (v PersistView) UserProfile() tailcfg.UserProfile     { return v.ж.UserProfile }
+func (v PersistView) NetworkLockKey() key.NLPrivate        { return v.ж.NetworkLockKey }
+func (v PersistView) NodeID() tailcfg.StableNodeID         { return v.ж.NodeID }
+func (v PersistView) AttestationKey() tailcfg.StableNodeID { panic("unsupported") }
 
 // DisallowedTKAStateIDs stores the tka.State.StateID values which
 // this node will not operate network lock on. This is used to
@@ -110,5 +111,6 @@ var _PersistViewNeedsRegeneration = Persist(struct {
 	UserProfile           tailcfg.UserProfile
 	NetworkLockKey        key.NLPrivate
 	NodeID                tailcfg.StableNodeID
+	AttestationKey        key.HardwareAttestationKey
 	DisallowedTKAStateIDs []string
 }{})