77 lines
2.6 KiB
Go
77 lines
2.6 KiB
Go
// 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: "<unix_timestamp>:<hmac_hex>"
|
|
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<org>\x00<repo>\x00<workflow>\x00<job>\x00<timestamp>".
|
|
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
|
|
}
|