mirror of
https://github.com/tailscale/tailscale.git
synced 2025-09-21 05:31:36 +02:00
Updates #17115 Change-Id: I6b083c0db4c4d359e49eb129d626b7f128f0a9d2 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
414 lines
11 KiB
Go
414 lines
11 KiB
Go
// 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)
|
|
}
|