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 <tom@tailscale.com>
This commit is contained in:
Tom DNetto 2022-08-11 10:43:09 -07:00 committed by Tom
parent 18edd79421
commit facafd8819
18 changed files with 514 additions and 13 deletions

View File

@ -36,6 +36,7 @@
"tailscale.com/paths" "tailscale.com/paths"
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tka"
) )
// defaultLocalClient is the default LocalClient when using the legacy // 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 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 // tailscaledConnectHint gives a little thing about why tailscaled (or
// platform equivalent) is not answering localapi connections. // platform equivalent) is not answering localapi connections.
// //

View File

@ -1,9 +1,13 @@
tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depaware) 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 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/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy 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/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/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink 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/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket 💣 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/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/wgengine/filter go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ 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/safesocket from tailscale.com/client/tailscale
tailscale.com/syncs from tailscale.com/cmd/derper+ tailscale.com/syncs from tailscale.com/cmd/derper+
tailscale.com/tailcfg from tailscale.com/client/tailscale+ 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 W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate 💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter 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 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 from golang.org/x/crypto/acme/autocert
golang.org/x/crypto/acme/autocert from tailscale.com/cmd/derper 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/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls golang.org/x/crypto/chacha20poly1305 from crypto/tls
golang.org/x/crypto/cryptobyte from crypto/ecdsa+ 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+ embed from crypto/internal/nistec+
encoding from encoding/json+ encoding from encoding/json+
encoding/asn1 from crypto/x509+ encoding/asn1 from crypto/x509+
encoding/base32 from tailscale.com/tka
encoding/base64 from encoding/json+ encoding/base64 from encoding/json+
encoding/binary from compress/gzip+ encoding/binary from compress/gzip+
encoding/hex from crypto/x509+ encoding/hex from crypto/x509+

View File

@ -169,6 +169,7 @@ func Run(args []string) (err error) {
fileCmd, fileCmd,
bugReportCmd, bugReportCmd,
certCmd, certCmd,
netlockCmd,
}, },
FlagSet: rootfs, FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp }, Exec: func(context.Context, []string) error { return flag.ErrHelp },

View File

@ -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
}

View File

@ -1,9 +1,13 @@
tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/depaware) 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 from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common 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 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/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/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink 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/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli 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/mem from tailscale.com/derp+
go4.org/netipx from tailscale.com/wgengine/filter go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ 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/safesocket from tailscale.com/cmd/tailscale/cli+
tailscale.com/syncs from tailscale.com/net/netcheck+ tailscale.com/syncs from tailscale.com/net/netcheck+
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+ 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 W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate 💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter 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 from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro 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 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/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2s from tailscale.com/control/controlbase 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/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls+ golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+ 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+ embed from tailscale.com/cmd/tailscale/cli+
encoding from encoding/json+ encoding from encoding/json+
encoding/asn1 from crypto/x509+ encoding/asn1 from crypto/x509+
encoding/base32 from tailscale.com/tka
encoding/base64 from encoding/json+ encoding/base64 from encoding/json+
encoding/binary from compress/gzip+ encoding/binary from compress/gzip+
encoding/hex from crypto/x509+ encoding/hex from crypto/x509+

View File

@ -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
}
}

View File

