「V4 签名实战」系列第 3 篇,语言轮到 C++。签名原理在第 1 篇(Python)已逐字节讲透,本篇聚焦 C++ 落地时的工程问题:header-only 签名器设计、二进制安全的 HMAC 中间值、自实现 URI 编码、CMake + OpenSSL 构建,以及如何把官方文档示例做成 CTest 自测。签名算法本身与第 1 篇完全同源,两份实现共用同一组官方测试向量。
配套完整工程(可直接导入编辑器构建运行):
https://github.com/zhoubiao188/aliyun-v4-signature-examples/tree/main/cpp-oss-putobject-v4
一、C++ 实现签名的四个工程难点
手写 OSS V4 签名在 Python 里是 130 行的事,在 C++ 里真正的难点不在算法,而在工程细节:
- HMAC 中间值是二进制:派生密钥链
kDate → kRegion → kService → kSigning每一步的输出都是 32 字节二进制摘要,要作为下一步的 key。C++ 里必须用std::string(二进制安全)传递,一旦经过c_str()、strlen或 char* 拼接,遇到0x00字节就会截断——这是 C++ 版签名最隐蔽的 bug 来源; - 规范化头的排序:签名要求头名小写 + 字典序。C++ 里直接用
std::map<std::string, std::string>,天然按 key 字典序遍历,比手工排序可靠; - URI 编码不能偷懒:libcurl 的
curl_easy_escape会把空格编成+之外还有细节差异,OSS V4 要求"仅A-Za-z0-9-._~不编码、空格为%20、路径保留/",自己写 20 行才是正解; - 时间必须 UTC:
x-oss-date形如20261009T145101Z,要用gmtime_r + strftime,误用localtime在东八区会把签名时间提前 8 小时,直接RequestTimeTooSkewed。
二、工程结构与构建
cpp-oss-putobject-v4/
├── CMakeLists.txt
├── include/oss_v4_signer.hpp # header-only 签名器(可直接复用到你的项目)
├── src/main.cpp # libcurl 发起 PutObject
└── test/test_signature.cpp # 官方示例向量自测(接入 CTest)
构建与运行(依赖只有一个 OpenSSL Crypto 和 libcurl):
# macOS: brew install cmake openssl
# Ubuntu: sudo apt install cmake g++ libssl-dev libcurl4-openssl-dev
cmake -B build -DCMAKE_BUILD_TYPE=Release
# macOS 找不到 OpenSSL 时追加: -DOPENSSL_ROOT_DIR=$(brew --prefix openssl)
cmake --build build
ctest --test-dir build --output-on-failure # ① 官方向量自测(无需网络和真实 AK)
./build/oss_putobject_v4 --dry-run # ② 打印签名全过程
export ALIBABA_CLOUD_ACCESS_KEY_ID=<AK>
export ALIBABA_CLOUD_ACCESS_KEY_SECRET=<SK>
export OSS_BUCKET=<你的Bucket>
./build/oss_putobject_v4 # ③ 真实上传,预期 HTTP 200 + ETag
CMakeLists 只有三块值得看:find_package(OpenSSL REQUIRED)、find_package(CURL REQUIRED),以及把自测挂进 CTest:
add_executable(v4_selftest test/test_signature.cpp)
target_include_directories(v4_selftest PRIVATE include)
target_link_libraries(v4_selftest PRIVATE OpenSSL::Crypto) # 自测不需要 curl
enable_testing()
add_test(NAME v4_selftest COMMAND v4_selftest)
三、核心代码逐段讲解
HMAC 与 SHA256(OpenSSL 的 HMAC 接口返回二进制,用 std::string 原样接住):
inline std::string hmac_sha256(const std::string& key, const std::string& msg) {
unsigned char md[EVP_MAX_MD_SIZE];
unsigned int md_len = 0;
HMAC(EVP_sha256(), key.data(), static_cast<int>(key.size()),
reinterpret_cast<const unsigned char*>(msg.data()), msg.size(), md, &md_len);
return std::string(reinterpret_cast<char*>(md), md_len);
}
派生密钥链——四步 HMAC,上一步输出直接做下一步 key,全程二进制安全:
std::string k_date = hmac_sha256("aliyun_v4" + sk, date); // 日期取 x-oss-date 前 8 位
std::string k_region = hmac_sha256(k_date, region);
std::string k_service = hmac_sha256(k_region, "oss");
std::string k_signing = hmac_sha256(k_service, "aliyun_v4_request");
std::string signature = to_hex(hmac_sha256(k_signing, string_to_sign));
规范化请求——std::map 自动字典序,每行头以 \n 结尾,五段拼接:
std::string canonical_headers;
for (const auto& kv : headers) // std::map 天然按 key 排序
canonical_headers += kv.first + ":" + trim(kv.second) + "\n";
std::string canonical_request = method + "\n" + canonical_uri + "\n" + canonical_query +
"\n" + canonical_headers + "\n" + additional_str + "\n" +
"UNSIGNED-PAYLOAD"; // 当前 V4 仅支持此固定值
待签串与 Authorization:
std::string scope = date + "/" + region + "/oss/aliyun_v4_request";
std::string string_to_sign = "OSS4-HMAC-SHA256\n" + x_oss_date + "\n" + scope + "\n" +
sha256_hex(canonical_request);
std::string authorization = "OSS4-HMAC-SHA256 Credential=" + ak + "/" + scope +
",Signature=" + signature;
发起请求(libcurl,把签名相关头逐个挂上):
curl_slist* hdrs = nullptr;
hdrs = curl_slist_append(hdrs, ("Authorization: " + r.authorization).c_str());
hdrs = curl_slist_append(hdrs, "x-oss-content-sha256: UNSIGNED-PAYLOAD");
hdrs = curl_slist_append(hdrs, ("x-oss-date: " + std::string(x_oss_date)).c_str());
hdrs = curl_slist_append(hdrs, ("content-type: " + content_type).c_str());
curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body.data());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
四、官方向量自测:跨语言交叉验证
test/test_signature.cpp 使用与第 1 篇 Python 完全相同的官方文档测试向量,构建后 ctest 输出:
[PASS] CanonicalRequest 的 SHA256(官方示例值)
c46d96390bdbc2d739ac9363293ae9d710b14e48081fcb22cd8ad54b63136eca
[PASS] StringToSign(用文档 SigningKey HMAC = 官方签名)
053edbf550ebd239b32a9cdfd93b0b2b3f2d223083aa61f75e9ac16856d61f23
[PASS] 本工程 SigningKey(官方 SDK 公式,对照项)
8a01ff4efcc65ca2cbc75375045c61ab5f3fa8b9a2d84f0add27ef16a25feb3c
第三项的 SigningKey 与 Python 篇实现算出的值逐字节相同——两门语言、两份独立实现、同一个官方公式,这就是"测试向量背书"的价值:它证明的不是代码跑通了,而是每一处换行、排序、编码都与官方规范一致。CMake 已把自测挂进 CTest,cmake --build 后顺手 ctest 即可回归。
五、踩坑清单
- HMAC 中间值过
c_str()/strlen:二进制摘要含0x00,C 字符串函数会截断,密钥链输出全错;全程std::string; localtime而非gmtime_r:UTC 时间错 8 小时,RequestTimeTooSkewed;- 用
curl_easy_escape做 URI 编码:它的转义集与 OSS V4 规范不一致(空格、波浪号等差异),自行实现 20 行才可靠; std::map被换成std::unordered_map:迭代顺序不再是字典序,规范化头排序直接破坏;- macOS 找不到 OpenSSL 头文件:Apple 自带 LibreSSL 且不在默认路径,
brew install openssl后加-DOPENSSL_ROOT_DIR=$(brew --prefix openssl); - 自测没接入构建系统:签名代码改一行没回归就上线是最危险的,CTest 里挂上官方向量,改坏当场爆;
EVP_MAX_MD_SIZE忘了用:摘要缓冲区按固定 32 字节硬编码虽然对 HMAC-SHA256 恰好不错,但读md_len才是正途。
六、小结与下一篇预告
本篇把 OSS V4 签名在 C++ 里完整落地:header-only 签名器 + libcurl 上传 + CTest 官方向量回归,签名器只有两个头文件依赖,可直接拷进任何 C++ 项目。三种语言写完同一套签名后你会发现:算法永远不变,变的只是字符串与字节的处理习惯——而签名 bug 几乎全部藏在字符串处理里。
下一篇(第 4 篇)回到 Python:手写 ACS3-HMAC-SHA256 签名调用 CDN DescribeCdnDomainLogs,对比 OSS 专用签名与全系 OpenAPI 通用签名的结构差异。欢迎收藏仓库:https://github.com/zhoubiao188/aliyun-v4-signature-examples
声明:本文为作者个人实践笔记,签名细节以阿里云官方签名文档为准;文中无任何真实密钥。