| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413 |
- // 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() {
- Register("tka/affected-sigs", (*Handler).serveTKAAffectedSigs)
- Register("tka/cosign-recovery-aum", (*Handler).serveTKACosignRecoveryAUM)
- Register("tka/disable", (*Handler).serveTKADisable)
- Register("tka/force-local-disable", (*Handler).serveTKALocalDisable)
- Register("tka/generate-recovery-aum", (*Handler).serveTKAGenerateRecoveryAUM)
- Register("tka/init", (*Handler).serveTKAInit)
- Register("tka/log", (*Handler).serveTKALog)
- Register("tka/modify", (*Handler).serveTKAModify)
- Register("tka/sign", (*Handler).serveTKASign)
- Register("tka/status", (*Handler).serveTKAStatus)
- Register("tka/submit-recovery-aum", (*Handler).serveTKASubmitRecoveryAUM)
- Register("tka/verify-deeplink", (*Handler).serveTKAVerifySigningDeeplink)
- Register("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 != "" {
- lm, err := strconv.Atoi(limitStr)
- if err != nil {
- http.Error(w, "parsing 'limit' parameter: "+err.Error(), http.StatusBadRequest)
- return
- }
- limit = int(lm)
- }
- 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)
- }
|