mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-25 19:15:34 +00:00
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:
parent
909e9eabe4
commit
0ed088b47b
@ -18,6 +18,68 @@
|
|||||||
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
52
tka/deeplink_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user