diff --git a/tka/deeplink.go b/tka/deeplink.go index 7bd55d667..5cf24fc5c 100644 --- a/tka/deeplink.go +++ b/tka/deeplink.go @@ -18,6 +18,68 @@ 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 { diff --git a/tka/deeplink_test.go b/tka/deeplink_test.go new file mode 100644 index 000000000..03523202f --- /dev/null +++ b/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 := "insecure@example.com" + + 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) + } +}