cmd,ipn/ipnlocal,tailcfg: implement TKA disablement

* Plumb disablement values through some of the internals of TKA enablement.
 * Transmit the node's TKA hash at the end of sync so the control plane understands each node's head.
 * Implement /machine/tka/disable RPC to actuate disablement on the control plane.

There is a partner PR for the control server I'll send shortly.

Signed-off-by: Tom DNetto <tom@tailscale.com>
This commit is contained in:
Tom DNetto 2022-10-27 13:40:31 -07:00 committed by Tom
parent 3d8eda5b72
commit d98305c537
12 changed files with 322 additions and 38 deletions

View File

@ -778,13 +778,16 @@ func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.Network
} }
// NetworkLockInit initializes the tailnet key authority. // NetworkLockInit initializes the tailnet key authority.
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ipnstate.NetworkLockStatus, error) { //
// TODO(tom): Plumb through disablement secrets.
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte) (*ipnstate.NetworkLockStatus, error) {
var b bytes.Buffer var b bytes.Buffer
type initRequest struct { type initRequest struct {
Keys []tka.Key Keys []tka.Key
DisablementValues [][]byte
} }
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys}); err != nil { if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues}); err != nil {
return nil, err return nil, err
} }

View File

@ -5,6 +5,7 @@
package cli package cli
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"fmt" "fmt"
@ -51,7 +52,10 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
return err return err
} }
status, err := localClient.NetworkLockInit(ctx, keys) // TODO(tom): Implement specification of disablement values from the command line.
disablementValues := [][]byte{bytes.Repeat([]byte{0xa5}, 32)}
status, err := localClient.NetworkLockInit(ctx, keys, disablementValues)
if err != nil { if err != nil {
return err return err
} }

View File

