Updates #17115 Change-Id: I6b083c0db4c4d359e49eb129d626b7f128f0a9d2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>main
parent
0e3d942e39
commit
3a49b7464c
@ -0,0 +1,204 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
|
||||
package local |
||||
|
||||
import ( |
||||
"bytes" |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/url" |
||||
|
||||
"tailscale.com/ipn/ipnstate" |
||||
"tailscale.com/tka" |
||||
"tailscale.com/types/key" |
||||
"tailscale.com/types/tkatype" |
||||
) |
||||
|
||||
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
|
||||
func (lc *Client) 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) |
||||
} |
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body) |
||||
} |
||||
|
||||
// NetworkLockInit initializes the tailnet key authority.
|
||||
//
|
||||
// TODO(tom): Plumb through disablement secrets.
|
||||
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) { |
||||
var b bytes.Buffer |
||||
type initRequest struct { |
||||
Keys []tka.Key |
||||
DisablementValues [][]byte |
||||
SupportDisablement []byte |
||||
} |
||||
|
||||
if err := json.NewEncoder(&b).Encode(initRequest{Keys: keys, DisablementValues: disablementValues, SupportDisablement: supportDisablement}); 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) |
||||
} |
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body) |
||||
} |
||||
|
||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||
// enable unattended bringup in the locked tailnet.
|
||||
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) { |
||||
encodedPrivate, err := tkaKey.MarshalText() |
||||
if err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
var b bytes.Buffer |
||||
type wrapRequest struct { |
||||
TSKey string |
||||
TKAKey string // key.NLPrivate.MarshalText
|
||||
} |
||||
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil { |
||||
return "", err |
||||
} |
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b) |
||||
if err != nil { |
||||
return "", fmt.Errorf("error: %w", err) |
||||
} |
||||
return string(body), nil |
||||
} |
||||
|
||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error { |
||||
var b bytes.Buffer |
||||
type modifyRequest struct { |
||||
AddKeys []tka.Key |
||||
RemoveKeys []tka.Key |
||||
} |
||||
|
||||
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 204, &b); err != nil { |
||||
return fmt.Errorf("error: %w", err) |
||||
} |
||||
return 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 *Client) 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 |
||||
} |
||||
|
||||
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
|
||||
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) { |
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("error: %w", err) |
||||
} |
||||
return decodeJSON[[]tkatype.MarshaledSignature](body) |
||||
} |
||||
|
||||
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) { |
||||
v := url.Values{} |
||||
v.Set("limit", fmt.Sprint(maxEntries)) |
||||
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("error %w: %s", err, body) |
||||
} |
||||
return decodeJSON[[]ipnstate.NetworkLockUpdate](body) |
||||
} |
||||
|
||||
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
|
||||
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error { |
||||
// This endpoint expects an empty JSON stanza as the payload.
|
||||
var b bytes.Buffer |
||||
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/force-local-disable", 200, &b); err != nil { |
||||
return fmt.Errorf("error: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
|
||||
// in url and returns information extracted from it.
|
||||
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) { |
||||
vr := struct { |
||||
URL string |
||||
}{url} |
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("sending verify-deeplink: %w", err) |
||||
} |
||||
|
||||
return decodeJSON[*tka.DeeplinkValidationResult](body) |
||||
} |
||||
|
||||
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
|
||||
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) { |
||||
vr := struct { |
||||
Keys []tkatype.KeyID |
||||
ForkFrom string |
||||
}{removeKeys, forkFrom.String()} |
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr)) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err) |
||||
} |
||||
|
||||
return body, nil |
||||
} |
||||
|
||||
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
|
||||
func (lc *Client) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) { |
||||
r := bytes.NewReader(aum.Serialize()) |
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err) |
||||
} |
||||
|
||||
return body, nil |
||||
} |
||||
|
||||
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
|
||||
func (lc *Client) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error { |
||||
r := bytes.NewReader(aum.Serialize()) |
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r) |
||||
if err != nil { |
||||
return fmt.Errorf("sending cosign-recovery-aum: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// NetworkLockDisable shuts down network-lock across the tailnet.
|
||||
func (lc *Client) 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 |
||||
} |
||||
@ -0,0 +1,31 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_tailnetlock
|
||||
|
||||
package ipnlocal |
||||
|
||||
import ( |
||||
"tailscale.com/ipn" |
||||
"tailscale.com/ipn/ipnstate" |
||||
"tailscale.com/tka" |
||||
"tailscale.com/types/netmap" |
||||
) |
||||
|
||||
type tkaState struct { |
||||
authority *tka.Authority |
||||
} |
||||
|
||||
func (b *LocalBackend) initTKALocked() error { |
||||
return nil |
||||
} |
||||
|
||||
func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsView) error { |
||||
return nil |
||||
} |
||||
|
||||
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {} |
||||
|
||||
func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus { |
||||
return &ipnstate.NetworkLockStatus{Enabled: false} |
||||
} |
||||
@ -0,0 +1,413 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_tailnetlock
|
||||
|
||||
package localapi |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"io" |
||||
"net/http" |
||||
"strconv" |
||||
|
||||
"tailscale.com/tka" |
||||
"tailscale.com/types/key" |
||||
"tailscale.com/types/tkatype" |
||||
"tailscale.com/util/httpm" |
||||
) |
||||
|
||||
func init() { |
||||
handler["tka/affected-sigs"] = (*Handler).serveTKAAffectedSigs |
||||
handler["tka/cosign-recovery-aum"] = (*Handler).serveTKACosignRecoveryAUM |
||||
handler["tka/disable"] = (*Handler).serveTKADisable |
||||
handler["tka/force-local-disable"] = (*Handler).serveTKALocalDisable |
||||
handler["tka/generate-recovery-aum"] = (*Handler).serveTKAGenerateRecoveryAUM |
||||
handler["tka/init"] = (*Handler).serveTKAInit |
||||
handler["tka/log"] = (*Handler).serveTKALog |
||||
handler["tka/modify"] = (*Handler).serveTKAModify |
||||
handler["tka/sign"] = (*Handler).serveTKASign |
||||
handler["tka/status"] = (*Handler).serveTKAStatus |
||||
handler["tka/submit-recovery-aum"] = (*Handler).serveTKASubmitRecoveryAUM |
||||
handler["tka/verify-deeplink"] = (*Handler).serveTKAVerifySigningDeeplink |
||||
handler["tka/wrap-preauth-key"] = (*Handler).serveTKAWrapPreauthKey |
||||
} |
||||
|
||||
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 != httpm.GET { |
||||
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", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", "application/json") |
||||
w.Write(j) |
||||
} |
||||
|
||||
func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) { |
||||
if !h.PermitWrite { |
||||
http.Error(w, "lock sign access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
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) { |
||||
if !h.PermitWrite { |
||||
http.Error(w, "lock init access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
type initRequest struct { |
||||
Keys []tka.Key |
||||
DisablementValues [][]byte |
||||
SupportDisablement []byte |
||||
} |
||||
var req initRequest |
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if !h.b.NetworkLockAllowed() { |
||||
http.Error(w, "Tailnet Lock is not supported on your pricing plan", http.StatusForbidden) |
||||
return |
||||
} |
||||
|
||||
if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues, req.SupportDisablement); 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", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", "application/json") |
||||
w.Write(j) |
||||
} |
||||
|
||||
func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) { |
||||
if !h.PermitWrite { |
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
type modifyRequest struct { |
||||
AddKeys []tka.Key |
||||
RemoveKeys []tka.Key |
||||
} |
||||
var req modifyRequest |
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil { |
||||
http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.WriteHeader(204) |
||||
} |
||||
|
||||
func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request) { |
||||
if !h.PermitWrite { |
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
type wrapRequest struct { |
||||
TSKey string |
||||
TKAKey string // key.NLPrivate.MarshalText
|
||||
} |
||||
var req wrapRequest |
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 12*1024)).Decode(&req); err != nil { |
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest) |
||||
return |
||||
} |
||||
var priv key.NLPrivate |
||||
if err := priv.UnmarshalText([]byte(req.TKAKey)); err != nil { |
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
wrappedKey, err := h.b.NetworkLockWrapPreauthKey(req.TSKey, priv) |
||||
if err != nil { |
||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.WriteHeader(http.StatusOK) |
||||
w.Write([]byte(wrappedKey)) |
||||
} |
||||
|
||||
func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.Request) { |
||||
if !h.PermitRead { |
||||
http.Error(w, "signing deeplink verification access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
type verifyRequest struct { |
||||
URL string |
||||
} |
||||
var req verifyRequest |
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
||||
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
res := h.b.NetworkLockVerifySigningDeeplink(req.URL) |
||||
j, err := json.MarshalIndent(res, "", "\t") |
||||
if err != nil { |
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", "application/json") |
||||
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 != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
body := io.LimitReader(r.Body, 1024*1024) |
||||
secret, err := io.ReadAll(body) |
||||
if err != nil { |
||||
http.Error(w, "reading secret", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if err := h.b.NetworkLockDisable(secret); err != nil { |
||||
http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest) |
||||
return |
||||
} |
||||
w.WriteHeader(http.StatusOK) |
||||
} |
||||
|
||||
func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) { |
||||
if !h.PermitWrite { |
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
// Require a JSON stanza for the body as an additional CSRF protection.
|
||||
var req struct{} |
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
||||
http.Error(w, "invalid JSON body", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if err := h.b.NetworkLockForceLocalDisable(); err != nil { |
||||
http.Error(w, "network-lock local disable failed: "+err.Error(), http.StatusBadRequest) |
||||
return |
||||
} |
||||
w.WriteHeader(http.StatusOK) |
||||
} |
||||
|
||||
func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != httpm.GET { |
||||
http.Error(w, "use GET", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
limit := 50 |
||||
if limitStr := r.FormValue("limit"); limitStr != "" { |
||||
l, err := strconv.Atoi(limitStr) |
||||
if err != nil { |
||||
http.Error(w, "parsing 'limit' parameter: "+err.Error(), http.StatusBadRequest) |
||||
return |
||||
} |
||||
limit = int(l) |
||||
} |
||||
|
||||
updates, err := h.b.NetworkLockLog(limit) |
||||
if err != nil { |
||||
http.Error(w, "reading log failed: "+err.Error(), http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
j, err := json.MarshalIndent(updates, "", "\t") |
||||
if err != nil { |
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", "application/json") |
||||
w.Write(j) |
||||
} |
||||
|
||||
func (h *Handler) serveTKAAffectedSigs(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
keyID, err := io.ReadAll(http.MaxBytesReader(w, r.Body, 2048)) |
||||
if err != nil { |
||||
http.Error(w, "reading body", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
sigs, err := h.b.NetworkLockAffectedSigs(keyID) |
||||
if err != nil { |
||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
j, err := json.MarshalIndent(sigs, "", "\t") |
||||
if err != nil { |
||||
http.Error(w, "JSON encoding error", http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", "application/json") |
||||
w.Write(j) |
||||
} |
||||
|
||||
func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Request) { |
||||
if !h.PermitWrite { |
||||
http.Error(w, "access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
type verifyRequest struct { |
||||
Keys []tkatype.KeyID |
||||
ForkFrom string |
||||
} |
||||
var req verifyRequest |
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { |
||||
http.Error(w, "invalid JSON for verifyRequest body", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
var forkFrom tka.AUMHash |
||||
if req.ForkFrom != "" { |
||||
if err := forkFrom.UnmarshalText([]byte(req.ForkFrom)); err != nil { |
||||
http.Error(w, "decoding fork-from: "+err.Error(), http.StatusBadRequest) |
||||
return |
||||
} |
||||
} |
||||
|
||||
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom) |
||||
if err != nil { |
||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", "application/octet-stream") |
||||
w.Write(res.Serialize()) |
||||
} |
||||
|
||||
func (h *Handler) serveTKACosignRecoveryAUM(w http.ResponseWriter, r *http.Request) { |
||||
if !h.PermitWrite { |
||||
http.Error(w, "access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
body := io.LimitReader(r.Body, 1024*1024) |
||||
aumBytes, err := io.ReadAll(body) |
||||
if err != nil { |
||||
http.Error(w, "reading AUM", http.StatusBadRequest) |
||||
return |
||||
} |
||||
var aum tka.AUM |
||||
if err := aum.Unserialize(aumBytes); err != nil { |
||||
http.Error(w, "decoding AUM", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
res, err := h.b.NetworkLockCosignRecoveryAUM(&aum) |
||||
if err != nil { |
||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.Header().Set("Content-Type", "application/octet-stream") |
||||
w.Write(res.Serialize()) |
||||
} |
||||
|
||||
func (h *Handler) serveTKASubmitRecoveryAUM(w http.ResponseWriter, r *http.Request) { |
||||
if !h.PermitWrite { |
||||
http.Error(w, "access denied", http.StatusForbidden) |
||||
return |
||||
} |
||||
if r.Method != httpm.POST { |
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
body := io.LimitReader(r.Body, 1024*1024) |
||||
aumBytes, err := io.ReadAll(body) |
||||
if err != nil { |
||||
http.Error(w, "reading AUM", http.StatusBadRequest) |
||||
return |
||||
} |
||||
var aum tka.AUM |
||||
if err := aum.Unserialize(aumBytes); err != nil { |
||||
http.Error(w, "decoding AUM", http.StatusBadRequest) |
||||
return |
||||
} |
||||
|
||||
if err := h.b.NetworkLockSubmitRecoveryAUM(&aum); err != nil { |
||||
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
return |
||||
} |
||||
w.WriteHeader(http.StatusOK) |
||||
} |
||||
@ -0,0 +1,149 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_tailnetlock
|
||||
|
||||
package tka |
||||
|
||||
import ( |
||||
"crypto/ed25519" |
||||
"errors" |
||||
|
||||
"tailscale.com/types/key" |
||||
"tailscale.com/types/logger" |
||||
"tailscale.com/types/tkatype" |
||||
) |
||||
|
||||
type Authority struct { |
||||
head AUM |
||||
oldestAncestor AUM |
||||
state State |
||||
} |
||||
|
||||
func (*Authority) Head() AUMHash { return AUMHash{} } |
||||
|
||||
func (AUMHash) MarshalText() ([]byte, error) { return nil, errNoTailnetLock } |
||||
|
||||
type State struct{} |
||||
|
||||
// AUMKind describes valid AUM types.
|
||||
type AUMKind uint8 |
||||
|
||||
type AUMHash [32]byte |
||||
|
||||
type AUM struct { |
||||
MessageKind AUMKind `cbor:"1,keyasint"` |
||||
PrevAUMHash []byte `cbor:"2,keyasint"` |
||||
|
||||
// Key encodes a public key to be added to the key authority.
|
||||
// This field is used for AddKey AUMs.
|
||||
Key *Key `cbor:"3,keyasint,omitempty"` |
||||
|
||||
// KeyID references a public key which is part of the key authority.
|
||||
// This field is used for RemoveKey and UpdateKey AUMs.
|
||||
KeyID tkatype.KeyID `cbor:"4,keyasint,omitempty"` |
||||
|
||||
// State describes the full state of the key authority.
|
||||
// This field is used for Checkpoint AUMs.
|
||||
State *State `cbor:"5,keyasint,omitempty"` |
||||
|
||||
// Votes and Meta describe properties of a key in the key authority.
|
||||
// These fields are used for UpdateKey AUMs.
|
||||
Votes *uint `cbor:"6,keyasint,omitempty"` |
||||
Meta map[string]string `cbor:"7,keyasint,omitempty"` |
||||
|
||||
// Signatures lists the signatures over this AUM.
|
||||
// CBOR key 23 is the last key which can be encoded as a single byte.
|
||||
Signatures []tkatype.Signature `cbor:"23,keyasint,omitempty"` |
||||
} |
||||
|
||||
type Chonk interface { |
||||
// AUM returns the AUM with the specified digest.
|
||||
//
|
||||
// If the AUM does not exist, then os.ErrNotExist is returned.
|
||||
AUM(hash AUMHash) (AUM, error) |
||||
|
||||
// ChildAUMs returns all AUMs with a specified previous
|
||||
// AUM hash.
|
||||
ChildAUMs(prevAUMHash AUMHash) ([]AUM, error) |
||||
|
||||
// CommitVerifiedAUMs durably stores the provided AUMs.
|
||||
// Callers MUST ONLY provide AUMs which are verified (specifically,
|
||||
// a call to aumVerify() must return a nil error).
|
||||
// as the implementation assumes that only verified AUMs are stored.
|
||||
CommitVerifiedAUMs(updates []AUM) error |
||||
|
||||
// Heads returns AUMs for which there are no children. In other
|
||||
// words, the latest AUM in all possible chains (the 'leaves').
|
||||
Heads() ([]AUM, error) |
||||
|
||||
// SetLastActiveAncestor is called to record the oldest-known AUM
|
||||
// that contributed to the current state. This value is used as
|
||||
// a hint on next startup to determine which chain to pick when computing
|
||||
// the current state, if there are multiple distinct chains.
|
||||
SetLastActiveAncestor(hash AUMHash) error |
||||
|
||||
// LastActiveAncestor returns the oldest-known AUM that was (in a
|
||||
// previous run) an ancestor of the current state. This is used
|
||||
// as a hint to pick the correct chain in the event that the Chonk stores
|
||||
// multiple distinct chains.
|
||||
LastActiveAncestor() (*AUMHash, error) |
||||
} |
||||
|
||||
// SigKind describes valid NodeKeySignature types.
|
||||
type SigKind uint8 |
||||
|
||||
type NodeKeySignature struct { |
||||
// SigKind identifies the variety of signature.
|
||||
SigKind SigKind `cbor:"1,keyasint"` |
||||
// Pubkey identifies the key.NodePublic which is being authorized.
|
||||
// SigCredential signatures do not use this field.
|
||||
Pubkey []byte `cbor:"2,keyasint,omitempty"` |
||||
|
||||
// KeyID identifies which key in the tailnet key authority should
|
||||
// be used to verify this signature. Only set for SigDirect and
|
||||
// SigCredential signature kinds.
|
||||
KeyID []byte `cbor:"3,keyasint,omitempty"` |
||||
|
||||
// Signature is the packed (R, S) ed25519 signature over all other
|
||||
// fields of the structure.
|
||||
Signature []byte `cbor:"4,keyasint,omitempty"` |
||||
|
||||
// Nested describes a NodeKeySignature which authorizes the node-key
|
||||
// used as Pubkey. Only used for SigRotation signatures.
|
||||
Nested *NodeKeySignature `cbor:"5,keyasint,omitempty"` |
||||
|
||||
// WrappingPubkey specifies the ed25519 public key which must be used
|
||||
// to sign a Signature which embeds this one.
|
||||
//
|
||||
// For SigRotation signatures multiple levels deep, intermediate
|
||||
// signatures may omit this value, in which case the parent WrappingPubkey
|
||||
// is used.
|
||||
//
|
||||
// SigCredential signatures use this field to specify the public key
|
||||
// they are certifying, following the usual semanticsfor WrappingPubkey.
|
||||
WrappingPubkey []byte `cbor:"6,keyasint,omitempty"` |
||||
} |
||||
|
||||
type DeeplinkValidationResult struct { |
||||
} |
||||
|
||||
func (h *AUMHash) UnmarshalText(text []byte) error { |
||||
return errNoTailnetLock |
||||
} |
||||
|
||||
var errNoTailnetLock = errors.New("tailnet lock is not enabled") |
||||
|
||||
func DecodeWrappedAuthkey(wrappedAuthKey string, logf logger.Logf) (authKey string, isWrapped bool, sig *NodeKeySignature, priv ed25519.PrivateKey) { |
||||
return wrappedAuthKey, false, nil, nil |
||||
} |
||||
|
||||
func ResignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.MarshaledSignature) (tkatype.MarshaledSignature, error) { |
||||
return nil, nil |
||||
} |
||||
|
||||
func SignByCredential(privKey []byte, wrapped *NodeKeySignature, nodeKey key.NodePublic) (tkatype.MarshaledSignature, error) { |
||||
return nil, nil |
||||
} |
||||
|
||||
func (s NodeKeySignature) String() string { return "" } |
||||
Loading…
Reference in new issue