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 <ross@rosszurowski.com>
This commit is contained in:
Ross Zurowski 2023-06-20 09:36:37 -07:00 committed by GitHub
parent 909e9eabe4
commit 0ed088b47b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 121 additions and 15 deletions

View File

@ -18,6 +18,68 @@ const (
DeeplinkCommandSign = "sign-device" 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 { type DeeplinkValidationResult struct {
IsValid bool IsValid bool
Error string Error string
@ -29,18 +91,6 @@ type DeeplinkValidationResult struct {
EmailAddress 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. // ValidateDeeplink validates a device signing deeplink using the authority's stateID.
// The input urlString follows this structure: // The input urlString follows this structure:
// //
@ -140,9 +190,13 @@ func (a *Authority) ValidateDeeplink(urlString string) DeeplinkValidationResult
} }
} }
components := []string{nodeKey, tlPub, deviceName, osName, emailAddress} computedHMAC := a.generateHMAC(NewDeeplinkParams{
stateID1, _ := a.StateIDs() NodeKey: nodeKey,
computedHMAC := generateHMAC(stateID1, components) TLPub: tlPub,
DeviceName: deviceName,
OSName: osName,
LoginName: emailAddress,
})
hmacHexBytes, err := hex.DecodeString(hmacString) hmacHexBytes, err := hex.DecodeString(hmacString)
if err != nil { if err != nil {

52
tka/deeplink_test.go Normal file
View File

@ -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)
}
}