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>
This commit is contained in:
Tom DNetto 2022-11-04 12:12:51 -07:00 committed by Tom
parent fb392e34b5
commit 3271daf7a3
4 changed files with 188 additions and 30 deletions

View File

@ -827,6 +827,14 @@ func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConf
return nil 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. // GetServeConfig return the current serve config.
// //
// If the serve config is empty, it returns (nil, nil). // If the serve config is empty, it returns (nil, nil).

View File

@ -19,6 +19,7 @@
"tailscale.com/health/healthmsg" "tailscale.com/health/healthmsg"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/tstest" "tailscale.com/tstest"
"tailscale.com/types/persist" "tailscale.com/types/persist"
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
@ -1156,3 +1157,69 @@ func TestUpWorthWarning(t *testing.T) {
t.Errorf("want false for other misc errors") 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)
}
})
}
}

View File

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

View File

@ -13,6 +13,7 @@
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
@ -80,6 +81,7 @@
"tka/modify": (*Handler).serveTKAModify, "tka/modify": (*Handler).serveTKAModify,
"tka/sign": (*Handler).serveTKASign, "tka/sign": (*Handler).serveTKASign,
"tka/status": (*Handler).serveTKAStatus, "tka/status": (*Handler).serveTKAStatus,
"tka/disable": (*Handler).serveTKADisable,
"upload-client-metrics": (*Handler).serveUploadClientMetrics, "upload-client-metrics": (*Handler).serveUploadClientMetrics,
"whois": (*Handler).serveWhoIs, "whois": (*Handler).serveWhoIs,
} }
@ -1073,6 +1075,30 @@ type modifyRequest struct {
w.Write(j) 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 { func defBool(a string, def bool) bool {
if a == "" { if a == "" {
return def return def