package auth import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "encoding/json" "errors" "strings" "time" ) type Claims struct { Sub string `json:"sub"` // account ID (acc_…) Exp int64 `json:"exp"` // unix seconds Nbf int64 `json:"nbf,omitempty"` // not before Iss string `json:"iss,omitempty"` // greencoast Aud string `json:"aud,omitempty"` // api Jti string `json:"jti,omitempty"` // token id (optional) CNF string `json:"cnf,omitempty"` // key binding: "p256:" or "ed25519:" } func MintGC2(signKey []byte, c Claims) (string, error) { if len(signKey) == 0 { return "", errors.New("sign key missing") } if c.Sub == "" || c.Exp == 0 { return "", errors.New("claims incomplete") } body, _ := json.Marshal(c) mac := hmac.New(sha256.New, signKey) mac.Write(body) sig := mac.Sum(nil) return "gc2." + base64.RawURLEncoding.EncodeToString(body) + "." + base64.RawURLEncoding.EncodeToString(sig), nil } func VerifyGC2(signKey []byte, tok string, now time.Time) (Claims, error) { var zero Claims if !strings.HasPrefix(tok, "gc2.") { return zero, errors.New("bad prefix") } parts := strings.Split(tok, ".") if len(parts) != 3 { return zero, errors.New("bad parts") } body, err := base64.RawURLEncoding.DecodeString(parts[1]) if err != nil { return zero, err } want, err := base64.RawURLEncoding.DecodeString(parts[2]) if err != nil { return zero, err } mac := hmac.New(sha256.New, signKey) mac.Write(body) if !hmac.Equal(want, mac.Sum(nil)) { return zero, errors.New("bad sig") } var c Claims if err := json.Unmarshal(body, &c); err != nil { return zero, err } t := now.Unix() if c.Nbf != 0 && t < c.Nbf { return zero, errors.New("nbf") } if t > c.Exp { return zero, errors.New("expired") } return c, nil } func AccountIDFromPub(raw []byte) string { // acc_ sum := sha256.Sum256(raw) return "acc_" + hex.EncodeToString(sum[:16]) }