签名证明什么
请求签名把共享密钥或私钥绑定到请求的确切字节。接收方可以确认调用方持有密钥,且方法、路径、请求头和负载未被篡改。它提供完整性与身份认证,不是加密:除非 TLS 保护,内容仍然可见。
工作机制
RFC 9421 定义了由选定组件构成的规范化形式。发送方在 Signature-Input 中列出组件,例如 sig1=("@method" "@path" "content-digest");created=1788307200;keyid="k1",再发送 Signature: sig1=:base64value: 和正文的 Content-Digest。接收方重建相同字符串,用登记的密钥验签,检查创建时间是否在短窗口内,并可选记录 nonce 防重放。
常见替代方案是对排序后的参数串做 HMAC-SHA256,或使用非对称签名,让验证方不持有签名密钥。当多个接收方都要验证且都不应能伪造时,非对称更合适。
何时使用与取舍
跨信任边界的服务间调用、Webhook 投递和支付指令都应签名。请求经过会终止 TLS 的代理或网关时,签名仍然有效。浏览器客户端无法保存密钥,应改用短期令牌。
规范化是最脆弱的部分:头顺序、空白、重复头、URL 编码必须完全一致。签名大正文需要缓冲或流式摘要。时钟会漂移,窗口要容忍偏差又不能允许重放。密钥分发与轮换也增加运维工作。
示例与常见误解
某 Webhook 提供方对 POST /hooks 的请求体摘要和时间戳签名。接收方只校验正文,攻击者篡改 X-Tenant 头后重放仍会通过;校验完整组件列表和时间窗口才能拒绝。
常见误解是签名可以替代 TLS。两者互补:TLS 保护通道,签名保护请求免受中间人和重放影响。相关概念:Webhooks、幂等性、JSON Web Token。
代码示例
package main
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"strings"
)
type Middleware func(http.Handler) http.Handler
type Payload struct {
Name string `json:"name"`
}
func LimitBody(maxBytes int64) Middleware {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
next.ServeHTTP(w, r)
})
}
}
func decodePayload(w http.ResponseWriter, r *http.Request) (Payload, error) {
var payload Payload
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(&payload); err != nil {
var tooLarge *http.MaxBytesError
if errors.As(err, &tooLarge) {
http.Error(w, fmt.Sprintf("body exceeds %d bytes", tooLarge.Limit), http.StatusRequestEntityTooLarge)
return payload, err
}
http.Error(w, "invalid JSON body", http.StatusBadRequest)
return payload, err
}
if strings.TrimSpace(payload.Name) == "" {
http.Error(w, "name is required", http.StatusUnprocessableEntity)
return payload, errors.New("name is required")
}
return payload, nil
}
func main() {
handler := LimitBody(64)(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
payload, err := decodePayload(w, r)
if err != nil {
return
}
w.WriteHeader(http.StatusCreated)
fmt.Fprintf(w, "created %s\n", payload.Name)
}))
bodies := []string{
`{"name":"ada"}`,
`{"name":"` + strings.Repeat("x", 80) + `"}`,
}
for _, body := range bodies {
request := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader(body))
recorder := httptest.NewRecorder()
handler.ServeHTTP(recorder, request)
fmt.Printf("status=%d body=%q\n", recorder.Code, strings.TrimSpace(recorder.Body.String()))
}
}
import re
from pathlib import PurePosixPath
from urllib.parse import unquote
NAME_STAR = re.compile(r"filename\*\s*=\s*([^;]+)", re.IGNORECASE)
NAME = re.compile(r'filename\s*=\s*(?:"([^"]*)"|([^;]+))', re.IGNORECASE)
def safe_basename(value: str) -> str | None:
candidate = PurePosixPath(value.replace("\\", "/")).name.strip()
if candidate in {"", ".", ".."} or "\x00" in candidate:
return None
return candidate
def attachment_filename(header: str) -> str | None:
match = NAME_STAR.search(header)
if match:
value = match.group(1).strip().strip('"')
charset, _, remainder = value.partition("'")
_, _, encoded = remainder.partition("'")
try:
decoded = unquote(
encoded or remainder,
encoding=charset or "utf-8",
errors="strict",
)
except (LookupError, UnicodeDecodeError):
decoded = unquote(encoded or remainder)
return safe_basename(decoded)
match = NAME.search(header)
if not match:
return None
value = match.group(1) if match.group(1) is not None else match.group(2)
return safe_basename(unquote(value.strip()))
def main() -> None:
headers = [
'attachment; filename="report final.pdf"',
"attachment; filename=../../etc/passwd",
"attachment; filename*=UTF-8''%E5%B9%B4%E6%8A%A5.pdf",
'attachment; filename=".."',
]
for header in headers:
print(attachment_filename(header))
if __name__ == "__main__":
main()