@ -67,6 +67,21 @@ type Status struct {
User map[tailcfg.UserID]tailcfg.UserProfile 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"). // TailnetStatus is information about a Tailscale network ("tailnet").
type TailnetStatus struct { type TailnetStatus struct {
// Name is the name of the network that's currently in use. // Name is the name of the network that's currently in use.

View File

@ -31,6 +31,7 @@
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/version" "tailscale.com/version"
@ -150,6 +151,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveIDToken(w, r) h.serveIDToken(w, r)
case "/localapi/v0/upload-client-metrics": case "/localapi/v0/upload-client-metrics":
h.serveUploadClientMetrics(w, r) h.serveUploadClientMetrics(w, r)
case "/localapi/v0/tka/status":
h.serveTkaStatus(w, r)
case "/localapi/v0/tka/init":
h.serveTkaInit(w, r)
case "/": case "/":
io.WriteString(w, "tailscaled\n") io.WriteString(w, "tailscaled\n")
default: default:
@ -791,6 +796,58 @@ type clientMetricJSON struct {
json.NewEncoder(w).Encode(struct{}{}) 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 { func defBool(a string, def bool) bool {
if a == "" { if a == "" {
return def return def

View File

@ -1826,6 +1826,32 @@ type PeerChange struct {
Capabilities *[]string `json:",omitempty"` 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 // DerpMagicIP is a fake WireGuard endpoint IP address that means to
// use DERP. When used (in the Node.DERP field), the port number of // use DERP. When used (in the Node.DERP field), the port number of
// the WireGuard endpoint is the DERP region ID number to use. // the WireGuard endpoint is the DERP region ID number to use.

View File

@ -216,7 +216,7 @@ func (a *AUM) StaticValidate() error {
// We would implement encoding.BinaryMarshaler, except that would // We would implement encoding.BinaryMarshaler, except that would
// unfortunately get called by the cbor marshaller resulting in infinite // unfortunately get called by the cbor marshaller resulting in infinite
// recursion. // recursion.
func (a *AUM) Serialize() []byte { func (a *AUM) Serialize() tkatype.MarshaledAUM {
// Why CBOR and not something like JSON? // Why CBOR and not something like JSON?
// //
// The main function of an AUM is to carry signed data. Signatures are // The main function of an AUM is to carry signed data. Signatures are

View File

@ -158,7 +158,7 @@ func TestSerialization(t *testing.T) {
for _, tc := range tcs { for _, tc := range tcs {
t.Run(tc.Name, func(t *testing.T) { 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 != "" { if diff := cmp.Diff(tc.Expect, data); diff != "" {
t.Errorf("serialization differs (-want, +got):\n%s", diff) t.Errorf("serialization differs (-want, +got):\n%s", diff)
} }

View File

@ -12,6 +12,7 @@
// Types implementing Signer can sign update messages. // Types implementing Signer can sign update messages.
type Signer interface { type Signer interface {
// SignAUM returns signatures for the AUM encoded by the given AUMSigHash.
SignAUM(tkatype.AUMSigHash) ([]tkatype.Signature, error) SignAUM(tkatype.AUMSigHash) ([]tkatype.Signature, error)
} }

View File

@ -91,6 +91,9 @@ func (k Key) StaticValidate() error {
if k.Votes > 4096 { if k.Votes > 4096 {
return fmt.Errorf("excessive key weight: %d > 4096", k.Votes) 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 // We have an arbitrary upper limit on the amount
// of metadata that can be associated with a key, so // of metadata that can be associated with a key, so

View File

@ -55,13 +55,13 @@ type NodeKeySignature struct {
Signature []byte `cbor:"4,keyasint,omitempty"` Signature []byte `cbor:"4,keyasint,omitempty"`
} }
// sigHash returns the cryptographic digest which a signature // SigHash returns the cryptographic digest which a signature
// is over. // is over.
// //
// This is a hash of the serialized structure, sans the signature. // This is a hash of the serialized structure, sans the signature.
// Without this exclusion, the hash used for the signature // Without this exclusion, the hash used for the signature
// would be circularly dependent on 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 := s
dupe.Signature = nil dupe.Signature = nil
return blake2s.Sum256(dupe.Serialize()) 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 // verifySignature checks that the NodeKeySignature is authentic and certified
// by the given verificationKey. // by the given verificationKey.
func (s *NodeKeySignature) verifySignature(verificationKey Key) error { func (s *NodeKeySignature) verifySignature(verificationKey Key) error {
sigHash := s.sigHash() sigHash := s.SigHash()
switch verificationKey.Kind { switch verificationKey.Kind {
case Key25519: case Key25519:
if ed25519consensus.Verify(ed25519.PublicKey(verificationKey.Public), sigHash[:], s.Signature) { if ed25519consensus.Verify(ed25519.PublicKey(verificationKey.Public), sigHash[:], s.Signature) {

View File

@ -23,11 +23,11 @@ func TestSigDirect(t *testing.T) {
KeyID: key.ID(), KeyID: key.ID(),
Pubkey: nodeKeyPub, Pubkey: nodeKeyPub,
} }
sigHash := sig.sigHash() sigHash := sig.SigHash()
sig.Signature = ed25519.Sign(priv, sigHash[:]) sig.Signature = ed25519.Sign(priv, sigHash[:])
if sig.sigHash() != sigHash { if sig.SigHash() != sigHash {
t.Errorf("sigHash changed after signing: %x != %x", sig.sigHash(), sigHash) t.Errorf("sigHash changed after signing: %x != %x", sig.SigHash(), sigHash)
} }
if err := sig.verifySignature(key); err != nil { if err := sig.verifySignature(key); err != nil {
@ -44,7 +44,7 @@ func TestSigSerializeUnserialize(t *testing.T) {
KeyID: key.ID(), KeyID: key.ID(),
Pubkey: nodeKeyPub, Pubkey: nodeKeyPub,
} }
sigHash := sig.sigHash() sigHash := sig.SigHash()
sig.Signature = ed25519.Sign(priv, sigHash[:]) sig.Signature = ed25519.Sign(priv, sigHash[:])
var decoded NodeKeySignature var decoded NodeKeySignature

View File

@ -82,7 +82,7 @@ func (k NLPrivate) KeyID() tkatype.KeyID {
return pub[:] return pub[:]
} }
// SignAUM implements tka.UpdateSigner. // SignAUM implements tka.Signer.
func (k NLPrivate) SignAUM(sigHash tkatype.AUMSigHash) ([]tkatype.Signature, error) { func (k NLPrivate) SignAUM(sigHash tkatype.AUMSigHash) ([]tkatype.Signature, error) {
return []tkatype.Signature{{ return []tkatype.Signature{{
KeyID: k.KeyID(), KeyID: k.KeyID(),
@ -90,6 +90,11 @@ func (k NLPrivate) SignAUM(sigHash tkatype.AUMSigHash) ([]tkatype.Signature, err
}}, nil }}, 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. // NLPublic is the public portion of a a NLPrivate.
type NLPublic struct { type NLPublic struct {
k [ed25519.PublicKeySize]byte k [ed25519.PublicKeySize]byte

View File

@ -22,10 +22,17 @@
// MarshaledSignature represents a marshaled tka.NodeKeySignature. // MarshaledSignature represents a marshaled tka.NodeKeySignature.
type MarshaledSignature []byte type MarshaledSignature []byte
// MarshaledAUM represents a marshaled tka.AUM.
type MarshaledAUM []byte
// AUMSigHash represents the BLAKE2s digest of an Authority Update // AUMSigHash represents the BLAKE2s digest of an Authority Update
// Message (AUM), sans any signatures. // Message (AUM), sans any signatures.
type AUMSigHash [32]byte 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 // Signature describes a signature over an AUM, which can be verified
// using the key referenced by KeyID. // using the key referenced by KeyID.
type Signature struct { type Signature struct {

View File

@ -14,4 +14,9 @@ func TestSigHashSize(t *testing.T) {
if len(sigHash) != blake2s.Size { if len(sigHash) != blake2s.Size {
t.Errorf("AUMSigHash is wrong size: got %d, want %d", 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)
}
} }