cmd/tailscale,ipn: implement lock sign command

Signed-off-by: Tom DNetto <tom@tailscale.com>
pull/6175/head
Tom DNetto 2 years ago committed by Tom
parent 7d6775b082
commit 0af57fce4c

@ -36,6 +36,7 @@ import (
"tailscale.com/safesocket" "tailscale.com/safesocket"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tka" "tailscale.com/tka"
"tailscale.com/types/key"
) )
// defaultLocalClient is the default LocalClient when using the legacy // defaultLocalClient is the default LocalClient when using the legacy
@ -827,6 +828,25 @@ func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKey
return pr, nil return pr, nil
} }
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
// rotationPublic, if specified, must be an ed25519 public key.
func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
var b bytes.Buffer
type signRequest struct {
NodeKey key.NodePublic
RotationPublic []byte
}
if err := json.NewEncoder(&b).Encode(signRequest{NodeKey: nodeKey, RotationPublic: rotationPublic}); err != nil {
return err
}
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/sign", 200, &b); err != nil {
return fmt.Errorf("error: %w", err)
}
return 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.
// //

@ -26,6 +26,7 @@ var netlockCmd = &ffcli.Command{
nlStatusCmd, nlStatusCmd,
nlAddCmd, nlAddCmd,
nlRemoveCmd, nlRemoveCmd,
nlSignCmd,
}, },
Exec: runNetworkLockStatus, Exec: runNetworkLockStatus,
} }
@ -163,3 +164,27 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
fmt.Printf("Status: %+v\n\n", status) fmt.Printf("Status: %+v\n\n", status)
return nil return nil
} }
var nlSignCmd = &ffcli.Command{
Name: "sign",
ShortUsage: "sign <node-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
if err := nodeKey.UnmarshalText([]byte(args[0])); err != nil {
return fmt.Errorf("decoding node-key: %w", err)
}
return localClient.NetworkLockSign(ctx, nodeKey, nil)
default:
return errors.New("expected a single node-key as only argument")
}
}

@ -430,6 +430,47 @@ func (b *LocalBackend) NetworkLockKeyTrustedForTest(keyID tkatype.KeyID) bool {
return b.tka.authority.KeyTrusted(keyID) return b.tka.authority.KeyTrusted(keyID)
} }
// NetworkLockSign signs the given node-key and submits it to the control plane.
// rotationPublic, if specified, must be an ed25519 public key.
func (b *LocalBackend) NetworkLockSign(nodeKey key.NodePublic, rotationPublic []byte) error {
ourNodeKey, sig, err := func(nodeKey key.NodePublic, rotationPublic []byte) (key.NodePublic, tka.NodeKeySignature, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return key.NodePublic{}, tka.NodeKeySignature{}, errNetworkLockNotActive
}
if !b.tka.authority.KeyTrusted(b.nlPrivKey.KeyID()) {
return key.NodePublic{}, tka.NodeKeySignature{}, errors.New("this node is not trusted by network lock")
}
p, err := nodeKey.MarshalBinary()
if err != nil {
return key.NodePublic{}, tka.NodeKeySignature{}, err
}
sig := tka.NodeKeySignature{
SigKind: tka.SigDirect,
KeyID: b.nlPrivKey.KeyID(),
Pubkey: p,
WrappingPubkey: rotationPublic,
}
sig.Signature, err = b.nlPrivKey.SignNKS(sig.SigHash())
if err != nil {
return key.NodePublic{}, tka.NodeKeySignature{}, fmt.Errorf("signature failed: %w", err)
}
return b.prefs.Persist().PublicNodeKey(), sig, nil
}(nodeKey, rotationPublic)
if err != nil {
return err
}
b.logf("Generated network-lock signature for %v, submitting to control plane", nodeKey)
if _, err := b.tkaSubmitSignature(ourNodeKey, sig.Serialize()); err != nil {
return err
}
return nil
}
// NetworkLockModify adds and/or removes keys in the tailnet's key authority. // NetworkLockModify adds and/or removes keys in the tailnet's key authority.
func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err error) { func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err error) {
defer func() { defer func() {
@ -817,3 +858,39 @@ func (b *LocalBackend) tkaDoDisablement(ourNodeKey key.NodePublic, head tka.AUMH
return a, nil return a, nil
} }
func (b *LocalBackend) tkaSubmitSignature(ourNodeKey key.NodePublic, sig tkatype.MarshaledSignature) (*tailcfg.TKASubmitSignatureResponse, error) {
var req bytes.Buffer
if err := json.NewEncoder(&req).Encode(tailcfg.TKASubmitSignatureRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: ourNodeKey,
Signature: sig,
}); err != nil {
return nil, fmt.Errorf("encoding request: %v", err)
}
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sign", &req)
if err != nil {
return nil, fmt.Errorf("req: %w", err)
}
res, err := b.DoNoiseRequest(req2)
if err != nil {
return nil, fmt.Errorf("resp: %w", err)
}
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.TKASubmitSignatureResponse)
err = json.NewDecoder(&io.LimitedReader{R: res.Body, N: 1024 * 1024}).Decode(a)
res.Body.Close()
if err != nil {
return nil, fmt.Errorf("decoding JSON: %w", err)
}
return a, nil
}

