Skip to content

签名示例 ​

仓库提供 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 位小写十六进制签名。

实现步骤 ​

  1. 对请求体的原始 UTF-8 字节计算 SHA-256。零字节 Body 也必须计算空内容摘要。
  2. 按固定顺序拼接 11 行 Canonical Request,使用 LF 分隔,末尾不追加换行。
  3. 将无填充 Base64URL 格式的 SK 解码为 32 字节密钥。
  4. 对 Canonical Request 的 UTF-8 字节执行 HMAC-SHA256,输出 64 位小写十六进制字符串。

签名前必须先按认证与签名和请求约定完成查询串规范化。不要对已经发送的 URL 或 JSON 做二次重建,否则字节差异会导致验签失败。

凭证安全

固定向量只用于测试,不能作为真实凭证。生产 SK 仅应存在于服务端进程的受控内存或密钥管理系统中,不要写入前端代码、浏览器存储、日志、截图或版本库。