签名示例
仓库提供 JavaScript、Python、Go、Java 和 Shell 五种可运行示例。它们共享同一个固定测试向量,并在单元测试中校验输出必须完全一致。
多语言实现
下面的代码直接从仓库中的可运行示例导入,页面展示与测试执行使用同一份源码,避免文档示例与实际实现不一致。
js
import { createHash, createHmac } from "node:crypto";
const [accessKey, secretKey, timestamp, nonce, audience, method, path, rawQuery, contentType, body, idempotencyKey] = process.argv.slice(2);
if ([accessKey, secretKey, timestamp, nonce, audience, method, path, rawQuery, contentType, body, idempotencyKey].some((value) => value === undefined)) {
throw new Error("expected 11 signing arguments");
}
const bodyHash = createHash("sha256").update(body, "utf8").digest("hex");
const canonical = [
"UVP-HMAC-SHA256/1",
accessKey,
timestamp,
nonce,
method.toUpperCase(),
path,
rawQuery,
contentType,
bodyHash,
idempotencyKey,
audience
].join("\n");
const key = Buffer.from(secretKey, "base64url");
if (key.length !== 32) {
throw new Error("secret key must decode to 32 bytes");
}
process.stdout.write(`${createHmac("sha256", key).update(canonical, "utf8").digest("hex")}\n`);python
#!/usr/bin/env python3
import base64
import hashlib
import hmac
import sys
if len(sys.argv) != 12:
raise SystemExit("expected 11 signing arguments")
access_key, secret_key, timestamp, nonce, audience, method, path, raw_query, content_type, body, idempotency_key = sys.argv[1:]
key = base64.urlsafe_b64decode(secret_key + "=" * (-len(secret_key) % 4))
if len(key) != 32:
raise SystemExit("secret key must decode to 32 bytes")
body_hash = hashlib.sha256(body.encode("utf-8")).hexdigest()
canonical = "\n".join([
"UVP-HMAC-SHA256/1",
access_key,
timestamp,
nonce,
method.upper(),
path,
raw_query,
content_type,
body_hash,
idempotency_key,
audience,
])
print(hmac.new(key, canonical.encode("utf-8"), hashlib.sha256).hexdigest())go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"os"
"strings"
)
func main() {
if len(os.Args) != 12 {
fmt.Fprintln(os.Stderr, "expected 11 signing arguments")
os.Exit(2)
}
accessKey, secretKey := os.Args[1], os.Args[2]
timestamp, nonce, audience := os.Args[3], os.Args[4], os.Args[5]
method, path, rawQuery := os.Args[6], os.Args[7], os.Args[8]
contentType, body, idempotencyKey := os.Args[9], os.Args[10], os.Args[11]
key, err := base64.RawURLEncoding.DecodeString(secretKey)
if err != nil || len(key) != 32 {
fmt.Fprintln(os.Stderr, "secret key must be unpadded Base64URL for 32 bytes")
os.Exit(2)
}
bodyDigest := sha256.Sum256([]byte(body))
canonical := strings.Join([]string{
"UVP-HMAC-SHA256/1",
accessKey,
timestamp,
nonce,
strings.ToUpper(method),
path,
rawQuery,
contentType,
hex.EncodeToString(bodyDigest[:]),
idempotencyKey,
audience,
}, "\n")
mac := hmac.New(sha256.New, key)
_, _ = mac.Write([]byte(canonical))
fmt.Println(hex.EncodeToString(mac.Sum(nil)))
}java
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class UVPSignExample {
private UVPSignExample() {}
public static void main(String[] args) throws Exception {
if (args.length != 11) {
throw new IllegalArgumentException("expected 11 signing arguments");
}
byte[] key = Base64.getUrlDecoder().decode(padBase64(args[1]));
if (key.length != 32) {
throw new IllegalArgumentException("secret key must decode to 32 bytes");
}
String bodyHash = hex(MessageDigest.getInstance("SHA-256")
.digest(args[9].getBytes(StandardCharsets.UTF_8)));
String canonical = String.join("\n",
"UVP-HMAC-SHA256/1",
args[0],
args[2],
args[3],
args[5].toUpperCase(),
args[6],
args[7],
args[8],
bodyHash,
args[10],
args[4]);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key, "HmacSHA256"));
System.out.println(hex(mac.doFinal(canonical.getBytes(StandardCharsets.UTF_8))));
}
private static String padBase64(String value) {
StringBuilder padded = new StringBuilder(value);
while (padded.length() % 4 != 0) {
padded.append('=');
}
return padded.toString();
}
private static String hex(byte[] value) {
StringBuilder result = new StringBuilder(value.length * 2);
for (byte item : value) {
result.append(String.format("%02x", item & 0xff));
}
return result.toString();
}
}bash
#!/usr/bin/env bash
set -euo pipefail
if [[ "$#" -ne 11 ]]; then
echo "expected 11 signing arguments" >&2
exit 2
fi
access_key=$1
secret_key=$2
timestamp=$3
nonce=$4
audience=$5
method=$6
request_path=$7
raw_query=$8
content_type=$9
body=${10}
idempotency_key=${11}
padding=$(( (4 - ${#secret_key} % 4) % 4 ))
padded_secret=$secret_key
while (( padding > 0 )); do
padded_secret+="="
padding=$((padding - 1))
done
key_hex=$(printf '%s' "$padded_secret" | tr '_-' '/+' | openssl base64 -d -A | od -An -tx1 | tr -d ' \n')
if [[ "${#key_hex}" -ne 64 ]]; then
echo "secret key must decode to 32 bytes" >&2
exit 2
fi
body_hash=$(printf '%s' "$body" | openssl dgst -sha256 | awk '{print $NF}')
method=$(printf '%s' "$method" | tr '[:lower:]' '[:upper:]')
canonical=$(printf '%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s\n%s' \
'UVP-HMAC-SHA256/1' "$access_key" "$timestamp" "$nonce" "$method" \
"$request_path" "$raw_query" "$content_type" "$body_hash" "$idempotency_key" "$audience")
printf '%s' "$canonical" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:$key_hex" | awk '{print $NF}'运行与验证
固定测试向量位于 examples/signature-v1.json。在项目根目录执行:
bash
pnpm test:unit -- tests/signing-examples.test.ts测试会运行上述五种实现,并校验它们生成完全相同的 64 位小写十六进制签名。
实现步骤
- 对请求体的原始 UTF-8 字节计算 SHA-256。零字节 Body 也必须计算空内容摘要。
- 按固定顺序拼接 11 行 Canonical Request,使用 LF 分隔,末尾不追加换行。
- 将无填充 Base64URL 格式的 SK 解码为 32 字节密钥。
- 对 Canonical Request 的 UTF-8 字节执行 HMAC-SHA256,输出 64 位小写十六进制字符串。
签名前必须先按认证与签名和请求约定完成查询串规范化。不要对已经发送的 URL 或 JSON 做二次重建,否则字节差异会导致验签失败。
凭证安全
固定向量只用于测试,不能作为真实凭证。生产 SK 仅应存在于服务端进程的受控内存或密钥管理系统中,不要写入前端代码、浏览器存储、日志、截图或版本库。