// ABOUTME: HMAC-SHA256 token generation and validation for scoped push authentication. // ABOUTME: Tokens are derived from a key + scope + timestamp, enabling stateless validation with expiration. package receiver import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/hex" "fmt" "strconv" "strings" "time" ) // DefaultTokenTTL is the default time-to-live for push tokens. const DefaultTokenTTL = 2 * time.Hour // GenerateToken creates a token with embedded timestamp for expiration support. // Format: ":" func GenerateToken(key, org, repo, workflow, job string) string { return GenerateTokenAt(key, org, repo, workflow, job, time.Now()) } // GenerateTokenAt creates a token with the specified timestamp. // The HMAC input is "v1\x00\x00\x00\x00\x00". func GenerateTokenAt(key, org, repo, workflow, job string, timestamp time.Time) string { ts := strconv.FormatInt(timestamp.Unix(), 10) mac := hmac.New(sha256.New, []byte(key)) mac.Write([]byte("v1\x00" + org + "\x00" + repo + "\x00" + workflow + "\x00" + job + "\x00" + ts)) return ts + ":" + hex.EncodeToString(mac.Sum(nil)) } // ValidateToken validates a token and checks expiration. // Returns true if the token is valid and not expired. func ValidateToken(key, token, org, repo, workflow, job string, ttl time.Duration) bool { return ValidateTokenAt(key, token, org, repo, workflow, job, ttl, time.Now()) } // ValidateTokenAt validates a token against a specific reference time. func ValidateTokenAt(key, token, org, repo, workflow, job string, ttl time.Duration, now time.Time) bool { parts := strings.SplitN(token, ":", 2) if len(parts) != 2 { return false } tsStr, hmacHex := parts[0], parts[1] ts, err := strconv.ParseInt(tsStr, 10, 64) if err != nil { return false } tokenTime := time.Unix(ts, 0) if now.Sub(tokenTime) > ttl { return false } // Recompute expected HMAC mac := hmac.New(sha256.New, []byte(key)) mac.Write([]byte("v1\x00" + org + "\x00" + repo + "\x00" + workflow + "\x00" + job + "\x00" + tsStr)) expected := hex.EncodeToString(mac.Sum(nil)) return subtle.ConstantTimeCompare([]byte(hmacHex), []byte(expected)) == 1 } // ParseTokenTimestamp extracts the timestamp from a timestamped token without validating it. func ParseTokenTimestamp(token string) (time.Time, error) { parts := strings.SplitN(token, ":", 2) if len(parts) != 2 { return time.Time{}, fmt.Errorf("invalid token format") } ts, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { return time.Time{}, fmt.Errorf("invalid timestamp: %w", err) } return time.Unix(ts, 0), nil }