139 lines
4.8 KiB
Go
139 lines
4.8 KiB
Go
package ssl
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type dnsSignatureTransport struct {
|
|
t *testing.T
|
|
nonces map[string]bool
|
|
}
|
|
|
|
// 独立按字节编码,避免验证器与生产编码函数共享同一种错误。
|
|
func referenceDNSEncode(value string) string {
|
|
var result strings.Builder
|
|
for _, value := range []byte(value) {
|
|
if value >= 'a' && value <= 'z' || value >= 'A' && value <= 'Z' || value >= '0' && value <= '9' || strings.ContainsRune("-_.~", rune(value)) {
|
|
result.WriteByte(value)
|
|
} else {
|
|
fmt.Fprintf(&result, "%%%02X", value)
|
|
}
|
|
}
|
|
return result.String()
|
|
}
|
|
|
|
func referenceDNSSignature(values url.Values) string {
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
if key != "Signature" {
|
|
keys = append(keys, key)
|
|
}
|
|
}
|
|
sort.Strings(keys)
|
|
var parts []string
|
|
for _, key := range keys {
|
|
for _, value := range values[key] {
|
|
parts = append(parts, referenceDNSEncode(key)+"="+referenceDNSEncode(value))
|
|
}
|
|
}
|
|
mac := hmac.New(sha1.New, []byte("test-secret&"))
|
|
_, _ = mac.Write([]byte("POST&%2F&" + referenceDNSEncode(strings.Join(parts, "&"))))
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func (transport *dnsSignatureTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
t := transport.t
|
|
if request.Method != http.MethodPost || request.URL.Scheme != "https" || request.URL.Host != "alidns.aliyuncs.com" || request.URL.RawQuery != "" {
|
|
t.Error("DNS 签名请求没有使用预期的地址和方法")
|
|
}
|
|
defer request.Body.Close()
|
|
body, err := io.ReadAll(request.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
values, err := url.ParseQuery(string(body))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if values.Get("Signature") != referenceDNSSignature(values) {
|
|
t.Error("DNS 请求签名不匹配,可能包含上一次的签名")
|
|
}
|
|
for key, expected := range map[string]string{
|
|
"Action": "AddDomainRecord", "Version": "2015-01-09", "Format": "JSON",
|
|
"AccessKeyId": "test-access", "SignatureMethod": "HMAC-SHA1", "SignatureVersion": "1.0",
|
|
"Value": "中文 a b+*/~=&%", "DomainName": "example.com",
|
|
} {
|
|
if values.Get(key) != expected {
|
|
t.Errorf("DNS 请求字段不符合预期:%s", key)
|
|
}
|
|
}
|
|
nonce := values.Get("SignatureNonce")
|
|
decoded, err := hex.DecodeString(nonce)
|
|
if err != nil || len(decoded) != 16 || transport.nonces[nonce] {
|
|
t.Error("DNS 请求随机数无效或被重复使用")
|
|
}
|
|
transport.nonces[nonce] = true
|
|
timestamp, err := time.Parse("2006-01-02T15:04:05Z", values.Get("Timestamp"))
|
|
if err != nil || time.Since(timestamp) > time.Minute || time.Until(timestamp) > time.Minute {
|
|
t.Error("DNS 请求时间戳无效")
|
|
}
|
|
if strings.Contains(string(body), "test-secret") || request.Header.Get("Content-Type") != "application/x-www-form-urlencoded" {
|
|
t.Error("DNS 请求泄露测试密钥或使用了错误的内容类型")
|
|
}
|
|
return &http.Response{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"RecordId":"created"}`)), Header: make(http.Header)}, nil
|
|
}
|
|
|
|
func TestAutomaticDNSSigningPreservesReusableParameters(t *testing.T) {
|
|
for _, name := range []string{"fresh", "stale"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
transport := &dnsSignatureTransport{t: t, nonces: make(map[string]bool)}
|
|
provider := &aliDNSChallenge{
|
|
accessKey: "test-access", secretKey: "test-secret", endpoint: "https://alidns.aliyuncs.com/",
|
|
client: &http.Client{Transport: transport},
|
|
}
|
|
values := url.Values{"DomainName": {"example.com"}, "Value": {"中文 a b+*/~=&%"}}
|
|
expected := url.Values{"DomainName": {"example.com"}, "Value": {"中文 a b+*/~=&%"}}
|
|
if name == "stale" {
|
|
values.Set("Signature", "stale-signature")
|
|
expected.Set("Signature", "stale-signature")
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
var result struct {
|
|
RecordID string `json:"RecordId"`
|
|
}
|
|
if err := provider.call("AddDomainRecord", values, &result); err != nil || result.RecordID != "created" {
|
|
t.Fatal("DNS 签名请求未正确返回")
|
|
}
|
|
if !reflect.DeepEqual(values, expected) {
|
|
t.Error("DNS 请求改写了调用者参数,重试会带入之前的签名")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAutomaticDNSSignatureReferenceVector(t *testing.T) {
|
|
// 固定向量由独立 Python 标准库计算;只是测试样本,不是真实云凭据。
|
|
values := url.Values{
|
|
"Action": {"AddDomainRecord"}, "Version": {"2015-01-09"}, "Format": {"JSON"},
|
|
"AccessKeyId": {"test-access"}, "SignatureMethod": {"HMAC-SHA1"}, "SignatureVersion": {"1.0"},
|
|
"SignatureNonce": {"00000000000000000000000000000000"}, "Timestamp": {"2026-09-21T00:00:00Z"},
|
|
"DomainName": {"example.com"}, "RR": {"_acme-challenge"}, "Type": {"TXT"}, "Value": {"a b+*/~=&%"},
|
|
}
|
|
if referenceDNSSignature(values) != "s+e5Vs2kLbIFh5nFBykkdS2IrFU=" {
|
|
t.Fatal("独立签名验证器不符合固定向量")
|
|
}
|
|
}
|