This PR implements the client-side of initializing network-lock with the Coordination server. Signed-off-by: Tom DNetto <tom@tailscale.com>main
parent
18edd79421
commit
facafd8819
@ -0,0 +1,101 @@ |
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli" |
||||
"tailscale.com/tka" |
||||
"tailscale.com/types/key" |
||||
) |
||||
|
||||
var netlockCmd = &ffcli.Command{ |
||||
Name: "lock", |
||||
ShortUsage: "lock <sub-command> <arguments>", |
||||
ShortHelp: "Manipulate the tailnet key authority", |
||||
Subcommands: []*ffcli.Command{nlInitCmd, nlStatusCmd}, |
||||
Exec: runNetworkLockStatus, |
||||
} |
||||
|
||||
var nlInitCmd = &ffcli.Command{ |
||||
Name: "init", |
||||
ShortUsage: "init <public-key>...", |
||||
ShortHelp: "Initialize the tailnet key authority", |
||||
Exec: runNetworkLockInit, |
||||
} |
||||
|
||||
func runNetworkLockInit(ctx context.Context, args []string) error { |
||||
st, err := localClient.NetworkLockStatus(ctx) |
||||
if err != nil { |
||||
return fixTailscaledConnectError(err) |
||||
} |
||||
if st.Enabled { |
||||
return errors.New("network-lock is already enabled") |
||||
} |
||||
|
||||
// Parse the set of initially-trusted keys.
|
||||
// Keys are specified using their key.NLPublic.MarshalText representation,
|
||||
// with an optional '?<votes>' suffix.
|
||||
var keys []tka.Key |
||||
for i, a := range args { |
||||
var key key.NLPublic |
||||
spl := strings.SplitN(a, "?", 2) |
||||
if err := key.UnmarshalText([]byte(spl[0])); err != nil { |
||||
return fmt.Errorf("parsing key %d: %v", i+1, err) |
||||
} |
||||
|
||||
k := tka.Key{ |
||||
Kind: tka.Key25519, |
||||
Public: key.Verifier(), |
||||
Votes: 1, |
||||
} |
||||
if len(spl) > 1 { |
||||
votes, err := strconv.Atoi(spl[1]) |
||||
if err != nil { |
||||
return fmt.Errorf("parsing key %d votes: %v", i+1, err) |
||||
} |
||||
k.Votes = uint(votes) |
||||
} |
||||
keys = append(keys, k) |
||||
} |
||||
|
||||
status, err := localClient.NetworkLockInit(ctx, keys) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
fmt.Printf("Status: %+v\n\n", status) |
||||
return nil |
||||
} |
||||
|
||||
var nlStatusCmd = &ffcli.Command{ |
||||
Name: "status", |
||||
ShortUsage: "status", |
||||
ShortHelp: "Outputs the state of network lock", |
||||
Exec: runNetworkLockStatus, |
||||
} |
||||
|
||||
func runNetworkLockStatus(ctx context.Context, args []string) error { |
||||
st, err := localClient.NetworkLockStatus(ctx) |
||||
if err != nil { |
||||
return fixTailscaledConnectError(err) |
||||
} |
||||
if st.Enabled { |
||||
fmt.Println("Network-lock is ENABLED.") |
||||
} else { |
||||
fmt.Println("Network-lock is NOT enabled.") |
||||
} |
||||
p, err := st.PublicKey.MarshalText() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
fmt.Printf("our public-key: %s\n", p) |
||||
return nil |
||||
} |
||||
@ -0,0 +1,226 @@ |
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"encoding/json" |
||||
"errors" |
||||
"fmt" |
||||
"io" |
||||
"net/http" |
||||
"time" |
||||
|
||||
"tailscale.com/envknob" |
||||
"tailscale.com/ipn/ipnstate" |
||||
"tailscale.com/logtail/backoff" |
||||
"tailscale.com/tailcfg" |
||||
"tailscale.com/tka" |
||||
"tailscale.com/types/key" |
||||
"tailscale.com/types/tkatype" |
||||
) |
||||
|
||||
var networkLockAvailable = envknob.Bool("TS_EXPERIMENTAL_NETWORK_LOCK") |
||||
|
||||
// CanSupportNetworkLock returns true if tailscaled is able to operate
|
||||
// a local tailnet key authority (and hence enforce network lock).
|
||||
func (b *LocalBackend) CanSupportNetworkLock() bool { |
||||
if b.tka != nil { |
||||
// The TKA is being used, so yeah its supported.
|
||||
return true |
||||
} |
||||
|
||||
if b.TailscaleVarRoot() != "" { |
||||
// Theres a var root (aka --statedir), so if network lock gets
|
||||
// initialized we have somewhere to store our AUMs. Thats all
|
||||
// we need.
|
||||
return true |
||||
} |
||||
return false |
||||
} |
||||
|
||||
// NetworkLockStatus returns a structure describing the state of the
|
||||
// tailnet key authority, if any.
|
||||
func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { |
||||
if b.tka == nil { |
||||
return &ipnstate.NetworkLockStatus{ |
||||
Enabled: false, |
||||
PublicKey: b.nlPrivKey.Public(), |
||||
} |
||||
} |
||||
|
||||
var head [32]byte |
||||
h := b.tka.Head() |
||||
copy(head[:], h[:]) |
||||
|
||||
return &ipnstate.NetworkLockStatus{ |
||||
Enabled: true, |
||||
Head: &head, |
||||
PublicKey: b.nlPrivKey.Public(), |
||||
} |
||||
} |
||||
|
||||
// NetworkLockInit enables network-lock for the tailnet, with the tailnets'
|
||||
// key authority initialized to trust the provided keys.
|
||||
//
|
||||
// Initialization involves two RPCs with control, termed 'begin' and 'finish'.
|
||||
// The Begin RPC transmits the genesis Authority Update Message, which
|
||||
// encodes the initial state of the authority, and the list of all nodes
|
||||
// needing signatures is returned as a response.
|
||||
// The Finish RPC submits signatures for all these nodes, at which point
|
||||
// Control has everything it needs to atomically enable network lock.
|
||||
func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error { |
||||
if b.tka != nil { |
||||
return errors.New("network-lock is already initialized") |
||||
} |
||||
if !networkLockAvailable { |
||||
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.") |
||||
} |
||||
if !b.CanSupportNetworkLock() { |
||||
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?") |
||||
} |
||||
|
||||
// Generates a genesis AUM representing trust in the provided keys.
|
||||
// We use an in-memory tailchonk because we don't want to commit to
|
||||
// the filesystem until we've finished the initialization sequence,
|
||||
// just in case something goes wrong.
|
||||
_, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{ |
||||
Keys: keys, |
||||
// TODO(tom): Actually plumb a real disablement value.
|
||||
DisablementSecrets: [][]byte{bytes.Repeat([]byte{1}, 32)}, |
||||
}, b.nlPrivKey) |
||||
if err != nil { |
||||
return fmt.Errorf("tka.Create: %v", err) |
||||
} |
||||
|
||||
b.logf("Generated genesis AUM to initialize network lock, trusting the following keys:") |
||||
for i, k := range genesisAUM.State.Keys { |
||||
b.logf(" - key[%d] = nlpub:%x with %d votes", i, k.Public, k.Votes) |
||||
} |
||||
|
||||
// Phase 1/2 of initialization: Transmit the genesis AUM to Control.
|
||||
initResp, err := b.tkaInitBegin(genesisAUM) |
||||
if err != nil { |
||||
return fmt.Errorf("tka init-begin RPC: %w", err) |
||||
} |
||||
|
||||
// Our genesis AUM was accepted but before Control turns on enforcement of
|
||||
// node-key signatures, we need to sign keys for all the existing nodes.
|
||||
// If we don't get these signatures ahead of time, everyone will loose
|
||||
// connectivity because control won't have any signatures to send which
|
||||
// satisfy network-lock checks.
|
||||
var sigs []tkatype.MarshaledSignature |
||||
for _, nkp := range initResp.NeedSignatures { |
||||
nks, err := signNodeKey(nkp, b.nlPrivKey) |
||||
if err != nil { |
||||
return fmt.Errorf("generating signature: %v", err) |
||||
} |
||||
|
||||
sigs = append(sigs, nks.Serialize()) |
||||
} |
||||
|
||||
// Finalize enablement by transmitting signature for all nodes to Control.
|
||||
_, err = b.tkaInitFinish(sigs) |
||||
return err |
||||
} |
||||
|
||||
func signNodeKey(nk key.NodePublic, signer key.NLPrivate) (*tka.NodeKeySignature, error) { |
||||
p, err := nk.MarshalBinary() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
sig := tka.NodeKeySignature{ |
||||
SigKind: tka.SigDirect, |
||||
KeyID: signer.KeyID(), |
||||
Pubkey: p, |
||||
} |
||||
sig.Signature, err = signer.SignNKS(sig.SigHash()) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("signature failed: %w", err) |
||||
} |
||||
return &sig, nil |
||||
} |
||||
|
||||
func (b *LocalBackend) tkaInitBegin(aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) { |
||||
var req bytes.Buffer |
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{ |
||||
GenesisAUM: aum.Serialize(), |
||||
}); err != nil { |
||||
return nil, fmt.Errorf("encoding request: %v", err) |
||||
} |
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) |
||||
defer cancel() |
||||
bo := backoff.NewBackoff("tka-init-begin", b.logf, 5*time.Second) |
||||
for { |
||||
if err := ctx.Err(); err != nil { |
||||
return nil, fmt.Errorf("ctx: %w", err) |
||||
} |
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("req: %w", err) |
||||
} |
||||
res, err := b.DoNoiseRequest(req) |
||||
if err != nil { |
||||
bo.BackOff(ctx, err) |
||||
continue |
||||
} |
||||
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.TKAInitBeginResponse) |
||||
err = json.NewDecoder(res.Body).Decode(a) |
||||
res.Body.Close() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("decoding JSON: %w", err) |
||||
} |
||||
|
||||
return a, nil |
||||
} |
||||
} |
||||
|
||||
func (b *LocalBackend) tkaInitFinish(nks []tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) { |
||||
var req bytes.Buffer |
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{ |
||||
Signatures: nks, |
||||
}); err != nil { |
||||
return nil, fmt.Errorf("encoding request: %v", err) |
||||
} |
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) |
||||
defer cancel() |
||||
bo := backoff.NewBackoff("tka-init-finish", b.logf, 5*time.Second) |
||||
for { |
||||
if err := ctx.Err(); err != nil { |
||||
return nil, fmt.Errorf("ctx: %w", err) |
||||
} |
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("req: %w", err) |
||||
} |
||||
res, err := b.DoNoiseRequest(req) |
||||
if err != nil { |
||||
bo.BackOff(ctx, err) |
||||
continue |
||||
} |
||||
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.TKAInitFinishResponse) |
||||
err = json.NewDecoder(res.Body).Decode(a) |
||||
res.Body.Close() |
||||
if err != nil { |
||||
return nil, fmt.Errorf("decoding JSON: %w", err) |
||||
} |
||||
|
||||
return a, nil |
||||
} |
||||
} |
||||
Loading…
Reference in new issue