cmd/tailscale,ipn: support disablement args in lock cli, implement disable

* Support specifiying disablement values in lock init command
 * Support specifying rotation key in lock sign command
 * Implement lock disable command
 * Implement disablement-kdf command

Signed-off-by: Tom DNetto <tom@tailscale.com>
pull/6269/head
Tom DNetto 2 years ago committed by Tom
parent fb392e34b5
commit 3271daf7a3

@ -827,6 +827,14 @@ func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConf
return nil
}
// NetworkLockDisable shuts down network-lock across the tailnet.
func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) error {
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
return fmt.Errorf("error: %w", err)
}
return nil
}
// GetServeConfig return the current serve config.
//
// If the serve config is empty, it returns (nil, nil).

@ -19,6 +19,7 @@ import (
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/tstest"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
@ -1156,3 +1157,69 @@ func TestUpWorthWarning(t *testing.T) {
t.Errorf("want false for other misc errors")
}
}
func TestParseNLArgs(t *testing.T) {
tcs := []struct {
name string
input []string
parseKeys bool
parseDisablements bool
wantErr error
wantKeys []tka.Key
wantDisablements [][]byte
}{
{
name: "empty",
input: nil,
parseKeys: true,
parseDisablements: true,
},
{
name: "key no votes",
input: []string{"nlpub:" + strings.Repeat("00", 32)},
parseKeys: true,
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 1, Public: bytes.Repeat([]byte{0}, 32)}},
},
{
name: "key with votes",
input: []string{"nlpub:" + strings.Repeat("01", 32) + "?5"},
parseKeys: true,
wantKeys: []tka.Key{{Kind: tka.Key25519, Votes: 5, Public: bytes.Repeat([]byte{1}, 32)}},
},
{
name: "disablements",
input: []string{"disablement:" + strings.Repeat("02", 32), "disablement-secret:" + strings.Repeat("03", 32)},
parseDisablements: true,
wantDisablements: [][]byte{bytes.Repeat([]byte{2}, 32), bytes.Repeat([]byte{3}, 32)},
},
{
name: "disablements not allowed",
input: []string{"disablement:" + strings.Repeat("02", 32)},
parseKeys: true,
wantErr: fmt.Errorf("parsing key 1: key hex string doesn't have expected type prefix nlpub:"),
},
{
name: "keys not allowed",
input: []string{"nlpub:" + strings.Repeat("02", 32)},
parseDisablements: true,
wantErr: fmt.Errorf("parsing argument 1: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", "nlpub:0202020202020202020202020202020202020202020202020202020202020202"),
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
keys, disablements, err := parseNLArgs(tc.input, tc.parseKeys, tc.parseDisablements)
if !reflect.DeepEqual(err, tc.wantErr) {
t.Fatalf("parseNLArgs(%v).err = %v, want %v", tc.input, err, tc.wantErr)
}
if !reflect.DeepEqual(keys, tc.wantKeys) {
t.Errorf("keys = %v, want %v", keys, tc.wantKeys)
}
if !reflect.DeepEqual(disablements, tc.wantDisablements) {
t.Errorf("disablements = %v, want %v", disablements, tc.wantDisablements)
}
})
}
}

