feat: refactor DeriveToken to accept multiple parts and update related usages

Kim committed Apr 17, 2026 at 11:47 UTC 4ddb79daf560b9c58c4d7a926c07f86dedffd756
4 files changed +26 -24
portal/auth/hop_route.go
+6 -15
@@ -3,7 +3,6 @@ package auth
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 - "encoding/json"
6 "errors"
7 "fmt"
8 "strings"
@@ -24,20 +23,12 @@ func SignHopRoute(method string, route types.HopRoute, identity types.Identity,
23 if err != nil {
24 return types.HopRoute{}, err
25 }
27 - ownerScope := struct {
28 - RelayURL string `json:"relay_url"`
29 - MatchHostname string `json:"match_hostname"`
30 - MatchToken string `json:"match_token"`
31 - }{
32 - RelayURL: route.RelayURL,
33 - MatchHostname: route.MatchHostname,
34 - MatchToken: route.MatchToken,
35 - }
36 - encodedOwnerScope, err := json.Marshal(ownerScope)
37 - if err != nil {
38 - return types.HopRoute{}, err
39 - }
40 - ownerToken, err := identity.DeriveToken("hop-owner:" + string(encodedOwnerScope) + ":0")
26 + ownerToken, err := identity.DeriveToken(
27 + "hop-route-owner",
28 + route.RelayURL,
29 + route.MatchHostname,
30 + route.MatchToken,
31 + )
32 if err != nil {
33 return types.HopRoute{}, err
34 }
sdk/api_client.go
+8 -1
@@ -8,6 +8,7 @@ import (
8 "net/http"
9 "net/url"
10 "slices"
11 + "strconv"
12 "strings"
13 "time"
14
@@ -127,7 +128,13 @@ func (l *listener) registerLease(ctx context.Context, ttl time.Duration, udpEnab
128
129 tokens := make([]string, len(hopPath)-1)
130 for i := range tokens {
130 - token, err := l.identity.DeriveToken(fmt.Sprintf("hop:%s:%d:%s:%s", publicHostname, i, hopPath[i].APIHTTPSAddr, hopPath[i+1].APIHTTPSAddr))
131 + token, err := l.identity.DeriveToken(
132 + "hop-token",
133 + publicHostname,
134 + strconv.Itoa(i),
135 + hopPath[i].APIHTTPSAddr,
136 + hopPath[i+1].APIHTTPSAddr,
137 + )
138 if err != nil {
139 return types.RegisterResponse{}, nil, err
140 }
types/identity.go
+11 -7
@@ -6,6 +6,7 @@ import (
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 + "strconv"
10 "strings"
11 "time"
12 )
@@ -61,21 +62,24 @@ func (i Identity) Key() string {
62 return name + IdentityKeySeparator + address
63 }
64
64 -func (i Identity) DeriveToken(nonce string) (string, error) {
65 +// DeriveToken derives a deterministic identity-scoped token from ordered
66 +// length-prefixed token parts. The first part should identify the token family.
67 +func (i Identity) DeriveToken(parts ...string) (string, error) {
68 privateKey := strings.TrimSpace(i.PrivateKey)
69 if privateKey == "" {
70 return "", errors.New("identity private key is required")
71 }
69 - nonce = strings.TrimSpace(nonce)
70 - if nonce == "" {
71 - return "", errors.New("identity token nonce is required")
72 - }
72
73 mac := hmac.New(sha256.New, []byte(privateKey))
74 _, _ = mac.Write([]byte("Portal identity token v1\n"))
75 _, _ = mac.Write([]byte(i.Key()))
77 - _, _ = mac.Write([]byte("\n"))
78 - _, _ = mac.Write([]byte(nonce))
76 + for _, part := range parts {
77 + part = strings.TrimSpace(part)
78 + _, _ = mac.Write([]byte("\n"))
79 + _, _ = mac.Write([]byte(strconv.Itoa(len(part))))
80 + _, _ = mac.Write([]byte(":"))
81 + _, _ = mac.Write([]byte(part))
82 + }
83 return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)), nil
84 }
85
utils/identity.go
+1 -1
@@ -464,7 +464,7 @@ func populateRelayIdentity(identity *types.RelayIdentity, discoveryEnabled bool)
464 }
465
466 if strings.TrimSpace(identity.AdminSecretKey) == "" {
467 - adminSecretKey, err := identity.Identity.DeriveToken("admin")
467 + adminSecretKey, err := identity.Identity.DeriveToken("admin-secret")
468 if err != nil {
469 return fmt.Errorf("derive relay admin secret key: %w", err)
470 }