@ -629,3 +629,83 @@ func TestTKADisable(t *testing.T) {
t.Errorf("NetworkLockDisable() failed: %v", err) t.Errorf("NetworkLockDisable() failed: %v", err)
} }
} }
func TestTKASign(t *testing.T) {
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
temp := t.TempDir()
os.Mkdir(filepath.Join(temp, "tka"), 0755)
nodePriv := key.NewNode()
toSign := key.NewNode()
// Make a fake TKA authority, to seed local state.
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
nlPriv := key.NewNLPrivate()
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
if err != nil {
t.Fatal(err)
}
authority, _, err := tka.Create(chonk, tka.State{
Keys: []tka.Key{key},
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
}, nlPriv)
if err != nil {
t.Fatalf("tka.Create() failed: %v", err)
}
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
switch r.URL.Path {
case "/machine/tka/sign":
body := new(tailcfg.TKASubmitSignatureRequest)
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
t.Fatal(err)
}
if body.Version != tailcfg.CurrentCapabilityVersion {
t.Errorf("sign CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
}
if body.NodeKey != nodePriv.Public() {
t.Errorf("nodeKey = %v, want %v", body.NodeKey, nodePriv.Public())
}
var sig tka.NodeKeySignature
if err := sig.Unserialize(body.Signature); err != nil {
t.Fatalf("malformed signature: %v", err)
}
if err := authority.NodeKeyAuthorized(toSign.Public(), body.Signature); err != nil {
t.Errorf("signature does not verify: %v", err)
}
w.WriteHeader(200)
if err := json.NewEncoder(w).Encode(tailcfg.TKASubmitSignatureResponse{}); err != nil {
t.Fatal(err)
}
default:
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
w.WriteHeader(404)
}
}))
defer ts.Close()
cc := fakeControlClient(t, client)
b := LocalBackend{
varRoot: temp,
cc: cc,
ccAuto: cc,
logf: t.Logf,
tka: &tkaState{
authority: authority,
storage: chonk,
},
prefs: (&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
}).View(),
nlPrivKey: nlPriv,
}
if err := b.NetworkLockSign(toSign.Public(), nil); err != nil {
t.Errorf("NetworkLockSign() failed: %v", err)
}
}

@ -34,6 +34,7 @@ import (
"tailscale.com/net/netutil" "tailscale.com/net/netutil"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/tka" "tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/mak" "tailscale.com/util/mak"
@ -75,6 +76,7 @@ var handler = map[string]localAPIHandler{
"status": (*Handler).serveStatus, "status": (*Handler).serveStatus,
"tka/init": (*Handler).serveTKAInit, "tka/init": (*Handler).serveTKAInit,
"tka/modify": (*Handler).serveTKAModify, "tka/modify": (*Handler).serveTKAModify,
"tka/sign": (*Handler).serveTKASign,
"tka/status": (*Handler).serveTKAStatus, "tka/status": (*Handler).serveTKAStatus,
"upload-client-metrics": (*Handler).serveUploadClientMetrics, "upload-client-metrics": (*Handler).serveUploadClientMetrics,
"whois": (*Handler).serveWhoIs, "whois": (*Handler).serveWhoIs,
@ -921,6 +923,34 @@ func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
w.Write(j) w.Write(j)
} }
func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "lock status access denied", http.StatusForbidden)
return
}
if r.Method != http.MethodPost {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
type signRequest struct {
NodeKey key.NodePublic
RotationPublic []byte
}
var req signRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return
}
if err := h.b.NetworkLockSign(req.NodeKey, req.RotationPublic); err != nil {
http.Error(w, "signing failed: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite { if !h.PermitWrite {
http.Error(w, "lock init access denied", http.StatusForbidden) http.Error(w, "lock init access denied", http.StatusForbidden)

@ -214,3 +214,23 @@ type TKADisableRequest struct {
type TKADisableResponse struct { type TKADisableResponse struct {
// Nothing. (yet?) // Nothing. (yet?)
} }
// TKASubmitSignatureRequest transmits a node-key signature to the control plane.
//
// This is the request schema for a /tka/sign noise RPC.
type TKASubmitSignatureRequest struct {
// Version is the client's capabilities.
Version CapabilityVersion
// NodeKey is the client's current node key. The node-key which
// is being signed is embedded in Signature.
NodeKey key.NodePublic
// Signature encodes the node-key signature being submitted.
Signature tkatype.MarshaledSignature
}
// TKASubmitSignatureResponse is the JSON response from a /tka/sign RPC.
type TKASubmitSignatureResponse struct {
// Nothing. (yet?)
}

Loading…
Cancel
Save