@ -5,8 +5,8 @@
package cli
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"strconv"
@ -27,6 +27,8 @@ var netlockCmd = &ffcli.Command{
nlAddCmd,
nlRemoveCmd,
nlSignCmd,
nlDisableCmd,
nlDisablementKDFCmd,
},
Exec: runNetworkLockStatus,
}
@ -47,15 +49,12 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
return errors.New("network-lock is already enabled")
}
// Parse the set of initially-trusted keys.
keys, err := parseNLKeyArgs(args)
// Parse initially-trusted keys & disablement values.
keys, disablementValues, err := parseNLArgs(args, true, true)
if err != nil {
return err
}
// 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 {
return err
@ -143,17 +142,34 @@ var nlRemoveCmd = &ffcli.Command{
},
}
// parseNLKeyArgs converts a slice of strings into a slice of tka.Key. The keys
// should be specified using their key.NLPublic.MarshalText representation with
// an optional '?<votes>' suffix. If any of the keys encounters an error, a nil
// slice is returned along with an appropriate error.
func parseNLKeyArgs(args []string) ([]tka.Key, error) {
var keys []tka.Key
// parseNLArgs parses a slice of strings into slices of tka.Key & disablement
// values/secrets.
// The keys encoded in args should be specified using their key.NLPublic.MarshalText
// representation with an optional '?<votes>' suffix.
// Disablement values or secrets must be encoded in hex with a prefix of 'disablement:' or
// 'disablement-secret:'.
//
// If any element could not be parsed,
// a nil slice is returned along with an appropriate error.
func parseNLArgs(args []string, parseKeys, parseDisablements bool) (keys []tka.Key, disablements [][]byte, err error) {
for i, a := range args {
if parseDisablements && (strings.HasPrefix(a, "disablement:") || strings.HasPrefix(a, "disablement-secret:")) {
b, err := hex.DecodeString(a[strings.Index(a, ":")+1:])
if err != nil {
return nil, nil, fmt.Errorf("parsing disablement %d: %v", i+1, err)
}
disablements = append(disablements, b)
continue
}
if !parseKeys {
return nil, nil, fmt.Errorf("parsing argument %d: expected value with \"disablement:\" or \"disablement-secret:\" prefix, got %q", i+1, a)
}
var nlpk key.NLPublic
spl := strings.SplitN(a, "?", 2)
if err := nlpk.UnmarshalText([]byte(spl[0])); err != nil {
return nil, fmt.Errorf("parsing key %d: %v", i+1, err)
return nil, nil, fmt.Errorf("parsing key %d: %v", i+1, err)
}
k := tka.Key{
@ -164,13 +180,13 @@ func parseNLKeyArgs(args []string) ([]tka.Key, error) {
if len(spl) > 1 {
votes, err := strconv.Atoi(spl[1])
if err != nil {
return nil, fmt.Errorf("parsing key %d votes: %v", i+1, err)
return nil, nil, fmt.Errorf("parsing key %d votes: %v", i+1, err)
}
k.Votes = uint(votes)
}
keys = append(keys, k)
}
return keys, nil
return keys, disablements, nil
}
func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) error {
@ -182,11 +198,11 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
return errors.New("network-lock is not enabled")
}
addKeys, err := parseNLKeyArgs(addArgs)
addKeys, _, err := parseNLArgs(addArgs, true, false)
if err != nil {
return err
}
removeKeys, err := parseNLKeyArgs(removeArgs)
removeKeys, _, err := parseNLArgs(removeArgs, true, false)
if err != nil {
return err
}
@ -202,24 +218,65 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
var nlSignCmd = &ffcli.Command{
Name: "sign",
ShortUsage: "sign <node-key>",
ShortUsage: "sign <node-key> [<rotation-key>]",
ShortHelp: "Signs a node-key and transmits that signature to the control plane",
Exec: runNetworkLockSign,
}
// TODO(tom): Implement specifying the rotation key for the signature.
func runNetworkLockSign(ctx context.Context, args []string) error {
switch len(args) {
case 0:
return errors.New("expected node-key as second argument")
case 1:
var nodeKey key.NodePublic
var (
nodeKey key.NodePublic
rotationKey key.NLPublic
)
if len(args) == 0 || len(args) > 2 {
return errors.New("usage: lock sign <node-key> [<rotation-key>]")
}
if err := nodeKey.UnmarshalText([]byte(args[0])); err != nil {
return fmt.Errorf("decoding node-key: %w", err)
}
if len(args) > 1 {
if err := rotationKey.UnmarshalText([]byte(args[1])); err != nil {
return fmt.Errorf("decoding rotation-key: %w", err)
}
}
return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
}
return localClient.NetworkLockSign(ctx, nodeKey, nil)
default:
return errors.New("expected a single node-key as only argument")
var nlDisableCmd = &ffcli.Command{
Name: "disable",
ShortUsage: "disable <disablement-secret>",
ShortHelp: "Consumes a disablement secret to shut down network-lock across the tailnet",
Exec: runNetworkLockDisable,
}
func runNetworkLockDisable(ctx context.Context, args []string) error {
_, secrets, err := parseNLArgs(args, false, true)
if err != nil {
return err
}
if len(secrets) != 1 {
return errors.New("usage: lock disable <disablement-secret>")
}
return localClient.NetworkLockDisable(ctx, secrets[0])
}
var nlDisablementKDFCmd = &ffcli.Command{
Name: "disablement-kdf",
ShortUsage: "disablement-kdf <hex-encoded-disablement-secret>",
ShortHelp: "Computes a disablement value from a disablement secret",
Exec: runNetworkLockDisablementKDF,
}
func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: lock disablement-kdf <hex-encoded-disablement-secret>")
}
secret, err := hex.DecodeString(args[0])
if err != nil {
return err
}
fmt.Printf("disablement:%x\n", tka.DisablementKDF(secret))
return nil
}

@ -13,6 +13,7 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/http/httputil"
@ -80,6 +81,7 @@ var handler = map[string]localAPIHandler{
"tka/modify": (*Handler).serveTKAModify,
"tka/sign": (*Handler).serveTKASign,
"tka/status": (*Handler).serveTKAStatus,
"tka/disable": (*Handler).serveTKADisable,
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
"whois": (*Handler).serveWhoIs,
}
@ -1073,6 +1075,30 @@ func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
w.Write(j)
}
func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
return
}
if r.Method != http.MethodPost {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
body := io.LimitReader(r.Body, 1024*1024)
secret, err := ioutil.ReadAll(body)
if err != nil {
http.Error(w, "reading secret", 400)
return
}
if err := h.b.NetworkLockDisable(secret); err != nil {
http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(200)
}
func defBool(a string, def bool) bool {
if a == "" {
return def

Loading…
Cancel
Save