a73x

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
}