internal/server/hosttoken/hosttoken.go
Ref: Size: 2.1 KiB History
// Package hosttoken mints and verifies generation-versioned host credentials.
//
// Format:
//
// "<host_id>.<generation>.<issued_unix>.<hex hmac-sha256>"
//
// The HMAC covers the three dotted fields, binding identity, credential
// generation, and issue time. Generation enables PER-HOST revocation: the
// server compares the credential's generation against the host row's
// cred_generation and rejects stale ones — bumping the row revokes that one
// host without rotating the fleet-wide secret. issued_unix enables an
// optional max-age policy (enforced by the caller; this package only signs
// and parses).
//
// Host IDs are hex strings and must not contain '.'; a dotted input fails
// verification safely because the signature is computed over the exact
// parsed fields and will never match.
package hosttoken
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strconv"
"strings"
"time"
)
// Claims is the verified content of a credential.
type Claims struct {
HostID string
Generation int64
IssuedAt time.Time
}
func sign(secret []byte, payload string) string {
m := hmac.New(sha256.New, secret)
m.Write([]byte(payload))
return hex.EncodeToString(m.Sum(nil))
}
// Mint returns a signed credential for hostID at the given generation.
func Mint(secret []byte, hostID string, generation int64, issuedAt time.Time) string {
payload := fmt.Sprintf("%s.%d.%d", hostID, generation, issuedAt.Unix())
return payload + "." + sign(secret, payload)
}
// Verify parses and authenticates cred: the four signed fields
// host_id.generation.issued_unix.hmac. Anything else fails.
func Verify(secret []byte, cred string) (Claims, bool) {
parts := strings.Split(cred, ".")
if len(parts) != 4 || parts[0] == "" {
return Claims{}, false
}
gen, err := strconv.ParseInt(parts[1], 10, 64)
if err != nil {
return Claims{}, false
}
issued, err := strconv.ParseInt(parts[2], 10, 64)
if err != nil {
return Claims{}, false
}
payload := strings.Join(parts[:3], ".")
if !hmac.Equal([]byte(parts[3]), []byte(sign(secret, payload))) {
return Claims{}, false
}
return Claims{HostID: parts[0], Generation: gen, IssuedAt: time.Unix(issued, 0)}, true
}