From facafd88190ed9b7bc6fc1977ffc52c5d3fe8545 Mon Sep 17 00:00:00 2001 From: Tom DNetto Date: Thu, 11 Aug 2022 10:43:09 -0700 Subject: [PATCH] client,cmd/tailscale,ipn,tka,types: implement tka initialization flow This PR implements the client-side of initializing network-lock with the Coordination server. Signed-off-by: Tom DNetto --- client/tailscale/localclient.go | 37 +++++ cmd/derper/depaware.txt | 11 +- cmd/tailscale/cli/cli.go | 1 + cmd/tailscale/cli/network-lock.go | 101 +++++++++++++ cmd/tailscale/depaware.txt | 12 +- ipn/ipnlocal/network-lock.go | 226 ++++++++++++++++++++++++++++++ ipn/ipnstate/ipnstate.go | 15 ++ ipn/localapi/localapi.go | 57 ++++++++ tailcfg/tailcfg.go | 26 ++++ tka/aum.go | 2 +- tka/aum_test.go | 2 +- tka/builder.go | 1 + tka/key.go | 3 + tka/sig.go | 6 +- tka/sig_test.go | 8 +- types/key/nl.go | 7 +- types/tkatype/tkatype.go | 7 + types/tkatype/tkatype_test.go | 5 + 18 files changed, 514 insertions(+), 13 deletions(-) create mode 100644 cmd/tailscale/cli/network-lock.go create mode 100644 ipn/ipnlocal/network-lock.go diff --git a/client/tailscale/localclient.go b/client/tailscale/localclient.go index 313d5fe39..d1f49f7e8 100644 --- a/client/tailscale/localclient.go +++ b/client/tailscale/localclient.go @@ -36,6 +36,7 @@ import ( "tailscale.com/paths" "tailscale.com/safesocket" "tailscale.com/tailcfg" + "tailscale.com/tka" ) // defaultLocalClient is the default LocalClient when using the legacy @@ -680,6 +681,42 @@ func (lc *LocalClient) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg return pr, nil } +// NetworkLockStatus fetches information about the tailnet key authority, if one is configured. +func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) { + body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil) + if err != nil { + return nil, fmt.Errorf("error: %w", err) + } + pr := new(ipnstate.NetworkLockStatus) + if err := json.Unmarshal(body, pr); err != nil { + return nil, err + } + return pr, nil +} + +// NetworkLockInit initializes the tailnet key authority. +func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ipnstate.NetworkLockStatus, error) { + var b bytes.Buffer + type initRequest struct { + Keys []tka.Key + } + + if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys}); err != nil { + return nil, err + } + + body, err := lc.send(ctx, "POST", "/localapi/v0/tka/init", 200, &b) + if err != nil { + return nil, fmt.Errorf("error: %w", err) + } + + pr := new(ipnstate.NetworkLockStatus) + if err := json.Unmarshal(body, pr); err != nil { + return nil, err + } + return pr, nil +} + // tailscaledConnectHint gives a little thing about why tailscaled (or // platform equivalent) is not answering localapi connections. // diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 262c65b4f..e86596f73 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -1,9 +1,13 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depaware) + filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus + filippo.io/edwards25519/field from filippo.io/edwards25519 W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+ W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy + github.com/fxamacker/cbor/v2 from tailscale.com/tka github.com/golang/groupcache/lru from tailscale.com/net/dnscache + github.com/hdevalence/ed25519consensus from tailscale.com/tka L github.com/josharian/native from github.com/mdlayher/netlink+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink @@ -12,6 +16,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink 💣 github.com/mitchellh/go-ps from tailscale.com/safesocket + github.com/x448/float16 from github.com/fxamacker/cbor/v2 💣 go4.org/mem from tailscale.com/client/tailscale+ go4.org/netipx from tailscale.com/wgengine/filter W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ @@ -46,6 +51,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa tailscale.com/safesocket from tailscale.com/client/tailscale tailscale.com/syncs from tailscale.com/cmd/derper+ tailscale.com/tailcfg from tailscale.com/client/tailscale+ + tailscale.com/tka from tailscale.com/client/tailscale W tailscale.com/tsconst from tailscale.com/net/interfaces 💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate tailscale.com/tstime/rate from tailscale.com/wgengine/filter @@ -76,7 +82,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa tailscale.com/wgengine/filter from tailscale.com/types/netmap golang.org/x/crypto/acme from golang.org/x/crypto/acme/autocert golang.org/x/crypto/acme/autocert from tailscale.com/cmd/derper - golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box + golang.org/x/crypto/argon2 from tailscale.com/tka + golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+ + golang.org/x/crypto/blake2s from tailscale.com/tka golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305 golang.org/x/crypto/chacha20poly1305 from crypto/tls golang.org/x/crypto/cryptobyte from crypto/ecdsa+ @@ -133,6 +141,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa embed from crypto/internal/nistec+ encoding from encoding/json+ encoding/asn1 from crypto/x509+ + encoding/base32 from tailscale.com/tka encoding/base64 from encoding/json+ encoding/binary from compress/gzip+ encoding/hex from crypto/x509+ diff --git a/cmd/tailscale/cli/cli.go b/cmd/tailscale/cli/cli.go index 141863911..4a73c4ce6 100644 --- a/cmd/tailscale/cli/cli.go +++ b/cmd/tailscale/cli/cli.go @@ -169,6 +169,7 @@ change in the future. fileCmd, bugReportCmd, certCmd, + netlockCmd, }, FlagSet: rootfs, Exec: func(context.Context, []string) error { return flag.ErrHelp }, diff --git a/cmd/tailscale/cli/network-lock.go b/cmd/tailscale/cli/network-lock.go new file mode 100644 index 000000000..51191d9c3 --- /dev/null +++ b/cmd/tailscale/cli/network-lock.go @@ -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 ", + ShortHelp: "Manipulate the tailnet key authority", + Subcommands: []*ffcli.Command{nlInitCmd, nlStatusCmd}, + Exec: runNetworkLockStatus, +} + +var nlInitCmd = &ffcli.Command{ + Name: "init", + ShortUsage: "init ...", + 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 '?' 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 +} diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index cfab809cb..1d3f4cb59 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -1,9 +1,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware) + filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus + filippo.io/edwards25519/field from filippo.io/edwards25519 W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+ W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy + github.com/fxamacker/cbor/v2 from tailscale.com/tka github.com/golang/groupcache/lru from tailscale.com/net/dnscache + github.com/hdevalence/ed25519consensus from tailscale.com/tka L github.com/josharian/native from github.com/mdlayher/netlink+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink @@ -26,6 +30,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli + github.com/x448/float16 from github.com/fxamacker/cbor/v2 💣 go4.org/mem from tailscale.com/derp+ go4.org/netipx from tailscale.com/wgengine/filter W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ @@ -69,6 +74,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+ tailscale.com/syncs from tailscale.com/net/netcheck+ tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+ + tailscale.com/tka from tailscale.com/client/tailscale+ W tailscale.com/tsconst from tailscale.com/net/interfaces 💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate tailscale.com/tstime/rate from tailscale.com/wgengine/filter @@ -100,8 +106,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/version from tailscale.com/cmd/tailscale/cli+ tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+ tailscale.com/wgengine/filter from tailscale.com/types/netmap - golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box - golang.org/x/crypto/blake2s from tailscale.com/control/controlbase + golang.org/x/crypto/argon2 from tailscale.com/tka + golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+ + golang.org/x/crypto/blake2s from tailscale.com/control/controlbase+ golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305 golang.org/x/crypto/chacha20poly1305 from crypto/tls+ golang.org/x/crypto/cryptobyte from crypto/ecdsa+ @@ -162,6 +169,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep embed from tailscale.com/cmd/tailscale/cli+ encoding from encoding/json+ encoding/asn1 from crypto/x509+ + encoding/base32 from tailscale.com/tka encoding/base64 from encoding/json+ encoding/binary from compress/gzip+ encoding/hex from crypto/x509+ diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go new file mode 100644 index 000000000..cf7f30312 --- /dev/null +++ b/ipn/ipnlocal/network-lock.go @@ -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 + } +} diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 23b19ec9d..00d236b40 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -67,6 +67,21 @@ type Status struct { User map[tailcfg.UserID]tailcfg.UserProfile } +// NetworkLockStatus represents whether network-lock is enabled, +// along with details about the locally-known state of the tailnet +// key authority. +type NetworkLockStatus struct { + // Enabled is true if network lock is enabled. + Enabled bool + + // Head describes the AUM hash of the leaf AUM. Head is nil + // if network lock is not enabled. + Head *[32]byte + + // PublicKey describes the nodes' network-lock public key. + PublicKey key.NLPublic +} + // TailnetStatus is information about a Tailscale network ("tailnet"). type TailnetStatus struct { // Name is the name of the network that's currently in use. diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index de6e5cddc..053a0a776 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -31,6 +31,7 @@ import ( "tailscale.com/ipn/ipnstate" "tailscale.com/net/netutil" "tailscale.com/tailcfg" + "tailscale.com/tka" "tailscale.com/types/logger" "tailscale.com/util/clientmetric" "tailscale.com/version" @@ -150,6 +151,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { h.serveIDToken(w, r) case "/localapi/v0/upload-client-metrics": h.serveUploadClientMetrics(w, r) + case "/localapi/v0/tka/status": + h.serveTkaStatus(w, r) + case "/localapi/v0/tka/init": + h.serveTkaInit(w, r) case "/": io.WriteString(w, "tailscaled\n") default: @@ -791,6 +796,58 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques json.NewEncoder(w).Encode(struct{}{}) } +func (h *Handler) serveTkaStatus(w http.ResponseWriter, r *http.Request) { + if !h.PermitRead { + http.Error(w, "lock status access denied", http.StatusForbidden) + return + } + if r.Method != http.MethodGet { + http.Error(w, "use Get", http.StatusMethodNotAllowed) + return + } + + j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t") + if err != nil { + http.Error(w, "JSON encoding error", 500) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(j) +} + +func (h *Handler) serveTkaInit(w http.ResponseWriter, r *http.Request) { + if !h.PermitWrite { + http.Error(w, "lock init access denied", http.StatusForbidden) + return + } + if r.Method != http.MethodPost { + http.Error(w, "use POST", http.StatusMethodNotAllowed) + return + } + + type initRequest struct { + Keys []tka.Key + } + var req initRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "invalid JSON body", 400) + return + } + + if err := h.b.NetworkLockInit(req.Keys); err != nil { + http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError) + return + } + + j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t") + if err != nil { + http.Error(w, "JSON encoding error", 500) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(j) +} + func defBool(a string, def bool) bool { if a == "" { return def diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 9a6acd43a..ab91de303 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -1826,6 +1826,32 @@ type PeerChange struct { Capabilities *[]string `json:",omitempty"` } +// TKAInitBeginRequest submits a genesis AUM to seed the creation of the +// tailnet's key authority. +type TKAInitBeginRequest struct { + NodeID NodeID + + GenesisAUM tkatype.MarshaledAUM +} + +// TKAInitBeginResponse describes a set of NodeKeys which must be signed to +// complete initialization of the tailnets' key authority. +type TKAInitBeginResponse struct { + NodeID NodeID + + NeedSignatures []key.NodePublic +} + +// TKAInitFinishRequest finalizes initialization of the tailnet key authority +// by submitting node-key signatures for all existing nodes. +type TKAInitFinishRequest struct { + Signatures []tkatype.MarshaledSignature +} + +// TKAInitFinishResponse describes the successful enablement of the tailnet's +// key authority. +type TKAInitFinishResponse struct{} + // DerpMagicIP is a fake WireGuard endpoint IP address that means to // use DERP. When used (in the Node.DERP field), the port number of // the WireGuard endpoint is the DERP region ID number to use. diff --git a/tka/aum.go b/tka/aum.go index 05bf9221b..fef2b6b78 100644 --- a/tka/aum.go +++ b/tka/aum.go @@ -216,7 +216,7 @@ func (a *AUM) StaticValidate() error { // We would implement encoding.BinaryMarshaler, except that would // unfortunately get called by the cbor marshaller resulting in infinite // recursion. -func (a *AUM) Serialize() []byte { +func (a *AUM) Serialize() tkatype.MarshaledAUM { // Why CBOR and not something like JSON? // // The main function of an AUM is to carry signed data. Signatures are diff --git a/tka/aum_test.go b/tka/aum_test.go index 3ca9755d9..3f2449889 100644 --- a/tka/aum_test.go +++ b/tka/aum_test.go @@ -158,7 +158,7 @@ func TestSerialization(t *testing.T) { for _, tc := range tcs { t.Run(tc.Name, func(t *testing.T) { - data := tc.AUM.Serialize() + data := []byte(tc.AUM.Serialize()) if diff := cmp.Diff(tc.Expect, data); diff != "" { t.Errorf("serialization differs (-want, +got):\n%s", diff) } diff --git a/tka/builder.go b/tka/builder.go index 500fcbb25..e44875c94 100644 --- a/tka/builder.go +++ b/tka/builder.go @@ -12,6 +12,7 @@ import ( // Types implementing Signer can sign update messages. type Signer interface { + // SignAUM returns signatures for the AUM encoded by the given AUMSigHash. SignAUM(tkatype.AUMSigHash) ([]tkatype.Signature, error) } diff --git a/tka/key.go b/tka/key.go index aeb2a461a..aa0a253f9 100644 --- a/tka/key.go +++ b/tka/key.go @@ -91,6 +91,9 @@ func (k Key) StaticValidate() error { if k.Votes > 4096 { return fmt.Errorf("excessive key weight: %d > 4096", k.Votes) } + if k.Votes == 0 { + return errors.New("key votes must be non-zero") + } // We have an arbitrary upper limit on the amount // of metadata that can be associated with a key, so diff --git a/tka/sig.go b/tka/sig.go index 1f17b0c53..177c52ec3 100644 --- a/tka/sig.go +++ b/tka/sig.go @@ -55,13 +55,13 @@ type NodeKeySignature struct { Signature []byte `cbor:"4,keyasint,omitempty"` } -// sigHash returns the cryptographic digest which a signature +// SigHash returns the cryptographic digest which a signature // is over. // // This is a hash of the serialized structure, sans the signature. // Without this exclusion, the hash used for the signature // would be circularly dependent on the signature. -func (s NodeKeySignature) sigHash() [blake2s.Size]byte { +func (s NodeKeySignature) SigHash() [blake2s.Size]byte { dupe := s dupe.Signature = nil return blake2s.Sum256(dupe.Serialize()) @@ -100,7 +100,7 @@ func (s *NodeKeySignature) Unserialize(data []byte) error { // verifySignature checks that the NodeKeySignature is authentic and certified // by the given verificationKey. func (s *NodeKeySignature) verifySignature(verificationKey Key) error { - sigHash := s.sigHash() + sigHash := s.SigHash() switch verificationKey.Kind { case Key25519: if ed25519consensus.Verify(ed25519.PublicKey(verificationKey.Public), sigHash[:], s.Signature) { diff --git a/tka/sig_test.go b/tka/sig_test.go index cf7ec8e91..c757e8049 100644 --- a/tka/sig_test.go +++ b/tka/sig_test.go @@ -23,11 +23,11 @@ func TestSigDirect(t *testing.T) { KeyID: key.ID(), Pubkey: nodeKeyPub, } - sigHash := sig.sigHash() + sigHash := sig.SigHash() sig.Signature = ed25519.Sign(priv, sigHash[:]) - if sig.sigHash() != sigHash { - t.Errorf("sigHash changed after signing: %x != %x", sig.sigHash(), sigHash) + if sig.SigHash() != sigHash { + t.Errorf("sigHash changed after signing: %x != %x", sig.SigHash(), sigHash) } if err := sig.verifySignature(key); err != nil { @@ -44,7 +44,7 @@ func TestSigSerializeUnserialize(t *testing.T) { KeyID: key.ID(), Pubkey: nodeKeyPub, } - sigHash := sig.sigHash() + sigHash := sig.SigHash() sig.Signature = ed25519.Sign(priv, sigHash[:]) var decoded NodeKeySignature diff --git a/types/key/nl.go b/types/key/nl.go index 5d3144658..5bede831b 100644 --- a/types/key/nl.go +++ b/types/key/nl.go @@ -82,7 +82,7 @@ func (k NLPrivate) KeyID() tkatype.KeyID { return pub[:] } -// SignAUM implements tka.UpdateSigner. +// SignAUM implements tka.Signer. func (k NLPrivate) SignAUM(sigHash tkatype.AUMSigHash) ([]tkatype.Signature, error) { return []tkatype.Signature{{ KeyID: k.KeyID(), @@ -90,6 +90,11 @@ func (k NLPrivate) SignAUM(sigHash tkatype.AUMSigHash) ([]tkatype.Signature, err }}, nil } +// SignNKS signs the tka.NodeKeySignature identified by sigHash. +func (k NLPrivate) SignNKS(sigHash tkatype.NKSSigHash) ([]byte, error) { + return ed25519.Sign(ed25519.PrivateKey(k.k[:]), sigHash[:]), nil +} + // NLPublic is the public portion of a a NLPrivate. type NLPublic struct { k [ed25519.PublicKeySize]byte diff --git a/types/tkatype/tkatype.go b/types/tkatype/tkatype.go index c20ab119a..8b60f3762 100644 --- a/types/tkatype/tkatype.go +++ b/types/tkatype/tkatype.go @@ -22,10 +22,17 @@ type KeyID []byte // MarshaledSignature represents a marshaled tka.NodeKeySignature. type MarshaledSignature []byte +// MarshaledAUM represents a marshaled tka.AUM. +type MarshaledAUM []byte + // AUMSigHash represents the BLAKE2s digest of an Authority Update // Message (AUM), sans any signatures. type AUMSigHash [32]byte +// NKSSigHash represents the BLAKE2s digest of a Node-Key Signature (NKS), +// sans the Signature field if present. +type NKSSigHash [32]byte + // Signature describes a signature over an AUM, which can be verified // using the key referenced by KeyID. type Signature struct { diff --git a/types/tkatype/tkatype_test.go b/types/tkatype/tkatype_test.go index 59153fb59..78a9865d4 100644 --- a/types/tkatype/tkatype_test.go +++ b/types/tkatype/tkatype_test.go @@ -14,4 +14,9 @@ func TestSigHashSize(t *testing.T) { if len(sigHash) != blake2s.Size { t.Errorf("AUMSigHash is wrong size: got %d, want %d", len(sigHash), blake2s.Size) } + + var nksHash NKSSigHash + if len(nksHash) != blake2s.Size { + t.Errorf("NKSSigHash is wrong size: got %d, want %d", len(nksHash), blake2s.Size) + } }