libwebsockets Secure Streams Sigv4 实战:用 s3PutObj 策略流将文件上传到 AWS S3

原创2026-10-13 15:11:201,897 阅读
文章标签:网络通信WebSocket嵌入式

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 是理解整个示例的关键,它由五部分组成:

  1. retry:命名重试策略 default,退避表 [100, 200, 300, 500, 1000] ms,conceal: 5、jitterpc: 20(20% 抖动)、svalidping: 30 / svalidhup: 35(连接有效性检测参数)。
  2. certs + trust_stores:三张 BASE64 DER 根证书(Amazon Root CA 1、Starfield Services Root CA、Baltimore CyberTrust Root),组成名为 s3-root-cert 的信任栈,供 TLS 校验 S3 服务端证书。
  3. auth:{"name": "sigv4_br", "type": "sigv4", "blob": 0}——声明一个 SigV4 认证器,凭据存放在 system blob 索引 0 对应的槽位(LWS_SYSBLOB_TYPE_EXT_AUTH1,见 lws-system.h,库支持最多 4 组 EXT_AUTH 槽位)。
  4. 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 刚建立、尚未发起网络事务):

  1. 生成 1KB 测试 payload('a' + i % 16 循环填充),记录到 m->buf / m->total;
  2. 用 lws_genhash_*(SHA-256)计算 payload 哈希并转 hex,存入 payload_hash——这就是 S3 要求的 x-amz-content-sha256 头值(sigv4_sha256hash_payload);
  3. 用 gmtime_r 生成 ISO8601 基本格式时间戳 YYYYMMDDTHHMMSSZ,作为 x-amz-date;
  4. 依次调用 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;
  5. 返回 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 场景时通常只需动三处:

  1. policy(或 default_ss_policy 字符串 / static_policy.h):确认 endpoint 的虚拟主机寻址、http_url 对象路径占位符、trust store 与 use_auth 仍匹配你的目标(若换到其他 AWS 服务,type: "sigv4" 复用不变,但 metadata 中的 x-amz-* 头集合需按该服务的签名要求调整);
  2. LWSSSCS_CREATING 中的 metadata:bucket、对象名、region、service、payload 哈希、时间戳逐项 lws_ss_set_metadata(),注意 region/service 的名称必须与 policy 中 aws_region / aws_service 指向的 metadata 名一致;
  3. 凭据注入: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。

登录后查看全文
libwebsockets