mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-05 23:07:44 +00:00
tka: provide verify-deeplink local API endpoint (#8303)
* tka: provide verify-deeplink local API endpoint Fixes https://github.com/tailscale/tailscale/issues/8302 Signed-off-by: Andrea Gottardo <andrea@tailscale.com> Address code review comments Signed-off-by: Andrea Gottardo <andrea@tailscale.com> Address code review comments by Ross Signed-off-by: Andrea Gottardo <andrea@tailscale.com> * Improve error encoding, fix logic error Signed-off-by: Andrea Gottardo <andrea@tailscale.com> --------- Signed-off-by: Andrea Gottardo <andrea@tailscale.com>
This commit is contained in:
parent
4dda949760
commit
99f17a7135
@ -887,6 +887,18 @@ func (b *LocalBackend) NetworkLockWrapPreauthKey(preauthKey string, tkaKey key.N
|
||||
return fmt.Sprintf("%s--TL%s-%s", preauthKey, tkaSuffixEncoder.EncodeToString(sig.Serialize()), tkaSuffixEncoder.EncodeToString(priv)), nil
|
||||
}
|
||||
|
||||
// NetworkLockVerifySigningDeeplink asks the authority to verify the given deeplink
|
||||
// URL. See the comment for ValidateDeeplink for details.
|
||||
func (b *LocalBackend) NetworkLockVerifySigningDeeplink(url string) tka.DeeplinkValidationResult {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return tka.DeeplinkValidationResult{IsValid: false, Error: errNetworkLockNotActive.Error()}
|
||||
}
|
||||
|
||||
return b.tka.authority.ValidateDeeplink(url)
|
||||
}
|
||||
|
||||
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||
p, err := nodeInfo.NodePublic.MarshalBinary()
|
||||
if err != nil {
|
||||
|
@ -104,6 +104,7 @@
|
||||
"tka/force-local-disable": (*Handler).serveTKALocalDisable,
|
||||
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
|
||||
"tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey,
|
||||
"tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink,
|
||||
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
|
||||
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
|
||||
"whois": (*Handler).serveWhoIs,
|
||||
@ -1610,6 +1611,35 @@ type wrapRequest struct {
|
||||
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", 400)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
|
||||
j, err := json.MarshalIndent(res, "", "\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) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||
|
167
tka/deeplink.go
Normal file
167
tka/deeplink.go
Normal file
@ -0,0 +1,167 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package tka
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
DeeplinkTailscaleURLScheme = "tailscale"
|
||||
DeeplinkCommandSign = "sign-device"
|
||||
)
|
||||
|
||||
type DeeplinkValidationResult struct {
|
||||
IsValid bool
|
||||
Error string
|
||||
Version uint8
|
||||
NodeKey string
|
||||
TLPub string
|
||||
DeviceName string
|
||||
OSName string
|
||||
EmailAddress string
|
||||
}
|
||||
|
||||
// GenerateHMAC computes a SHA-256 HMAC for the concatenation of components, using
|
||||
// stateID as secret.
|
||||
func generateHMAC(stateID uint64, components []string) []byte {
|
||||
key := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(key, stateID)
|
||||
mac := hmac.New(sha256.New, key)
|
||||
for _, component := range components {
|
||||
mac.Write([]byte(component))
|
||||
}
|
||||
return mac.Sum(nil)
|
||||
}
|
||||
|
||||
// ValidateDeeplink validates a device signing deeplink using the authority's stateID.
|
||||
// The input urlString follows this structure:
|
||||
//
|
||||
// tailscale://sign-device/v1/?nk=xxx&tp=xxx&dn=xxx&os=xxx&em=xxx&hm=xxx
|
||||
//
|
||||
// where:
|
||||
// - "nk" is the nodekey of the node being signed
|
||||
// - "tp" is the tailnet lock public key
|
||||
// - "dn" is the name of the node
|
||||
// - "os" is the operating system of the node
|
||||
// - "em" is the email address associated with the node
|
||||
// - "hm" is a SHA-256 HMAC computed over the concatenation of the above fields, encoded as a hex string
|
||||
func (a *Authority) ValidateDeeplink(urlString string) DeeplinkValidationResult {
|
||||
parsedUrl, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: err.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
if parsedUrl.Scheme != DeeplinkTailscaleURLScheme {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: fmt.Sprintf("unhandled scheme %s, expected %s", parsedUrl.Scheme, DeeplinkTailscaleURLScheme),
|
||||
}
|
||||
}
|
||||
|
||||
if parsedUrl.Host != DeeplinkCommandSign {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: fmt.Sprintf("unhandled host %s, expected %s", parsedUrl.Host, DeeplinkCommandSign),
|
||||
}
|
||||
}
|
||||
|
||||
path := parsedUrl.EscapedPath()
|
||||
pathComponents := strings.Split(path, "/")
|
||||
if len(pathComponents) != 3 {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: "invalid path components number found",
|
||||
}
|
||||
}
|
||||
|
||||
if pathComponents[1] != "v1" {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: fmt.Sprintf("expected v1 deeplink version, found something else: %s", pathComponents[1]),
|
||||
}
|
||||
}
|
||||
|
||||
nodeKey := parsedUrl.Query().Get("nk")
|
||||
if len(nodeKey) == 0 {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: "missing nk (NodeKey) query parameter",
|
||||
}
|
||||
}
|
||||
|
||||
tlPub := parsedUrl.Query().Get("tp")
|
||||
if len(tlPub) == 0 {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: "missing tp (TLPub) query parameter",
|
||||
}
|
||||
}
|
||||
|
||||
deviceName := parsedUrl.Query().Get("dn")
|
||||
if len(deviceName) == 0 {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: "missing dn (DeviceName) query parameter",
|
||||
}
|
||||
}
|
||||
|
||||
osName := parsedUrl.Query().Get("os")
|
||||
if len(deviceName) == 0 {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: "missing os (OSName) query parameter",
|
||||
}
|
||||
}
|
||||
|
||||
emailAddress := parsedUrl.Query().Get("em")
|
||||
if len(emailAddress) == 0 {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: "missing em (EmailAddress) query parameter",
|
||||
}
|
||||
}
|
||||
|
||||
hmacString := parsedUrl.Query().Get("hm")
|
||||
if len(hmacString) == 0 {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: "missing hm (HMAC) query parameter",
|
||||
}
|
||||
}
|
||||
|
||||
components := []string{nodeKey, tlPub, deviceName, osName, emailAddress}
|
||||
stateID1, _ := a.StateIDs()
|
||||
computedHMAC := generateHMAC(stateID1, components)
|
||||
|
||||
hmacHexBytes, err := hex.DecodeString(hmacString)
|
||||
if err != nil {
|
||||
return DeeplinkValidationResult{IsValid: false, Error: "could not hex-decode hmac"}
|
||||
}
|
||||
|
||||
if !hmac.Equal(computedHMAC, hmacHexBytes) {
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: false,
|
||||
Error: "hmac authentication failed",
|
||||
}
|
||||
}
|
||||
|
||||
return DeeplinkValidationResult{
|
||||
IsValid: true,
|
||||
NodeKey: nodeKey,
|
||||
TLPub: tlPub,
|
||||
DeviceName: deviceName,
|
||||
OSName: osName,
|
||||
EmailAddress: emailAddress,
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user