Explorar o código

tka: add function for generating signing deeplinks (#8385)

This commit continues the work from #8303, providing a method for a
tka.Authority to generate valid deeplinks for signing devices. We'll
use this to provide the necessary deeplinks for users to sign from
their mobile devices.

Updates #8302

Signed-off-by: Ross Zurowski <[email protected]>
Ross Zurowski %!s(int64=2) %!d(string=hai) anos
pai
achega
0ed088b47b
Modificáronse 2 ficheiros con 121 adicións e 15 borrados
  1. 69 15
      tka/deeplink.go
  2. 52 0
      tka/deeplink_test.go

+ 69 - 15
tka/deeplink.go

@@ -18,6 +18,68 @@ const (
 	DeeplinkCommandSign        = "sign-device"
 )
 
+// generateHMAC computes a SHA-256 HMAC for the concatenation of components,
+// using the Authority stateID as secret.
+func (a *Authority) generateHMAC(params NewDeeplinkParams) []byte {
+	stateID, _ := a.StateIDs()
+
+	key := make([]byte, 8)
+	binary.LittleEndian.PutUint64(key, stateID)
+	mac := hmac.New(sha256.New, key)
+	mac.Write([]byte(params.NodeKey))
+	mac.Write([]byte(params.TLPub))
+	mac.Write([]byte(params.DeviceName))
+	mac.Write([]byte(params.OSName))
+	mac.Write([]byte(params.LoginName))
+	return mac.Sum(nil)
+}
+
+type NewDeeplinkParams struct {
+	NodeKey    string
+	TLPub      string
+	DeviceName string
+	OSName     string
+	LoginName  string
+}
+
+// NewDeeplink creates a signed deeplink using the authority's stateID as a
+// secret. This deeplink can then be validated by ValidateDeeplink.
+func (a *Authority) NewDeeplink(params NewDeeplinkParams) (string, error) {
+	if params.NodeKey == "" || !strings.HasPrefix(params.NodeKey, "nodekey:") {
+		return "", fmt.Errorf("invalid node key %q", params.NodeKey)
+	}
+	if params.TLPub == "" || !strings.HasPrefix(params.TLPub, "tlpub:") {
+		return "", fmt.Errorf("invalid tlpub %q", params.TLPub)
+	}
+	if params.DeviceName == "" {
+		return "", fmt.Errorf("invalid device name %q", params.DeviceName)
+	}
+	if params.OSName == "" {
+		return "", fmt.Errorf("invalid os name %q", params.OSName)
+	}
+	if params.LoginName == "" {
+		return "", fmt.Errorf("invalid login name %q", params.LoginName)
+	}
+
+	u := url.URL{
+		Scheme: DeeplinkTailscaleURLScheme,
+		Host:   DeeplinkCommandSign,
+		Path:   "/v1/",
+	}
+	v := url.Values{}
+	v.Set("nk", params.NodeKey)
+	v.Set("tp", params.TLPub)
+	v.Set("dn", params.DeviceName)
+	v.Set("os", params.OSName)
+	v.Set("em", params.LoginName)
+
+	hmac := a.generateHMAC(params)
+	v.Set("hm", hex.EncodeToString(hmac))
+
+	u.RawQuery = v.Encode()
+	return u.String(), nil
+}
+
 type DeeplinkValidationResult struct {
 	IsValid      bool
 	Error        string
@@ -29,18 +91,6 @@ type DeeplinkValidationResult struct {
 	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:
 //
@@ -140,9 +190,13 @@ func (a *Authority) ValidateDeeplink(urlString string) DeeplinkValidationResult
 		}
 	}
 
-	components := []string{nodeKey, tlPub, deviceName, osName, emailAddress}
-	stateID1, _ := a.StateIDs()
-	computedHMAC := generateHMAC(stateID1, components)
+	computedHMAC := a.generateHMAC(NewDeeplinkParams{
+		NodeKey:    nodeKey,
+		TLPub:      tlPub,
+		DeviceName: deviceName,
+		OSName:     osName,
+		LoginName:  emailAddress,
+	})
 
 	hmacHexBytes, err := hex.DecodeString(hmacString)
 	if err != nil {

+ 52 - 0
tka/deeplink_test.go

@@ -0,0 +1,52 @@
+// Copyright (c) Tailscale Inc & AUTHORS
+// SPDX-License-Identifier: BSD-3-Clause
+
+package tka
+
+import (
+	"testing"
+)
+
+func TestGenerateDeeplink(t *testing.T) {
+	pub, _ := testingKey25519(t, 1)
+	key := Key{Kind: Key25519, Public: pub, Votes: 2}
+	c := newTestchain(t, `
+        G1 -> L1
+
+        G1.template = genesis
+    `,
+		optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
+			Keys:               []Key{key},
+			DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
+		}}),
+	)
+	a, _ := Open(c.Chonk())
+
+	nodeKey := "nodekey:1234567890"
+	tlPub := "tlpub:1234567890"
+	deviceName := "Example Device"
+	osName := "iOS"
+	loginName := "[email protected]"
+
+	deeplink, err := a.NewDeeplink(NewDeeplinkParams{
+		NodeKey:    nodeKey,
+		TLPub:      tlPub,
+		DeviceName: deviceName,
+		OSName:     osName,
+		LoginName:  loginName,
+	})
+	if err != nil {
+		t.Errorf("deeplink generation failed: %v", err)
+	}
+
+	res := a.ValidateDeeplink(deeplink)
+	if !res.IsValid {
+		t.Errorf("deeplink validation failed: %s", res.Error)
+	}
+	if res.NodeKey != nodeKey {
+		t.Errorf("node key mismatch: %s != %s", res.NodeKey, nodeKey)
+	}
+	if res.TLPub != tlPub {
+		t.Errorf("tlpub mismatch: %s != %s", res.TLPub, tlPub)
+	}
+}