@ -561,6 +561,11 @@ func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
c.sendNewMapRequest() c.sendNewMapRequest()
} }
// SetTKAHead updates the TKA head hash that map-request infrastructure sends.
func (c *Auto) SetTKAHead(headHash string) {
c.direct.SetTKAHead(headHash)
}
func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) { func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
c.mu.Lock() c.mu.Lock()
if c.closed { if c.closed {

View File

@ -65,6 +65,9 @@ type Client interface {
// in a separate http request. It has nothing to do with the rest of // in a separate http request. It has nothing to do with the rest of
// the state machine. // the state machine.
SetNetInfo(*tailcfg.NetInfo) SetNetInfo(*tailcfg.NetInfo)
// SetTKAHead changes the TKA head hash value that will be sent in
// subsequent netmap requests.
SetTKAHead(headHash string)
// UpdateEndpoints changes the Endpoint structure that will be sent // UpdateEndpoints changes the Endpoint structure that will be sent
// in subsequent node registration requests. // in subsequent node registration requests.
// TODO: a server-side change would let us simply upload this // TODO: a server-side change would let us simply upload this

View File

@ -94,6 +94,7 @@ type Direct struct {
hostinfo *tailcfg.Hostinfo // always non-nil hostinfo *tailcfg.Hostinfo // always non-nil
netinfo *tailcfg.NetInfo netinfo *tailcfg.NetInfo
endpoints []tailcfg.Endpoint endpoints []tailcfg.Endpoint
tkaHead string
everEndpoints bool // whether we've ever had non-empty endpoints everEndpoints bool // whether we've ever had non-empty endpoints
lastPingURL string // last PingRequest.URL received, for dup suppression lastPingURL string // last PingRequest.URL received, for dup suppression
} }
@ -317,6 +318,21 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
return true return true
} }
// SetNetInfo stores a new TKA head value for next update.
// It reports whether the TKA head changed.
func (c *Direct) SetTKAHead(tkaHead string) bool {
c.mu.Lock()
defer c.mu.Unlock()
if tkaHead == c.tkaHead {
return false
}
c.tkaHead = tkaHead
c.logf("tkaHead: %v", tkaHead)
return true
}
func (c *Direct) GetPersist() persist.Persist { func (c *Direct) GetPersist() persist.Persist {
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
@ -829,6 +845,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
Hostinfo: hi, Hostinfo: hi,
DebugFlags: c.debugFlags, DebugFlags: c.debugFlags,
OmitPeers: cb == nil, OmitPeers: cb == nil,
TKAHead: c.tkaHead,
// On initial startup before we know our endpoints, set the ReadOnly flag // On initial startup before we know our endpoints, set the ReadOnly flag
// to tell the control server not to distribute out our (empty) endpoints to peers. // to tell the control server not to distribute out our (empty) endpoints to peers.

View File

@ -831,6 +831,16 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.logf("[v1] TKA sync error: %v", err) b.logf("[v1] TKA sync error: %v", err)
} }
b.mu.Lock() b.mu.Lock()
if b.tka != nil {
head, err := b.tka.authority.Head().MarshalText()
if err != nil {
b.logf("[v1] error marshalling tka head: %v", err)
} else {
b.cc.SetTKAHead(string(head))
}
} else {
b.cc.SetTKAHead("")
}
if !envknob.TKASkipSignatureCheck() { if !envknob.TKASkipSignatureCheck() {
b.tkaFilterNetmapLocked(st.NetMap) b.tkaFilterNetmapLocked(st.NetMap)
@ -1226,11 +1236,21 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
b.cc = cc b.cc = cc
b.ccAuto, _ = cc.(*controlclient.Auto) b.ccAuto, _ = cc.(*controlclient.Auto)
endpoints := b.endpoints endpoints := b.endpoints
var tkaHead string
if b.tka != nil {
head, err := b.tka.authority.Head().MarshalText()
if err != nil {
b.mu.Unlock()
return fmt.Errorf("marshalling tka head: %w", err)
}
tkaHead = string(head)
}
b.mu.Unlock() b.mu.Unlock()
if endpoints != nil { if endpoints != nil {
cc.UpdateEndpoints(endpoints) cc.UpdateEndpoints(endpoints)
} }
cc.SetTKAHead(tkaHead)
b.e.SetNetInfoCallback(b.setNetInfo) b.e.SetNetInfoCallback(b.setNetInfo)

View File

@ -95,6 +95,8 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap) error {
return nil return nil
} }
b.logf("tkaSyncIfNeeded: enabled=%v, head=%v", nm.TKAEnabled, nm.TKAHead)
b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section. b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section.
defer b.tkaSyncLock.Unlock() defer b.tkaSyncLock.Unlock()
b.mu.Lock() // take mu to protect access to synchronized fields. b.mu.Lock() // take mu to protect access to synchronized fields.
@ -125,15 +127,13 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap) error {
} }
isEnabled = true isEnabled = true
} else if !wantEnabled && isEnabled { } else if !wantEnabled && isEnabled {
if b.tka.authority.ValidDisablement(bs.DisablementSecret) { if err := b.tkaApplyDisablementLocked(bs.DisablementSecret); err != nil {
b.tka = nil // We log here instead of returning an error (which itself would be
isEnabled = false // logged), so that sync will continue even if control gives us an
// incorrect disablement secret.
if err := os.RemoveAll(b.chonkPath()); err != nil { b.logf("Disablement failed, leaving TKA enabled. Error: %v", err)
return fmt.Errorf("os.RemoveAll: %v", err)
}
} else { } else {
b.logf("Disablement secret did not verify, leaving TKA enabled.") isEnabled = false
} }
} else { } else {
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled") return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
@ -216,12 +216,11 @@ func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
} }
} }
// NOTE(tom): We could short-circuit here if our HEAD equals the // NOTE(tom): We always send this RPC so control knows what TKA
// control-plane's head, but we don't just so control always has a // head we landed at.
// copy of all forks that clients had. head := b.tka.authority.Head()
b.mu.Unlock() b.mu.Unlock()
sendResp, err := b.tkaDoSyncSend(ourNodeKey, toSendAUMs, false) sendResp, err := b.tkaDoSyncSend(ourNodeKey, head, toSendAUMs, false)
b.mu.Lock() b.mu.Lock()
if err != nil { if err != nil {
return fmt.Errorf("send RPC: %v", err) return fmt.Errorf("send RPC: %v", err)
@ -238,6 +237,21 @@ func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
return nil return nil
} }
// tkaApplyDisablementLocked checks a disablement secret and locally disables
// TKA (if correct). An error is returned if disablement failed.
//
// b.mu must be held & TKA must be initialized.
func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
if b.tka.authority.ValidDisablement(secret) {
if err := os.RemoveAll(b.chonkPath()); err != nil {
return err
}
b.tka = nil
return nil
}
return errors.New("incorrect disablement secret")
}
// chonkPath returns the absolute path to the directory in which TKA // chonkPath returns the absolute path to the directory in which TKA
// state (the 'tailchonk') is stored. // state (the 'tailchonk') is stored.
func (b *LocalBackend) chonkPath() string { func (b *LocalBackend) chonkPath() string {
@ -334,7 +348,7 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
// needing signatures is returned as a response. // needing signatures is returned as a response.
// The Finish RPC submits signatures for all these nodes, at which point // The Finish RPC submits signatures for all these nodes, at which point
// Control has everything it needs to atomically enable network lock. // Control has everything it needs to atomically enable network lock.
func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error { func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byte) error {
if err := b.CanSupportNetworkLock(); err != nil { if err := b.CanSupportNetworkLock(); err != nil {
return err return err
} }
@ -355,8 +369,11 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
// just in case something goes wrong. // just in case something goes wrong.
_, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{ _, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
Keys: keys, Keys: keys,
// TODO(tom): Actually plumb a real disablement value. // TODO(tom): s/tka.State.DisablementSecrets/tka.State.DisablementValues
DisablementSecrets: [][]byte{bytes.Repeat([]byte{1}, 32)}, // This will center on consistent nomenclature:
// - DisablementSecret: value needed to disable.
// - DisablementValue: the KDF of the disablement secret, a public value.
DisablementSecrets: disablementValues,
}, b.nlPrivKey) }, b.nlPrivKey)
if err != nil { if err != nil {
return fmt.Errorf("tka.Create: %v", err) return fmt.Errorf("tka.Create: %v", err)
@ -454,8 +471,9 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
} }
ourNodeKey := b.prefs.Persist().PublicNodeKey() ourNodeKey := b.prefs.Persist().PublicNodeKey()
head := b.tka.authority.Head()
b.mu.Unlock() b.mu.Unlock()
resp, err := b.tkaDoSyncSend(ourNodeKey, aums, true) resp, err := b.tkaDoSyncSend(ourNodeKey, head, aums, true)
b.mu.Lock() b.mu.Lock()
if err != nil { if err != nil {
return err return err
@ -474,6 +492,42 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
return nil return nil
} }
// NetworkLockDisable disables network-lock using the provided disablement secret.
func (b *LocalBackend) NetworkLockDisable(secret []byte) error {
if err := b.CanSupportNetworkLock(); err != nil {
return err
}
var (
ourNodeKey key.NodePublic
head tka.AUMHash
err error
)
b.mu.Lock()
if b.prefs.Valid() {
ourNodeKey = b.prefs.Persist().PublicNodeKey()
}
if b.tka == nil {
err = errNetworkLockNotActive
} else {
head = b.tka.authority.Head()
if !b.tka.authority.ValidDisablement(secret) {
err = errors.New("incorrect disablement secret")
}
}
b.mu.Unlock()
if err != nil {
return err
}
if ourNodeKey.IsZero() {
return errors.New("no node-key: is tailscale logged in?")
}
_, err = b.tkaDoDisablement(ourNodeKey, head, secret)
return err
}
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) { func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
p, err := nodeInfo.NodePublic.MarshalBinary() p, err := nodeInfo.NodePublic.MarshalBinary()
if err != nil { if err != nil {
@ -519,7 +573,7 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
} }
a := new(tailcfg.TKAInitBeginResponse) a := new(tailcfg.TKAInitBeginResponse)
err = json.NewDecoder(res.Body).Decode(a) err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a)
res.Body.Close() res.Body.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err) return nil, fmt.Errorf("decoding JSON: %w", err)
@ -555,7 +609,7 @@ func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
} }
a := new(tailcfg.TKAInitFinishResponse) a := new(tailcfg.TKAInitFinishResponse)
err = json.NewDecoder(res.Body).Decode(a) err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a)
res.Body.Close() res.Body.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err) return nil, fmt.Errorf("decoding JSON: %w", err)
@ -603,7 +657,7 @@ func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUM
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
} }
a := new(tailcfg.TKABootstrapResponse) a := new(tailcfg.TKABootstrapResponse)
err = json.NewDecoder(res.Body).Decode(a) err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a)
res.Body.Close() res.Body.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err) return nil, fmt.Errorf("decoding JSON: %w", err)
@ -664,7 +718,7 @@ func (b *LocalBackend) tkaDoSyncOffer(ourNodeKey key.NodePublic, offer tka.SyncO
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
} }
a := new(tailcfg.TKASyncOfferResponse) a := new(tailcfg.TKASyncOfferResponse)
err = json.NewDecoder(res.Body).Decode(a) err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a)
res.Body.Close() res.Body.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err) return nil, fmt.Errorf("decoding JSON: %w", err)
@ -675,10 +729,16 @@ func (b *LocalBackend) tkaDoSyncOffer(ourNodeKey key.NodePublic, offer tka.SyncO
// tkaDoSyncSend sends a /machine/tka/sync/send RPC to the control plane // tkaDoSyncSend sends a /machine/tka/sync/send RPC to the control plane
// over noise. This is the second of two RPCs implementing tka synchronization. // over noise. This is the second of two RPCs implementing tka synchronization.
func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, aums []tka.AUM, interactive bool) (*tailcfg.TKASyncSendResponse, error) { func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, head tka.AUMHash, aums []tka.AUM, interactive bool) (*tailcfg.TKASyncSendResponse, error) {
headBytes, err := head.MarshalText()
if err != nil {
return nil, fmt.Errorf("head.MarshalText: %w", err)
}
sendReq := tailcfg.TKASyncSendRequest{ sendReq := tailcfg.TKASyncSendRequest{
Version: tailcfg.CurrentCapabilityVersion, Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey, NodeKey: ourNodeKey,
Head: string(headBytes),
MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)), MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)),
Interactive: interactive, Interactive: interactive,
} }
@ -707,7 +767,49 @@ func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, aums []tka.AUM,
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body)) return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
} }
a := new(tailcfg.TKASyncSendResponse) a := new(tailcfg.TKASyncSendResponse)
err = json.NewDecoder(res.Body).Decode(a) err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 10 * 1024 * 1024}).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}
func (b *LocalBackend) tkaDoDisablement(ourNodeKey key.NodePublic, head tka.AUMHash, secret []byte) (*tailcfg.TKADisableResponse, error) {
headBytes, err := head.MarshalText()
if err != nil {
return nil, fmt.Errorf("head.MarshalText: %w", err)
}
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(tailcfg.TKADisableRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
Head: string(headBytes),
DisablementSecret: secret,
}); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/disable", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
}
a := new(tailcfg.TKADisableResponse)
err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a)
res.Body.Close() res.Body.Close()
if err != nil { if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err) return nil, fmt.Errorf("decoding JSON: %w", err)

