libwebsockets Secure Streams Sigv4 实战:用 s3PutObj 策略流将文件上传到 AWS S3
libwebsockets Secure Streams Sigv4 实战:用 s3PutObj 策略流将文件上传到 AWS S3
本文以 libwebsockets 官方示例 minimal-secure-streams-sigv4 为核心,讲解如何用 Secure Streams 的 s3PutObj streamtype 配合 AWS SigV4 签名,把测试文件通过 HTTPS PUT 上传到 S3。读完本文,你能理解该示例的 policy 结构与 metadata 机制、lws_ss_sigv4_set_aws_key() 凭据注入方式,以及 SigV4 签名在库内部(lib/secure-streams/system/auth-sigv4/sign.c)的完整计算链。
示例定位与文件构成
这个 minimal 示例的功能只有一句话:把本地一段测试数据作为对象上传到 AWS S3,认证方式为 SigV4,传输方式为 Secure Streams。它由 ss-s3-main.c(入口)、ss-s3-ss.c(streamtype 处理器)和 ss-s3-put.h(用户数据结构)组成,策略则内嵌在 policy.json 中(main 文件里同时保留了一份等价的 C 字符串形式 default_ss_policy)。
Secure Streams 是 lws 的“协议无关”API:应用代码只按 streamtype 声明“我想做哪件事”,具体的 HTTP 方法、URL、TLS 信任、重试、认证全部由 policy 描述并由库驱动。本示例中 streamtype 为 s3PutObj,处理器通过 lws_ss_info_t 的 .streamtype = "s3PutObj" 与之绑定(见 ss-s3-ss.c):
const lws_ss_info_t s3_ssi = {
.handle_offset = offsetof(ss_s3_put_t, ss),
.opaque_user_data_offset = offsetof(ss_s3_put_t, opaque_data),
.rx = ss_s3_rx,
.tx = ss_s3_tx,
.state = ss_s3_state,
.user_alloc = sizeof(ss_s3_put_t),
.streamtype = "s3PutObj"
};
用户数据 ss_s3_put_t 内嵌 struct lws_ss_handle *ss,用于回调中取回 handle;示例只上传一次,因此 tx 侧只实现了“按 pos 推进发送 1KB 测试 payload”的逻辑(ss-s3-put.h)。
构建前提与 CMake 开关
示例的 CMakeLists.txt 明确了启用它所需的 lws 构建选项:
| 选项 | 要求 | 含义 |
|---|---|---|
LWS_ROLE_H1 |
必须开启 | streamtype 走 HTTP/1.1 客户端上传 |
LWS_WITHOUT_CLIENT |
必须关闭 | 需要客户端能力 |
LWS_WITH_SECURE_STREAMS |
必须开启 | Secure Streams API(顶层 CMakeLists.txt 默认 ON) |
LWS_WITH_SECURE_STREAMS_AUTH_SIGV4 |
必须开启 | SigV4 认证支持,默认 OFF(见 CMakeLists.txt) |
USE_WOLFSSL |
必须关闭 | 本示例假设非 wolfSSL TLS 后端 |
也就是说,使用默认选项编译的 libwebsockets 不带 SigV4 支持,需要显式 cmake -DLWS_WITH_SECURE_STREAMS_AUTH_SIGV4=1 后再构建 lws 本体;该宏同时控制 lws-secure-streams.h 中 lws_ss_sigv4_set_aws_key() 与 lws_aws_filesystem_credentials_helper() 两个 API 的可见性。此外,若 lws 以 LWS_WITH_SECURE_STREAMS_PROXY_API(ssproxy 模式)构建,示例会额外编出 -client 变体,通过 Unix domain socket / TCP 连接 ssproxy 运行(CMakeLists.txt)。
构建与运行(在 lws 仓库根目录内):
$ cmake . && make
理解 policy:s3PutObj 策略流
policy.json 是理解整个示例的关键,它由五部分组成:
retry:命名重试策略default,退避表[100, 200, 300, 500, 1000]ms,conceal: 5、jitterpc: 20(20% 抖动)、svalidping: 30/svalidhup: 35(连接有效性检测参数)。certs+trust_stores:三张 BASE64 DER 根证书(Amazon Root CA 1、Starfield Services Root CA、Baltimore CyberTrust Root),组成名为s3-root-cert的信任栈,供 TLS 校验 S3 服务端证书。auth:{"name": "sigv4_br", "type": "sigv4", "blob": 0}——声明一个 SigV4 认证器,凭据存放在 system blob 索引 0 对应的槽位(LWS_SYSBLOB_TYPE_EXT_AUTH1,见 lws-system.h,库支持最多 4 组 EXT_AUTH 槽位)。s(streamtype 策略):s3PutObj的完整定义:
{"s3PutObj": {
"endpoint": "${s3bucket}.s3.amazonaws.com",
"port": 443, "protocol": "h1",
"http_method": "PUT", "http_url": "${s3Obj}",
"http_no_content_length": false,
"tls": true, "tls_trust_store": "s3-root-cert",
"opportunistic": true, "retry": "default",
"use_auth": "sigv4_br",
"aws_region": "region", "aws_service": "service",
"metadata": [
{"region": ""}, {"service": ""},
{"s3bucket": ""}, {"s3Obj": ""},
{"ctype": "content-type:"},
{"xcsha256": "x-amz-content-sha256:"},
{"xdate": "x-amz-date:"},
{"xacl": "x-amz-acl:"}
]}}
要点:
endpoint与http_url中的${s3bucket}、${s3Obj}是占位符,由运行时 metadata 展开,即 S3 的虚拟主机寻址<bucket>.s3.amazonaws.com加上对象路径。aws_region/aws_service本身并不携带 region/service 的值,而是指向 metadata 的名称(本例分别指向名为region和service的 metadata 条目)。解析器把它们直接保存为指针(lws-secure-streams-policy.h 中注释“directly point to the metadata name, no need to expand”),JSON 侧的解析在 policy-json.c 的LSSPPT_AWS_REGION/LSSPPT_AWS_SERVICE分支。metadata列表中“值形如content-type:这类带冒号后缀的条目”表示该 metadata 会映射为同名 HTTP 请求头;xcsha256、xdate、xacl三个x-amz-*头则是 SigV4 必须签名并携带的头。README 特别强调:aws_region、aws_service通过 metadata 配置,且 metadata 中至少要有x-amz-content-sha256:和x-amz-date:两个头——缺任一项签名器会直接报错(见下文源码分析)。
对于嵌入式或最小化构建,policy 也可以预编译为 C 结构体:static_policy.h 就是由上述 JSON 自动生成的静态策略(文件头注明 “Autogenerated from the following JSON policy”,并给出估算内存占用约 3.5KB),配合 LWS_WITH_SECURE_STREAMS_STATIC_POLICY_ONLY 使用,可避免运行期 JSON 解析。
凭据:从 ~/.aws/credentials 到 system blob
README 的 credentials 一节说明:凭据从 ~/.aws/credentials 读取(需要有效的 keyid 和 key),再调用 lws_ss_sigv4_set_aws_key() 把凭据插入 Secure Streams,且其 blob index 必须与 policy 中 auth 条目的 "blob" 值一致;另外 S3 bucket 名全球唯一,需要改成自己的。
入口代码 ss-s3-main.c 在系统状态进入 LWS_SYSTATE_OPERATIONAL 时完成这两步:
if (lws_aws_filesystem_credentials_helper(
"~/.aws/credentials",
"aws_access_key_id",
"aws_secret_access_key",
&aws_keyid, &aws_key))
return -1;
lws_ss_sigv4_set_aws_key(context, 0, aws_keyid, aws_key);
lws_aws_filesystem_credentials_helper()(sign.c)逐行扫描凭据文件,匹配aws_access_key_id=/aws_secret_access_key=前缀,裁剪首尾空白后 malloc 保存结果;~前缀会被替换为$HOME。失败时返回非零且不分配任何输出,成功则调用方负责free()(示例在 main 退出路径释放,见 ss-s3-main.c)。lws_ss_sigv4_set_aws_key(context, 0, ...)(sign.c)把 keyid 与 secret key 分别写入blob_idx[0](即LWS_SYSBLOB_TYPE_EXT_AUTH1)对应的LWS_SS_SIGV4_KEYID/LWS_SS_SIGV4_KEY两个 system blob 槽位。参数idx对应 policy"auth"中的"blob"值(示例为 0,对应 EXT_AUTH1);LWS_SS_USE_SSPC(ssproxy)模式下凭据不在此进程注入。
处理器:状态机、metadata 注入与 payload 哈希
ss-s3-ss.c 的核心是 ss_s3_state()。在 LWSSSCS_CREATING 阶段(stream 刚建立、尚未发起网络事务):
- 生成 1KB 测试 payload(
'a' + i % 16循环填充),记录到m->buf / m->total; - 用
lws_genhash_*(SHA-256)计算 payload 哈希并转 hex,存入payload_hash——这就是 S3 要求的x-amz-content-sha256头值(sigv4_sha256hash_payload); - 用
gmtime_r生成 ISO8601 基本格式时间戳YYYYMMDDTHHMMSSZ,作为x-amz-date; - 依次调用
lws_ss_set_metadata()绑定 8 项 metadata:s3bucket(示例桶名sstest2020)、s3Obj(对象名SSs3upload2.txt)、ctype(text/plain)、region(us-west-2)、service(s3)、xacl(bucket-owner-full-control)、xcsha256(上一步的 payload 哈希)、xdate; - 返回
lws_ss_request_tx_len(m->ss, m->total)声明要发送的总字节数。
此后 LWSSSCS_CONNECTED 时调用 lws_ss_request_tx() 驱动发送;LWSSSCS_QOS_ACK_REMOTE 表示远端已接收并确认全部 TX(bad = 0,即上传成功);LWSSSCS_QOS_NACK_REMOTE、LWSSSCS_ALL_RETRIES_FAILED 分别置失败;LWSSSCS_DISCONNECTED 后销毁 handle,rx 收到 LWSSS_FLAG_EOM 也直接退出事件循环(ss_s3_rx)。
注意这里的“metadata 是运行期数据,policy 是运行期数据模板”的分工:policy 只声明了 8 个 metadata 名称及其中 4 个对应的 HTTP 头名(值带 : 后缀者为头),region/service 两个 metadata 还兼任 SigV4 的 scope 参数。应用侧改桶名、region、对象名时只改 ss-s3-ss.c 顶部的 s3bucketName / s3ObjName / awsRegion 等常量即可。
SigV4 签名在库内如何完成
use_auth: "sigv4_br" 触发 h1 协议在组请求头时调用签名器:ss-h1.c 判断 policy 的 auth type 为 "sigv4" 后调用 lws_ss_apply_sigv4(),最终实现全部在 sign.c。从源码结构看,计算链与 AWS SigV4 规范逐段对应:
init_sigv4():收集参与签名的头。固定加入host:,然后遍历 policy metadata——凡默认值以x-amz-开头的(本例即x-amz-content-sha256:、x-amz-date:、x-amz-acl:)都纳入签名集,同时按 policy 的aws_region/aws_service指向从 metadata 取出 region 与 service(sign.c)。头名按字典序qsort排序,并在此处顺手提取timestamp(取自x-amz-date)与payload_hash(取自x-amz-content-sha256)。缺 timestamp 或 payload_hash 时直接失败——这正是 README 要求这两个头必须存在的底层原因。build_sign_string():先 SHA-256 计算 canonical request(method + URI + 空 query string + 排序后的name:value头 + 头名列表 + payload hash),再拼出 StringToSign:AWS4-HMAC-SHA256/ ISO8601 时间戳 /YYYYMMDD/region/service/aws4_request/ Hex(Hash(canonical_request))(sign.c)。calc_signing_key():从 system blob(blob_idx<a href="https://link.gitcode.com/i/62c3d98bd9144ecd96acf5adf60b664c" target="_blank">blob_index]的LWS_SS_SIGV4_KEY槽位,即lws_ss_sigv4_set_aws_key()写入的 secret key)逐级派生 DateKey → DateRegionKey → DateRegionServiceKey → SigningKey([sign.c)。build_auth_string():对 StringToSign 做最终 HMAC-SHA256,拼出Authorization: AWS4-HMAC-SHA256 Credential=.../YYYYMMDD/region/service/aws4_request, SignedHeaders=..., Signature=...,并以 HTTP 头形式追加到请求里(sign.c)。
全部哈希/HMAC 走 lws 的 genhash/genhmac 抽象(LWS_GENHASH_TYPE_SHA256、LWS_GENHMAC_TYPE_SHA256),因此不依赖外部 OpenSSL,适合嵌入式部署。
运行与预期输出
编译后运行该示例,一次成功上传的典型日志如下(来自示例 README 的 usage 一节):
[2020/12/19 15:25:06:9763] U: LWS minimal secure streams sigv4
[2020/12/19 15:25:07:0768] U: ss_s3_state: LWSSSCS_CREATING, ord 0x0
[2020/12/19 15:25:07:0769] U: ss_s3_state: LWSSSCS_POLL, ord 0x0
[2020/12/19 15:25:07:0770] U: ss_s3_state: LWSSSCS_CONNECTING, ord 0x0
[2020/12/19 15:25:07:2317] U: SS / TX Payload
[2020/12/19 15:25:07:2317] U: SS / TX Payload Total = 1024, Pos = 0
[2020/12/19 15:25:07:3267] U: ss_s3_state: LWSSSCS_CONNECTED, ord 0x0
[2020/12/19 15:25:07:3267] U: ss_s3_state: LWSSSCS_QOS_ACK_REMOTE, ord 0x0
[2020/12/19 15:25:07:3267] U: ss_s3_state: LWSSSCS_DISCONNECTED, ord 0x0
[2020/12/19 15:25:07:3268] U: ss_s3_state: LWSSSCS_DESTROYING, ord 0x0
[2020/12/19 15:25:07:3269] U: Completed: OK
状态顺序 CREATING → POLL → CONNECTING → CONNECTED → QOS_ACK_REMOTE → DISCONNECTED → DESTROYING 是 Secure Streams 客户端上传事务的完整生命周期;QOS_ACK_REMOTE 出现在这里意味着远端(S3 侧经库的 HTTP 事务完成判定)确认了全部 TX。程序以 Completed: OK / 返回码 0 收尾;若重试耗尽进入 LWSSSCS_ALL_RETRIES_FAILED,则以失败退出(ss-s3-ss.c)。
把它变成自己场景的清单
从示例结构看,复用到自己的 S3 场景时通常只需动三处:
- policy(或
default_ss_policy字符串 /static_policy.h):确认endpoint的虚拟主机寻址、http_url对象路径占位符、trust store 与use_auth仍匹配你的目标(若换到其他 AWS 服务,type: "sigv4"复用不变,但 metadata 中的x-amz-*头集合需按该服务的签名要求调整); LWSSSCS_CREATING中的 metadata:bucket、对象名、region、service、payload 哈希、时间戳逐项lws_ss_set_metadata(),注意 region/service 的名称必须与 policy 中aws_region/aws_service指向的 metadata 名一致;- 凭据注入:
lws_ss_sigv4_set_aws_key(context, idx, keyid, key)的idx与 policy"auth"中"blob"一致(示例均为 0);凭据来源不限于~/.aws/credentials,helper 接受任意路径与键名,也可自行取得后直接注入。
参考入口:示例 README、policy.json、lws-secure-streams.h 中的 API 文档、以及 SigV4 核心实现 sign.c。