// Package signutil 提供开放接口签名共用的底层原语。 // // 这里只收敛三套签名实现中语义完全一致的部分:HMAC-SHA256→hex、body 摘要、 // 时间戳偏差校验、常数时间比较。 // // 各接口的「签名串构造方式」刻意不在这里统一——它们是对外契约,彼此不同: // - 商户侧 /api/client/v1:字典序 & 拼接,body 以 SHA256 摘要参与 // - 源头侧 /api/open/v1 :字典序 & 拼接,body 原文参与 // - 乐玩开放平台 :换行分隔的 6 行签名串(method/path/query/bodyHash/ts/nonce) // // 把它们合并成一套「通用」实现会直接改变握手结果,属于破坏性变更。 package signutil import ( "crypto/hmac" "crypto/sha256" "crypto/subtle" "encoding/hex" "strconv" "strings" "time" ) // EmptyBodySHA256 是空字节串的十六进制 SHA-256 摘要。 const EmptyBodySHA256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // HMACSHA256Hex 用 secret 对 content 计算 HMAC-SHA256,返回小写十六进制。 func HMACSHA256Hex(secret, content string) string { return HMACSHA256HexBytes(secret, []byte(content)) } // HMACSHA256HexBytes 同上,但内容由多段字节依次写入。 // webhook 签名需要「前缀 + 原始 body」这种分段写入,用它可以避免先拼接再拷贝。 func HMACSHA256HexBytes(secret string, parts ...[]byte) string { mac := hmac.New(sha256.New, []byte(secret)) for _, part := range parts { _, _ = mac.Write(part) } return hex.EncodeToString(mac.Sum(nil)) } // SHA256Hex 返回字节的十六进制 SHA-256 摘要;空内容返回空串摘要常量。 func SHA256Hex(body []byte) string { if len(body) == 0 { return EmptyBodySHA256 } sum := sha256.Sum256(body) return hex.EncodeToString(sum[:]) } // TimestampWithinSkew 判断 Unix 秒时间戳与当前时间的偏差是否不超过 skew。 // // 时间戳必须是纯十进制(不裁剪空白),与各调用点原有行为保持一致。 // skew <= 0 时只有时间戳与当前秒完全相同才通过——需要「0 表示不校验」语义的调用方 // 请自行判断后再调用,以免语义被悄悄改变。 func TimestampWithinSkew(timestamp string, skew time.Duration) bool { ts, err := strconv.ParseInt(timestamp, 10, 64) if err != nil { return false } diff := time.Now().Unix() - ts if diff < 0 { diff = -diff } return time.Duration(diff)*time.Second <= skew } // TimestampWithinSkewSeconds 是 TimestampWithinSkew 的秒参数版本。 func TimestampWithinSkewSeconds(timestamp string, skewSeconds int64) bool { return TimestampWithinSkew(timestamp, time.Duration(skewSeconds)*time.Second) } // EqualSignatures 以常数时间比较两个签名字符串,不做任何大小写归一化。 func EqualSignatures(actual, expected string) bool { return subtle.ConstantTimeCompare([]byte(actual), []byte(expected)) == 1 } // EqualHexSignatures 以常数时间比较两个十六进制签名,比较前统一转小写 // (十六进制本身大小写不敏感,调用方允许大写签名)。 func EqualHexSignatures(actual, expected string) bool { return EqualSignatures(strings.ToLower(actual), strings.ToLower(expected)) }