View File

@ -261,6 +261,8 @@ func TestTKASync(t *testing.T) {
someKeyPriv := key.NewNLPrivate() someKeyPriv := key.NewNLPrivate()
someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1} someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1}
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
type tkaSyncScenario struct { type tkaSyncScenario struct {
name string name string
// controlAUMs is called (if non-nil) to get any AUMs which the tka state // controlAUMs is called (if non-nil) to get any AUMs which the tka state
@ -342,7 +344,7 @@ type tkaSyncScenario struct {
controlStorage := &tka.Mem{} controlStorage := &tka.Mem{}
controlAuthority, bootstrap, err := tka.Create(controlStorage, tka.State{ controlAuthority, bootstrap, err := tka.Create(controlStorage, tka.State{
Keys: []tka.Key{key, someKey}, Keys: []tka.Key{key, someKey},
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)}, DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
}, nlPriv) }, nlPriv)
if err != nil { if err != nil {
t.Fatalf("tka.Create() failed: %v", err) t.Fatalf("tka.Create() failed: %v", err)
@ -416,6 +418,11 @@ type tkaSyncScenario struct {
t.Fatal(err) t.Fatal(err)
} }
t.Logf("got sync send:\n%+v", body) t.Logf("got sync send:\n%+v", body)
var remoteHead tka.AUMHash
if err := remoteHead.UnmarshalText([]byte(body.Head)); err != nil {
t.Fatalf("head unmarshal: %v", err)
}
toApply := make([]tka.AUM, len(body.MissingAUMs)) toApply := make([]tka.AUM, len(body.MissingAUMs))
for i, a := range body.MissingAUMs { for i, a := range body.MissingAUMs {
if err := toApply[i].Unserialize(a); err != nil { if err := toApply[i].Unserialize(a); err != nil {
@ -434,7 +441,9 @@ type tkaSyncScenario struct {
} }
w.WriteHeader(200) w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(tailcfg.TKASyncSendResponse{Head: string(head)}); err != nil { if err := json.NewEncoder(w).Encode(tailcfg.TKASyncSendResponse{
Head: string(head),
}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@ -536,3 +545,87 @@ func TestTKAFilterNetmap(t *testing.T) {
t.Errorf("filtered netmap differs (-want, +got):\n%s", diff) t.Errorf("filtered netmap differs (-want, +got):\n%s", diff)
} }
} }
func TestTKADisable(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
if err != nil {
t.Fatal(err)
}
authority, _, err := tka.Create(chonk, tka.State{
Keys: []tka.Key{key},
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/disable":
body := new(tailcfg.TKADisableRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if body.Version != tailcfg.CurrentCapabilityVersion {
t.Errorf("disable CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
}
if body.NodeKey != nodePriv.Public() {
t.Errorf("nodeKey = %v, want %v", body.NodeKey, nodePriv.Public())
}
if !bytes.Equal(body.DisablementSecret, disablementSecret) {
t.Errorf("disablement secret = %x, want %x", body.DisablementSecret, disablementSecret)
}
var head tka.AUMHash
if err := head.UnmarshalText([]byte(body.Head)); err != nil {
t.Fatalf("failed unmarshal of body.Head: %v", err)
}
if head != authority.Head() {
t.Errorf("reported head = %x, want %x", head, authority.Head())
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(tailcfg.TKADisableResponse{}); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
tka: &tkaState{
authority: authority,
storage: chonk,
},
prefs: (&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View(),
}
// Test that we get an error for an incorrect disablement secret.
if err := b.NetworkLockDisable([]byte{1, 2, 3, 4}); err == nil || err.Error() != "incorrect disablement secret" {
t.Errorf("NetworkLockDisable(<bad secret>).err = %v, want 'incorrect disablement secret'", err)
}
if err := b.NetworkLockDisable(disablementSecret); err != nil {
t.Errorf("NetworkLockDisable() failed: %v", err)
}
}

View File

@ -248,6 +248,10 @@ func (cc *mockControl) SetNetInfo(ni *tailcfg.NetInfo) {
cc.called("SetNetInfo") cc.called("SetNetInfo")
} }
func (cc *mockControl) SetTKAHead(head string) {
cc.logf("SetTKAHead: %s", head)
}
func (cc *mockControl) UpdateEndpoints(endpoints []tailcfg.Endpoint) { func (cc *mockControl) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
// validate endpoint information here? // validate endpoint information here?
cc.logf("UpdateEndpoints: ep=%v", endpoints) cc.logf("UpdateEndpoints: ep=%v", endpoints)

View File

@ -932,7 +932,8 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
} }
type initRequest struct { type initRequest struct {
Keys []tka.Key Keys []tka.Key
DisablementValues [][]byte
} }
var req initRequest var req initRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -940,7 +941,7 @@ type initRequest struct {
return return
} }
if err := h.b.NetworkLockInit(req.Keys); err != nil { if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues); err != nil {
http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError) http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError)
return return
} }

View File

@ -947,6 +947,11 @@ type MapRequest struct {
// EndpointTypes are the types of the corresponding endpoints in Endpoints. // EndpointTypes are the types of the corresponding endpoints in Endpoints.
EndpointTypes []EndpointType `json:",omitempty"` EndpointTypes []EndpointType `json:",omitempty"`
// TKAHead describes the hash of the latest AUM applied to the local
// tailnet key authority, if one is operating.
// It is encoded as tka.AUMHash.MarshalText.
TKAHead string `json:",omitempty"`
// ReadOnly is whether the client just wants to fetch the // ReadOnly is whether the client just wants to fetch the
// MapResponse, without updating their Endpoints. The // MapResponse, without updating their Endpoints. The
// Endpoints field will be ignored and LastSeen will not be // Endpoints field will be ignored and LastSeen will not be

View File

@ -86,9 +86,6 @@ type TKAInfo struct {
// //
// If the Head state differs to that known locally, the node should perform // If the Head state differs to that known locally, the node should perform
// synchronization via a separate RPC. // synchronization via a separate RPC.
//
// TODO(tom): Implement AUM synchronization as noise endpoints
// /machine/tka/sync/offer & /machine/tka/sync/send.
Head string `json:",omitempty"` Head string `json:",omitempty"`
// Disabled indicates the control plane believes TKA should be disabled, // Disabled indicates the control plane believes TKA should be disabled,
@ -97,9 +94,6 @@ type TKAInfo struct {
// disable TKA locally. // disable TKA locally.
// This field exists to disambiguate a nil TKAInfo in a delta mapresponse // This field exists to disambiguate a nil TKAInfo in a delta mapresponse
// from a nil TKAInfo indicating TKA should be disabled. // from a nil TKAInfo indicating TKA should be disabled.
//
// TODO(tom): Implement /machine/tka/bootstrap as a noise endpoint, to
// communicate the genesis AUM & any disablement secrets.
Disabled bool `json:",omitempty"` Disabled bool `json:",omitempty"`
} }
@ -162,7 +156,8 @@ type TKASyncOfferResponse struct {
} }
// TKASyncSendRequest encodes AUMs that a node believes the control plane // TKASyncSendRequest encodes AUMs that a node believes the control plane
// is missing. // is missing, and notifies control of its local TKA state (specifically
// the head hash).
type TKASyncSendRequest struct { type TKASyncSendRequest struct {
// Version is the client's capabilities. // Version is the client's capabilities.
Version CapabilityVersion Version CapabilityVersion
@ -170,9 +165,15 @@ type TKASyncSendRequest struct {
// NodeKey is the client's current node key. // NodeKey is the client's current node key.
NodeKey key.NodePublic NodeKey key.NodePublic
// Head represents the node's head AUMHash (tka.Authority.Head) after
// applying any AUMs from the sync-offer response.
// It is encoded as tka.AUMHash.MarshalText.
Head string
// MissingAUMs encodes AUMs that the node believes the control plane // MissingAUMs encodes AUMs that the node believes the control plane
// is missing. // is missing.
MissingAUMs []tkatype.MarshaledAUM MissingAUMs []tkatype.MarshaledAUM
// Interactive is true if additional error checking should be performed as // Interactive is true if additional error checking should be performed as
// the request is on behalf of an interactive operation (e.g., an // the request is on behalf of an interactive operation (e.g., an
// administrator publishing new changes) as opposed to an automatic // administrator publishing new changes) as opposed to an automatic
@ -187,3 +188,29 @@ type TKASyncSendResponse struct {
// after applying the missing AUMs. // after applying the missing AUMs.
Head string Head string
} }
// TKADisableRequest disables network-lock across the tailnet using the
// provided disablement secret.
//
// This is the request schema for a /tka/disable noise RPC.
type TKADisableRequest struct {
// Version is the client's capabilities.
Version CapabilityVersion
// NodeKey is the client's current node key.
NodeKey key.NodePublic
// Head represents the node's head AUMHash (tka.Authority.Head).
// It is encoded as tka.AUMHash.MarshalText.
Head string
// DisablementSecret encodes the secret necessary to disable TKA.
DisablementSecret []byte
}
// TKADisableResponse is the JSON response from a /tka/disable RPC.
// This schema describes the successful disablement of the tailnet's
// key authority.
type TKADisableResponse struct {
// Nothing. (yet?)
}