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
@@ -121,7 +121,12 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !hasBasicUnderlying(ft) {
|
if !hasBasicUnderlying(ft) {
|
||||||
writef("dst.%s = *src.%s.Clone()", fname, fname)
|
// don't dereference if the underlying type is an interface
|
||||||
|
if _, isInterface := ft.Underlying().(*types.Interface); isInterface {
|
||||||
|
writef("if src.%s != nil { dst.%s = src.%s.Clone() }", fname, fname, fname)
|
||||||
|
} else {
|
||||||
|
writef("dst.%s = *src.%s.Clone()", fname, fname)
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,3 +59,52 @@ func TestSliceContainer(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestInterfaceContainer(t *testing.T) {
|
||||||
|
examples := []struct {
|
||||||
|
name string
|
||||||
|
in *clonerex.InterfaceContainer
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
in: nil,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "zero",
|
||||||
|
in: &clonerex.InterfaceContainer{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with_interface",
|
||||||
|
in: &clonerex.InterfaceContainer{
|
||||||
|
Interface: &clonerex.CloneableImpl{Value: 42},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "with_nil_interface",
|
||||||
|
in: &clonerex.InterfaceContainer{
|
||||||
|
Interface: nil,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ex := range examples {
|
||||||
|
t.Run(ex.name, func(t *testing.T) {
|
||||||
|
out := ex.in.Clone()
|
||||||
|
if !reflect.DeepEqual(ex.in, out) {
|
||||||
|
t.Errorf("Clone() = %v, want %v", out, ex.in)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify no aliasing: modifying the clone should not affect the original
|
||||||
|
if ex.in != nil && ex.in.Interface != nil {
|
||||||
|
if impl, ok := out.Interface.(*clonerex.CloneableImpl); ok {
|
||||||
|
impl.Value = 999
|
||||||
|
if origImpl, ok := ex.in.Interface.(*clonerex.CloneableImpl); ok {
|
||||||
|
if origImpl.Value == 999 {
|
||||||
|
t.Errorf("Clone() aliased memory with original")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) Tailscale Inc & AUTHORS
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
// SPDX-License-Identifier: BSD-3-Clause
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type SliceContainer
|
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type SliceContainer,InterfaceContainer
|
||||||
|
|
||||||
// Package clonerex is an example package for the cloner tool.
|
// Package clonerex is an example package for the cloner tool.
|
||||||
package clonerex
|
package clonerex
|
||||||
@@ -9,3 +9,26 @@ package clonerex
|
|||||||
type SliceContainer struct {
|
type SliceContainer struct {
|
||||||
Slice []*int
|
Slice []*int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cloneable is an interface with a Clone method.
|
||||||
|
type Cloneable interface {
|
||||||
|
Clone() Cloneable
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloneableImpl is a concrete type that implements Cloneable.
|
||||||
|
type CloneableImpl struct {
|
||||||
|
Value int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CloneableImpl) Clone() Cloneable {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &CloneableImpl{Value: c.Value}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InterfaceContainer has a pointer to an interface field, which tests
|
||||||
|
// the special handling for interface types in the cloner.
|
||||||
|
type InterfaceContainer struct {
|
||||||
|
Interface Cloneable
|
||||||
|
}
|
||||||
|
|||||||
@@ -35,9 +35,28 @@ var _SliceContainerCloneNeedsRegeneration = SliceContainer(struct {
|
|||||||
Slice []*int
|
Slice []*int
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
|
// Clone makes a deep copy of InterfaceContainer.
|
||||||
|
// The result aliases no memory with the original.
|
||||||
|
func (src *InterfaceContainer) Clone() *InterfaceContainer {
|
||||||
|
if src == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dst := new(InterfaceContainer)
|
||||||
|
*dst = *src
|
||||||
|
if src.Interface != nil {
|
||||||
|
dst.Interface = src.Interface.Clone()
|
||||||
|
}
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
|
var _InterfaceContainerCloneNeedsRegeneration = InterfaceContainer(struct {
|
||||||
|
Interface Cloneable
|
||||||
|
}{})
|
||||||
|
|
||||||
// Clone duplicates src into dst and reports whether it succeeded.
|
// Clone duplicates src into dst and reports whether it succeeded.
|
||||||
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
|
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
|
||||||
// where T is one of SliceContainer.
|
// where T is one of SliceContainer,InterfaceContainer.
|
||||||
func Clone(dst, src any) bool {
|
func Clone(dst, src any) bool {
|
||||||
switch src := src.(type) {
|
switch src := src.(type) {
|
||||||
case *SliceContainer:
|
case *SliceContainer:
|
||||||
@@ -49,6 +68,15 @@ func Clone(dst, src any) bool {
|
|||||||
*dst = src.Clone()
|
*dst = src.Clone()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
case *InterfaceContainer:
|
||||||
|
switch dst := dst.(type) {
|
||||||
|
case *InterfaceContainer:
|
||||||
|
*dst = *src.Clone()
|
||||||
|
return true
|
||||||
|
case **InterfaceContainer:
|
||||||
|
*dst = src.Clone()
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||||||
tailscale.com/types/logger from tailscale.com/cmd/derper+
|
tailscale.com/types/logger from tailscale.com/cmd/derper+
|
||||||
tailscale.com/types/netmap from tailscale.com/ipn
|
tailscale.com/types/netmap from tailscale.com/ipn
|
||||||
tailscale.com/types/opt from tailscale.com/envknob+
|
tailscale.com/types/opt from tailscale.com/envknob+
|
||||||
tailscale.com/types/persist from tailscale.com/ipn
|
tailscale.com/types/persist from tailscale.com/ipn+
|
||||||
tailscale.com/types/preftype from tailscale.com/ipn
|
tailscale.com/types/preftype from tailscale.com/ipn
|
||||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||||
tailscale.com/types/result from tailscale.com/util/lineiter
|
tailscale.com/types/result from tailscale.com/util/lineiter
|
||||||
|
|||||||
@@ -59,16 +59,17 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
|
|||||||
tailscale.com/net/stunserver from tailscale.com/cmd/stund
|
tailscale.com/net/stunserver from tailscale.com/cmd/stund
|
||||||
tailscale.com/net/tsaddr from tailscale.com/tsweb
|
tailscale.com/net/tsaddr from tailscale.com/tsweb
|
||||||
tailscale.com/syncs from tailscale.com/metrics+
|
tailscale.com/syncs from tailscale.com/metrics+
|
||||||
tailscale.com/tailcfg from tailscale.com/version
|
tailscale.com/tailcfg from tailscale.com/version+
|
||||||
tailscale.com/tsweb from tailscale.com/cmd/stund+
|
tailscale.com/tsweb from tailscale.com/cmd/stund+
|
||||||
tailscale.com/tsweb/promvarz from tailscale.com/cmd/stund
|
tailscale.com/tsweb/promvarz from tailscale.com/cmd/stund
|
||||||
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
tailscale.com/tsweb/varz from tailscale.com/tsweb+
|
||||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||||
tailscale.com/types/ipproto from tailscale.com/tailcfg
|
tailscale.com/types/ipproto from tailscale.com/tailcfg
|
||||||
tailscale.com/types/key from tailscale.com/tailcfg
|
tailscale.com/types/key from tailscale.com/tailcfg+
|
||||||
tailscale.com/types/lazy from tailscale.com/version+
|
tailscale.com/types/lazy from tailscale.com/version+
|
||||||
tailscale.com/types/logger from tailscale.com/tsweb+
|
tailscale.com/types/logger from tailscale.com/tsweb+
|
||||||
tailscale.com/types/opt from tailscale.com/envknob+
|
tailscale.com/types/opt from tailscale.com/envknob+
|
||||||
|
tailscale.com/types/persist from tailscale.com/feature
|
||||||
tailscale.com/types/ptr from tailscale.com/tailcfg+
|
tailscale.com/types/ptr from tailscale.com/tailcfg+
|
||||||
tailscale.com/types/result from tailscale.com/util/lineiter
|
tailscale.com/types/result from tailscale.com/util/lineiter
|
||||||
tailscale.com/types/structs from tailscale.com/tailcfg+
|
tailscale.com/types/structs from tailscale.com/tailcfg+
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||||||
tailscale.com/types/netmap from tailscale.com/ipn+
|
tailscale.com/types/netmap from tailscale.com/ipn+
|
||||||
tailscale.com/types/nettype from tailscale.com/net/netcheck+
|
tailscale.com/types/nettype from tailscale.com/net/netcheck+
|
||||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||||
tailscale.com/types/persist from tailscale.com/ipn
|
tailscale.com/types/persist from tailscale.com/ipn+
|
||||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||||
tailscale.com/types/result from tailscale.com/util/lineiter
|
tailscale.com/types/result from tailscale.com/util/lineiter
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import (
|
|||||||
"tailscale.com/syncs"
|
"tailscale.com/syncs"
|
||||||
"tailscale.com/tsd"
|
"tailscale.com/tsd"
|
||||||
"tailscale.com/types/flagtype"
|
"tailscale.com/types/flagtype"
|
||||||
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/logid"
|
"tailscale.com/types/logid"
|
||||||
"tailscale.com/util/osshare"
|
"tailscale.com/util/osshare"
|
||||||
@@ -111,19 +112,20 @@ var args struct {
|
|||||||
// or comma-separated list thereof.
|
// or comma-separated list thereof.
|
||||||
tunname string
|
tunname string
|
||||||
|
|
||||||
cleanUp bool
|
cleanUp bool
|
||||||
confFile string // empty, file path, or "vm:user-data"
|
confFile string // empty, file path, or "vm:user-data"
|
||||||
debug string
|
debug string
|
||||||
port uint16
|
port uint16
|
||||||
statepath string
|
statepath string
|
||||||
encryptState boolFlag
|
encryptState boolFlag
|
||||||
statedir string
|
statedir string
|
||||||
socketpath string
|
socketpath string
|
||||||
birdSocketPath string
|
birdSocketPath string
|
||||||
verbose int
|
verbose int
|
||||||
socksAddr string // listen address for SOCKS5 server
|
socksAddr string // listen address for SOCKS5 server
|
||||||
httpProxyAddr string // listen address for HTTP proxy server
|
httpProxyAddr string // listen address for HTTP proxy server
|
||||||
disableLogs bool
|
disableLogs bool
|
||||||
|
hardwareAttestation boolFlag
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -204,6 +206,9 @@ func main() {
|
|||||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||||
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
||||||
flag.StringVar(&args.confFile, "config", "", "path to config file, or 'vm:user-data' to use the VM's user-data (EC2)")
|
flag.StringVar(&args.confFile, "config", "", "path to config file, or 'vm:user-data' to use the VM's user-data (EC2)")
|
||||||
|
if buildfeatures.HasTPM {
|
||||||
|
flag.Var(&args.hardwareAttestation, "hardware-attestation", "use hardware-backed keys to bind node identity to this device when supported by the OS and hardware. Uses TPM 2.0 on Linux and Windows; SecureEnclave on macOS and iOS; and Keystore on Android")
|
||||||
|
}
|
||||||
if f, ok := hookRegisterOutboundProxyFlags.GetOk(); ok {
|
if f, ok := hookRegisterOutboundProxyFlags.GetOk(); ok {
|
||||||
f()
|
f()
|
||||||
}
|
}
|
||||||
@@ -667,6 +672,9 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
|
|||||||
log.Fatalf("failed to start netstack: %v", err)
|
log.Fatalf("failed to start netstack: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if buildfeatures.HasTPM && args.hardwareAttestation.v {
|
||||||
|
lb.SetHardwareAttested()
|
||||||
|
}
|
||||||
return lb, nil
|
return lb, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -879,9 +887,26 @@ func applyIntegrationTestEnvKnob() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleTPMFlags validates the --encrypt-state flag if set, and defaults
|
// handleTPMFlags validates the --encrypt-state and --hardware-attestation flags
|
||||||
// state encryption on if it's supported and compatible with other settings.
|
// if set, and defaults both to on if supported and compatible with other
|
||||||
|
// settings.
|
||||||
func handleTPMFlags() {
|
func handleTPMFlags() {
|
||||||
|
switch {
|
||||||
|
case args.hardwareAttestation.v:
|
||||||
|
if _, err := key.NewEmptyHardwareAttestationKey(); err == key.ErrUnsupported {
|
||||||
|
log.SetFlags(0)
|
||||||
|
log.Fatalf("--hardware-attestation is not supported on this platform or in this build of tailscaled")
|
||||||
|
}
|
||||||
|
case !args.hardwareAttestation.set:
|
||||||
|
policyHWAttestation, _ := policyclient.Get().GetBoolean(pkey.HardwareAttestation, feature.HardwareAttestationAvailable())
|
||||||
|
if !policyHWAttestation {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if feature.TPMAvailable() {
|
||||||
|
args.hardwareAttestation.v = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case args.encryptState.v:
|
case args.encryptState.v:
|
||||||
// Explicitly enabled, validate.
|
// Explicitly enabled, validate.
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"cmp"
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
@@ -604,6 +606,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
|||||||
if persist.NetworkLockKey.IsZero() {
|
if persist.NetworkLockKey.IsZero() {
|
||||||
persist.NetworkLockKey = key.NewNLPrivate()
|
persist.NetworkLockKey = key.NewNLPrivate()
|
||||||
}
|
}
|
||||||
|
|
||||||
nlPub := persist.NetworkLockKey.Public()
|
nlPub := persist.NetworkLockKey.Public()
|
||||||
|
|
||||||
if tryingNewKey.IsZero() {
|
if tryingNewKey.IsZero() {
|
||||||
@@ -944,6 +947,27 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
|
|||||||
TKAHead: tkaHead,
|
TKAHead: tkaHead,
|
||||||
ConnectionHandleForTest: connectionHandleForTest,
|
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
|
var extraDebugFlags []string
|
||||||
if buildfeatures.HasAdvertiseRoutes && hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
|
if buildfeatures.HasAdvertiseRoutes && hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
|
||||||
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
|
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ package feature
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/types/persist"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HookCanAutoUpdate is a hook for the clientupdate package
|
// HookCanAutoUpdate is a hook for the clientupdate package
|
||||||
@@ -45,6 +48,8 @@ var HookProxySetTransportGetProxyConnectHeader Hook[func(*http.Transport)]
|
|||||||
// and available.
|
// and available.
|
||||||
var HookTPMAvailable Hook[func() bool]
|
var HookTPMAvailable Hook[func() bool]
|
||||||
|
|
||||||
|
var HookGenerateAttestationKeyIfEmpty Hook[func(p *persist.Persist, logf logger.Logf) (bool, error)]
|
||||||
|
|
||||||
// TPMAvailable reports whether a TPM device is supported and available.
|
// TPMAvailable reports whether a TPM device is supported and available.
|
||||||
func TPMAvailable() bool {
|
func TPMAvailable() bool {
|
||||||
if f, ok := HookTPMAvailable.GetOk(); ok {
|
if f, ok := HookTPMAvailable.GetOk(); ok {
|
||||||
@@ -52,3 +57,17 @@ func TPMAvailable() bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HookHardwareAttestationAvailable is a hook that reports whether hardware
|
||||||
|
// attestation is supported and available.
|
||||||
|
var HookHardwareAttestationAvailable Hook[func() bool]
|
||||||
|
|
||||||
|
// HardwareAttestationAvailable reports whether hardware attestation is
|
||||||
|
// supported and available (TPM on Windows/Linux, Secure Enclave on macOS|iOS,
|
||||||
|
// KeyStore on Android)
|
||||||
|
func HardwareAttestationAvailable() bool {
|
||||||
|
if f, ok := HookHardwareAttestationAvailable.GetOk(); ok {
|
||||||
|
return f()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -142,13 +142,18 @@ type attestationKeySerialized struct {
|
|||||||
TPMPublic []byte `json:"tpmPublic"`
|
TPMPublic []byte `json:"tpmPublic"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON implements json.Marshaler.
|
||||||
func (ak *attestationKey) MarshalJSON() ([]byte, error) {
|
func (ak *attestationKey) MarshalJSON() ([]byte, error) {
|
||||||
|
if ak == nil || ak.IsZero() {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
return json.Marshal(attestationKeySerialized{
|
return json.Marshal(attestationKeySerialized{
|
||||||
TPMPublic: ak.tpmPublic.Bytes(),
|
TPMPublic: ak.tpmPublic.Bytes(),
|
||||||
TPMPrivate: ak.tpmPrivate.Buffer,
|
TPMPrivate: ak.tpmPrivate.Buffer,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON implements json.Unmarshaler.
|
||||||
func (ak *attestationKey) UnmarshalJSON(data []byte) (retErr error) {
|
func (ak *attestationKey) UnmarshalJSON(data []byte) (retErr error) {
|
||||||
var aks attestationKeySerialized
|
var aks attestationKeySerialized
|
||||||
if err := json.Unmarshal(data, &aks); err != nil {
|
if err := json.Unmarshal(data, &aks); err != nil {
|
||||||
@@ -254,6 +259,9 @@ func (ak *attestationKey) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ak *attestationKey) Clone() key.HardwareAttestationKey {
|
func (ak *attestationKey) Clone() key.HardwareAttestationKey {
|
||||||
|
if ak == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return &attestationKey{
|
return &attestationKey{
|
||||||
tpm: ak.tpm,
|
tpm: ak.tpm,
|
||||||
tpmPrivate: ak.tpmPrivate,
|
tpmPrivate: ak.tpmPrivate,
|
||||||
@@ -263,4 +271,9 @@ func (ak *attestationKey) Clone() key.HardwareAttestationKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ak *attestationKey) IsZero() bool { return !ak.loaded() }
|
func (ak *attestationKey) IsZero() bool {
|
||||||
|
if ak == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !ak.loaded()
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ var infoOnce = sync.OnceValue(info)
|
|||||||
func init() {
|
func init() {
|
||||||
feature.Register("tpm")
|
feature.Register("tpm")
|
||||||
feature.HookTPMAvailable.Set(tpmSupported)
|
feature.HookTPMAvailable.Set(tpmSupported)
|
||||||
|
feature.HookHardwareAttestationAvailable.Set(tpmSupported)
|
||||||
|
|
||||||
hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) {
|
hostinfo.RegisterHostinfoNewHook(func(hi *tailcfg.Hostinfo) {
|
||||||
hi.TPM = infoOnce()
|
hi.TPM = infoOnce()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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.
|
// See tailscale/corp#29969.
|
||||||
overrideExitNodePolicy bool
|
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.
|
// HealthTracker returns the health tracker for the backend.
|
||||||
@@ -2455,10 +2472,23 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
|||||||
if b.reconcilePrefsLocked(newPrefs) {
|
if b.reconcilePrefsLocked(newPrefs) {
|
||||||
prefsChanged = true
|
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 {
|
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 {
|
if err := b.pm.SetPrefs(newPrefs.View(), cn.NetworkProfile()); err != nil {
|
||||||
b.logf("failed to save updated and reconciled prefs: %v", err)
|
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()
|
discoPublic := b.MagicConn().DiscoPublicKey()
|
||||||
|
|
||||||
var err error
|
|
||||||
|
|
||||||
isNetstack := b.sys.IsNetstackRouter()
|
isNetstack := b.sys.IsNetstackRouter()
|
||||||
debugFlags := controlDebugFlags
|
debugFlags := controlDebugFlags
|
||||||
if isNetstack {
|
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) {
|
func TestDeps(t *testing.T) {
|
||||||
deptest.DepChecker{
|
deptest.DepChecker{
|
||||||
OnImport: func(pkg string) {
|
OnImport: func(pkg string) {
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import (
|
|||||||
"tailscale.com/ipn"
|
"tailscale.com/ipn"
|
||||||
"tailscale.com/ipn/ipnext"
|
"tailscale.com/ipn/ipnext"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
|
"tailscale.com/types/persist"
|
||||||
"tailscale.com/util/clientmetric"
|
"tailscale.com/util/clientmetric"
|
||||||
"tailscale.com/util/eventbus"
|
"tailscale.com/util/eventbus"
|
||||||
)
|
)
|
||||||
@@ -645,8 +647,8 @@ func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView)
|
|||||||
return pm.WriteState(k, []byte(profile.Key()))
|
return pm.WriteState(k, []byte(profile.Key()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
|
func (pm *profileManager) loadSavedPrefs(k ipn.StateKey) (ipn.PrefsView, error) {
|
||||||
bs, err := pm.store.ReadState(key)
|
bs, err := pm.store.ReadState(k)
|
||||||
if err == ipn.ErrStateNotExist || len(bs) == 0 {
|
if err == ipn.ErrStateNotExist || len(bs) == 0 {
|
||||||
return defaultPrefs, nil
|
return defaultPrefs, nil
|
||||||
}
|
}
|
||||||
@@ -654,10 +656,18 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
|
|||||||
return ipn.PrefsView{}, err
|
return ipn.PrefsView{}, err
|
||||||
}
|
}
|
||||||
savedPrefs := ipn.NewPrefs()
|
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 {
|
if err := ipn.PrefsFromBytes(bs, savedPrefs); err != nil {
|
||||||
return ipn.PrefsView{}, fmt.Errorf("parsing saved prefs: %v", err)
|
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
|
// Ignore any old stored preferences for https://login.tailscale.com
|
||||||
// as the control server that would override the new default of
|
// as the control server that would override the new default of
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ func TestProfileDupe(t *testing.T) {
|
|||||||
ID: tailcfg.UserID(user),
|
ID: tailcfg.UserID(user),
|
||||||
LoginName: fmt.Sprintf("user%d@example.com", user),
|
LoginName: fmt.Sprintf("user%d@example.com", user),
|
||||||
},
|
},
|
||||||
|
AttestationKey: nil,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user1Node1 := newPersist(1, 1)
|
user1Node1 := newPersist(1, 1)
|
||||||
|
|||||||
@@ -709,6 +709,7 @@ func NewPrefs() *Prefs {
|
|||||||
// Provide default values for options which might be missing
|
// Provide default values for options which might be missing
|
||||||
// from the json data for any reason. The json can still
|
// from the json data for any reason. The json can still
|
||||||
// override them to false.
|
// override them to false.
|
||||||
|
|
||||||
p := &Prefs{
|
p := &Prefs{
|
||||||
// ControlURL is explicitly not set to signal that
|
// ControlURL is explicitly not set to signal that
|
||||||
// it's not yet configured, which relaxes the CLI "up"
|
// it's not yet configured, which relaxes the CLI "up"
|
||||||
|
|||||||
+1
-1
@@ -501,7 +501,7 @@ func TestPrefsPretty(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"linux",
|
"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{
|
Prefs{
|
||||||
|
|||||||
+8
-3
@@ -176,7 +176,8 @@ type CapabilityVersion int
|
|||||||
// - 127: 2025-09-19: can handle C2N /debug/netmap.
|
// - 127: 2025-09-19: can handle C2N /debug/netmap.
|
||||||
// - 128: 2025-10-02: can handle C2N /debug/health.
|
// - 128: 2025-10-02: can handle C2N /debug/health.
|
||||||
// - 129: 2025-10-04: Fixed sleep/wake deadlock in magicsock when using peer relay (PR #17449)
|
// - 129: 2025-10-04: Fixed sleep/wake deadlock in magicsock when using peer relay (PR #17449)
|
||||||
const CurrentCapabilityVersion CapabilityVersion = 129
|
// - 130: 2025-10-06: client can send key.HardwareAttestationPublic and key.HardwareAttestationKeySignature in MapRequest
|
||||||
|
const CurrentCapabilityVersion CapabilityVersion = 130
|
||||||
|
|
||||||
// ID is an integer ID for a user, node, or login allocated by the
|
// ID is an integer ID for a user, node, or login allocated by the
|
||||||
// control plane.
|
// control plane.
|
||||||
@@ -1372,9 +1373,13 @@ type MapRequest struct {
|
|||||||
// HardwareAttestationKey is the public key of the node's hardware-backed
|
// HardwareAttestationKey is the public key of the node's hardware-backed
|
||||||
// identity attestation key, if any.
|
// identity attestation key, if any.
|
||||||
HardwareAttestationKey key.HardwareAttestationPublic `json:",omitzero"`
|
HardwareAttestationKey key.HardwareAttestationPublic `json:",omitzero"`
|
||||||
// HardwareAttestationKeySignature is the signature of the NodeKey
|
// HardwareAttestationKeySignature is the signature of
|
||||||
// serialized using MarshalText using its hardware attestation key, if any.
|
// "$UNIX_TIMESTAMP|$NODE_KEY" using its hardware attestation key, if any.
|
||||||
HardwareAttestationKeySignature []byte `json:",omitempty"`
|
HardwareAttestationKeySignature []byte `json:",omitempty"`
|
||||||
|
// HardwareAttestationKeySignatureTimestamp is the time at which the
|
||||||
|
// HardwareAttestationKeySignature was created, if any. This UNIX timestamp
|
||||||
|
// value is prepended to the node key when signing.
|
||||||
|
HardwareAttestationKeySignatureTimestamp time.Time `json:",omitzero"`
|
||||||
|
|
||||||
// Stream is whether the client wants to receive multiple MapResponses over
|
// Stream is whether the client wants to receive multiple MapResponses over
|
||||||
// the same HTTP connection.
|
// the same HTTP connection.
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type Persist struct {
|
|||||||
UserProfile tailcfg.UserProfile
|
UserProfile tailcfg.UserProfile
|
||||||
NetworkLockKey key.NLPrivate
|
NetworkLockKey key.NLPrivate
|
||||||
NodeID tailcfg.StableNodeID
|
NodeID tailcfg.StableNodeID
|
||||||
|
AttestationKey key.HardwareAttestationKey `json:",omitempty"`
|
||||||
|
|
||||||
// DisallowedTKAStateIDs stores the tka.State.StateID values which
|
// DisallowedTKAStateIDs stores the tka.State.StateID values which
|
||||||
// this node will not operate network lock on. This is used to
|
// 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
|
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) &&
|
return p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
|
||||||
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
||||||
p.UserProfile.Equal(&p2.UserProfile) &&
|
p.UserProfile.Equal(&p2.UserProfile) &&
|
||||||
p.NetworkLockKey.Equal(p2.NetworkLockKey) &&
|
p.NetworkLockKey.Equal(p2.NetworkLockKey) &&
|
||||||
p.NodeID == p2.NodeID &&
|
p.NodeID == p2.NodeID &&
|
||||||
|
pub.Equal(p2Pub) &&
|
||||||
reflect.DeepEqual(nilIfEmpty(p.DisallowedTKAStateIDs), nilIfEmpty(p2.DisallowedTKAStateIDs))
|
reflect.DeepEqual(nilIfEmpty(p.DisallowedTKAStateIDs), nilIfEmpty(p2.DisallowedTKAStateIDs))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +106,16 @@ func (p *Persist) Pretty() string {
|
|||||||
var (
|
var (
|
||||||
ok, nk key.NodePublic
|
ok, nk key.NodePublic
|
||||||
)
|
)
|
||||||
|
akString := "-"
|
||||||
if !p.OldPrivateNodeKey.IsZero() {
|
if !p.OldPrivateNodeKey.IsZero() {
|
||||||
ok = p.OldPrivateNodeKey.Public()
|
ok = p.OldPrivateNodeKey.Public()
|
||||||
}
|
}
|
||||||
if !p.PrivateNodeKey.IsZero() {
|
if !p.PrivateNodeKey.IsZero() {
|
||||||
nk = p.PublicNodeKey()
|
nk = p.PublicNodeKey()
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("Persist{o=%v, n=%v u=%#v}",
|
if p.AttestationKey != nil && !p.AttestationKey.IsZero() {
|
||||||
ok.ShortString(), nk.ShortString(), p.UserProfile.LoginName)
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ func (src *Persist) Clone() *Persist {
|
|||||||
}
|
}
|
||||||
dst := new(Persist)
|
dst := new(Persist)
|
||||||
*dst = *src
|
*dst = *src
|
||||||
|
if src.AttestationKey != nil {
|
||||||
|
dst.AttestationKey = src.AttestationKey.Clone()
|
||||||
|
}
|
||||||
dst.DisallowedTKAStateIDs = append(src.DisallowedTKAStateIDs[:0:0], src.DisallowedTKAStateIDs...)
|
dst.DisallowedTKAStateIDs = append(src.DisallowedTKAStateIDs[:0:0], src.DisallowedTKAStateIDs...)
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
@@ -31,5 +34,6 @@ var _PersistCloneNeedsRegeneration = Persist(struct {
|
|||||||
UserProfile tailcfg.UserProfile
|
UserProfile tailcfg.UserProfile
|
||||||
NetworkLockKey key.NLPrivate
|
NetworkLockKey key.NLPrivate
|
||||||
NodeID tailcfg.StableNodeID
|
NodeID tailcfg.StableNodeID
|
||||||
|
AttestationKey key.HardwareAttestationKey
|
||||||
DisallowedTKAStateIDs []string
|
DisallowedTKAStateIDs []string
|
||||||
}{})
|
}{})
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPersistEqual(t *testing.T) {
|
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) {
|
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",
|
t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||||
have, persistHandles)
|
have, persistHandles)
|
||||||
|
|||||||
@@ -89,10 +89,11 @@ func (v *PersistView) UnmarshalJSONFrom(dec *jsontext.Decoder) error {
|
|||||||
func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey }
|
func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey }
|
||||||
|
|
||||||
// needed to request key rotation
|
// needed to request key rotation
|
||||||
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
|
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
|
||||||
func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
|
func (v PersistView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
|
||||||
func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey }
|
func (v PersistView) NetworkLockKey() key.NLPrivate { return v.ж.NetworkLockKey }
|
||||||
func (v PersistView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
|
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
|
// DisallowedTKAStateIDs stores the tka.State.StateID values which
|
||||||
// this node will not operate network lock on. This is used to
|
// this node will not operate network lock on. This is used to
|
||||||
@@ -110,5 +111,6 @@ var _PersistViewNeedsRegeneration = Persist(struct {
|
|||||||
UserProfile tailcfg.UserProfile
|
UserProfile tailcfg.UserProfile
|
||||||
NetworkLockKey key.NLPrivate
|
NetworkLockKey key.NLPrivate
|
||||||
NodeID tailcfg.StableNodeID
|
NodeID tailcfg.StableNodeID
|
||||||
|
AttestationKey key.HardwareAttestationKey
|
||||||
DisallowedTKAStateIDs []string
|
DisallowedTKAStateIDs []string
|
||||||
}{})
|
}{})
|
||||||
|
|||||||
@@ -141,6 +141,10 @@ const (
|
|||||||
// It's a noop on other platforms.
|
// It's a noop on other platforms.
|
||||||
EncryptState Key = "EncryptState"
|
EncryptState Key = "EncryptState"
|
||||||
|
|
||||||
|
// HardwareAttestation is a boolean key that controls whether to use a
|
||||||
|
// hardware-backed key to bind the node identity to this device.
|
||||||
|
HardwareAttestation Key = "HardwareAttestation"
|
||||||
|
|
||||||
// PostureChecking indicates if posture checking is enabled and the client shall gather
|
// PostureChecking indicates if posture checking is enabled and the client shall gather
|
||||||
// posture data.
|
// posture data.
|
||||||
// Key is a string value that specifies an option: "always", "never", "user-decides".
|
// Key is a string value that specifies an option: "always", "never", "user-decides".
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ var implicitDefinitions = []*setting.Definition{
|
|||||||
setting.NewDefinition(pkey.PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
|
setting.NewDefinition(pkey.PostureChecking, setting.DeviceSetting, setting.PreferenceOptionValue),
|
||||||
setting.NewDefinition(pkey.ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
|
setting.NewDefinition(pkey.ReconnectAfter, setting.DeviceSetting, setting.DurationValue),
|
||||||
setting.NewDefinition(pkey.Tailnet, setting.DeviceSetting, setting.StringValue),
|
setting.NewDefinition(pkey.Tailnet, setting.DeviceSetting, setting.StringValue),
|
||||||
|
setting.NewDefinition(pkey.HardwareAttestation, setting.DeviceSetting, setting.BooleanValue),
|
||||||
|
|
||||||
// User policy settings (can be configured on a user- or device-basis):
|
// User policy settings (can be configured on a user- or device-basis):
|
||||||
setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
setting.NewDefinition(pkey.AdminConsoleVisibility, setting.UserSetting, setting.VisibilityValue),
|
||||||
|
|||||||
Reference in New Issue
Block a user