news 2026/9/25 8:53:40

Tekton Pipeline 依赖库 go-fed/httpsig:HTTP Signatures 请求/响应签名与验证实现解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Tekton Pipeline 依赖库 go-fed/httpsig:HTTP Signatures 请求/响应签名与验证实现解析
  • 云原生
  • CI/CD
  • DevOps
  • 后端

【免费下载链接】pipeline

A cloud-native Pipeline resource.

项目地址:https://gitcode.com/gh_mirrors/pipelin/pipeline
点击查看免费下载

本文以 Tekton Pipeline 仓库中 vendored 的第三方库go-fed/httpsig(v1.1.0)的 README 为核心,系统讲解 HTTP Signatures 方案的 Go 实现:如何构造Signer对 HTTP 请求与响应进行签名、如何构造Verifier完成验签、支持哪些签名与摘要算法、签名串是如何拼接的,并结合同仓库中 vendored 的 Gitea SDK 源码,展示该库在真实工程里“用 SSH 密钥对 API 请求签名”的落地用法。

一、这个库在 Tekton Pipeline 仓库中的位置

go-fed/httpsig是 IETF HTTP Signatures 草案(draft-cavage-http-signatures)的 Go 语言实现,其 README 明确给出的定位是:对 HTTP 请求或响应进行签名与验证,支持 MAC、HMAC 以及对摘要(hash)的 RSA 签名等多种算法组合。在本仓库中,它以间接依赖的方式被 vendor 进来:

  • go.mod 第 114 行声明github.com/go-fed/httpsig v1.1.0 // indirect,即 Tekton Pipeline 主模块本身不直接 import 该包,而是经由其他依赖引入;
  • vendor/modules.txt 第 369–371 行同样记录了该模块的版本与包路径;
  • 从源码结构看,本仓库内直接 import 该库的代码是 vendored 的 Gitea SDK:httpsign.go 中以legacyhttpsig "github.com/go-fed/httpsig"命名导入,与它的后继 forkgithub.com/42wim/httpsig并列使用。Tekton Pipeline 的 Gitea 集成测试(如 test/resolvers_gitea_test.go、test/git-resolver/gitea.yaml)依赖 Gitea SDK 与 Gitea 实例交互,因此该签名库最终服务于“客户端对 Gitea API 请求做签名认证”这一场景。

理解这一依赖关系很重要:阅读本库时不应把它当作 Tekton 控制器的核心组件,而应把它看作一条传递依赖链上的基础设施——它保证了与 Gitea(尤其是较老版本 Gitea)通信时,API 请求可以通过 HTTP 签名而非仅靠 Bearer Token 完成身份认证。

二、README 声明的设计目标

README 开宗明义地列出了库的设计目标,这些目标与源码实现一一对应:

  1. 提供非常简单、统一的签名与验证接口——对应Signer/Verifier两个接口;
  2. 支持多种签名算法及其组合——对应 httpsig.go 中的Algorithm常量族;
  3. 签名可以写入Authorization或Signature两个 HTTP 头之一——对应SignatureScheme类型;
  4. 参与签名的请求头集合是灵活配置的——对应NewSigner的headers参数;
  5. 同时支持 HTTP 请求与响应——对应SignRequest与SignResponse;
  6. 明确不支持已知密码学不安全的算法——对应 algorithms.go 中isForbiddenHash对 MD4、MD5、MD5SHA1 的封禁;
  7. 自动计算并签名Digest头,保证消息体不被篡改——对应 digest.go 的实现。

安装与导入方式按 README 说明为go get github.com/go-fed/httpsig,代码中import "github.com/go-fed/httpsig";在本仓库中则通过 vendor 机制使用,无需额外安装。

三、签名:Signer 的创建与使用

3.1 README 的签名示例(完整继承)

README 给出的请求签名示例是:创建Signer后调用SignRequest。要点是签名前必须保证Date、Digest头与r.URL已设置,并传入用于生成摘要的 body 副本:

func sign(privateKey crypto.PrivateKey, pubKeyId string, r *http.Request) error { prefs := []httpsig.Algorithm{httpsig.RSA_SHA512, httpsig.RSA_SHA256} digestAlgorithm := DigestSha256 // The "Date" and "Digest" headers must already be set on r, as well as r.URL. headersToSign := []string{httpsig.RequestTarget, "date", "digest"} signer, chosenAlgo, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature) if err != nil { return err } // To sign the digest, we need to give the signer a copy of the body... // ...but it is optional, no digest will be signed if given "nil" body := ... // If r were a http.ResponseWriter, call SignResponse instead. return signer.SignRequest(privateKey, pubKeyId, r, body) }

响应签名的用法相同,只是将SignRequest换成SignResponse,第二个参数改为http.ResponseWriter。

需要特别指出的是:vendored 的 v1.1.0 源码中NewSigner的实际签名比 README 示例多了一个expiresIn int64参数,定义为(见 httpsig.go):

func NewSigner(prefs []Algorithm, dAlgo DigestAlgorithm, headers []string, scheme SignatureScheme, expiresIn int64) (Signer, Algorithm, error)

其语义是:按prefs顺序尝试创建签名器,第一个可用的算法生效并随返回值给出;若所有偏好算法都不可用,则回退到默认算法(RSA_SHA256,见 algorithms.go)。传入未知或已知密码学不安全的Algorithm会直接报错。expiresIn非 0 时,签名器会在签名时记录created时间戳并计算expires = created + expiresIn,这两个值随后写入签名的created=与expires=参数(见 signing.go)。

v1.1.0 还新增了NewSSHSigner(httpsig.go),接受ssh.Signer而非裸crypto.PrivateKey,目前仅支持 ed25519 与 RSA 两类 SSH 密钥(getSSHAlgorithm的映射见 httpsig.go),这为直接使用 OpenSSH 密钥文件做签名提供了更贴近运维习惯的入口。

3.2 签名内部流程:Digest、签名串与签名头

以asymmSigner.SignRequest(signing.go)为例,一次签名分为三步:

  1. 写 Digest:若传入的body非 nil,则先调用addDigest计算 body 的摘要并写入Digest头(digest.go)。摘要格式为<算法> = <base64(摘要)>,例如SHA-256 = base64...;若Digest头已存在会报错,防止覆盖。body 为 nil 时跳过,不生成摘要。
  2. 构造签名串:signatureString(signing.go)按headers列表顺序逐行拼接参与签名的字段。每一行形如<小写头名>: <值>,行与行之间用\n分隔;同名头多值时用,连接。其中三个特殊项:
    • httpsig.RequestTarget(值为(request-target))展开为小写化的请求方法、URL 路径与查询串,如post /api/x?q=1,见addRequestTarget(signing.go);
    • (created)、(expires)展开为对应时间戳数值,缺失时报错;
    • 若headers列表为空,则默认仅签名Date头(defaultHeaders,signing.go);
    • 响应签名不允许出现(request-target),requestTargetNotPermitted会返回错误,因为方法/URI 只对请求有意义。
  3. 写出签名头:setSignatureHeader(signing.go)把 keyId、算法、时间戳、头列表与 base64 编码的签名值拼成一个键值参数串,写入目标头。生成的头形如:
Signature: keyId="my-key",algorithm="hs2019",created=1234,expires=1244,headers="(request-target) date digest",signature="base64..."

注意源码中algorithm参数固定写死为"hs2019"(signing.go 有注释说明真实算法被隐藏、以适配较新的草案版本),真实算法由验证方通过带外信息确定;同时 README 也强调pubKeyId将在验证阶段被用来定位公钥。

3.3 并发安全:Signer 不是线程安全的

README 用专门的篇幅提醒Signer不能在不加保护的情况下被多个 goroutine 共享,并给出了用互斥锁守护的示例:

type server struct { signer httpsig.Signer mu *sync.Mutex } func (s *server) handlerFunc(w http.ResponseWriter, r *http.Request) { privateKey := ... pubKeyId := ... // Set headers and such on w s.mu.Lock() defer s.mu.Unlock() // To sign the digest, we need to give the signer a copy of the response body... // ...but it is optional, no digest will be signed if given "nil" body := ... err := s.signer.SignResponse(privateKey, pubKeyId, w, body) if err != nil { ... } ... }

这一约束来自实现:Signer接口注释明确写着 “Signers are not safe to use between multiple goroutines”(httpsig.go),其内部持有headers、created/expires等可变状态,签名过程会改写请求头。服务端的常见做法如 README 示例所示加锁,或者干脆每次请求新建签名器。

四、支持的算法与摘要:清单与禁区

从 httpsig.go 的Algorithm常量定义可以完整列出 v1.1.0 支持的算法:

  • MAC 类(对称密钥,密钥类型为[]byte):HMAC_SHA224、HMAC_SHA256、HMAC_SHA384、HMAC_SHA512、HMAC_RIPEMD160、HMAC_SHA3_224/256/384/512、HMAC_SHA512_224/256、HMAC_BLAKE2S_256、HMAC_BLAKE2B_256/384/512;此外 BLAKE2 系列还可作为裸 MAC 算法使用(BLAKE2S_256、BLAKE2B_256/384/512,验证时采用常量时间比较,见 algorithms.go);
  • RSA 非对称类:RSA_SHA1、RSA_SHA224、RSA_SHA256(默认算法)、RSA_SHA384、RSA_SHA512、RSA_RIPEMD160;
  • ECDSA 类:ECDSA_SHA224/256/384/512、ECDSA_RIPEMD160,签名结果按 ASN.1 编码ECDSASignature{R, S}(algorithms.go);
  • ED25519:常量ED25519,注释说明“只能配合 SHA512”(ED25519 内部固定哈希,algorithms.go)。

算法字符串解析入口signerFromString/macerFromString(algorithms.go)先按前缀(rsa-、ecdsa-、ed25519、hmac-)归类,再解析哈希名;哈希名必须命中内部表且通过isForbiddenHash检查(algorithms.go):MD4、MD5、MD5SHA1 被硬性封禁,返回 “forbidden hash type” 错误。IsSupportedHttpSigAlgorithm(algorithms.go)对外暴露“某算法是否可用”的查询接口,其判定 = 表中存在 + 非禁用 + 平台可用(crypto.Hash.Available())。

摘要算法(Digest头)只开放两种(digest.go):DigestSha256(SHA-256)与DigestSha512(SHA-512),IsSupportedDigestAlgorithm负责校验。这体现了 README “明确不支持弱算法” 的目标在两层(签名算法、摘要算法)上的落地。

五、验证:Verifier 的构造、时间窗口与验签

5.1 README 的验证示例(完整继承)

验证侧的职责划分是:应用通过KeyId()拿到 keyId,再由应用自己负责据此取回公钥、确定算法,然后调用Verify:

func verify(r *http.Request) error { verifier, err := httpsig.NewVerifier(r) if err != nil { return err } pubKeyId := verifier.KeyId() var algo httpsig.Algorithm = ... var pubKey crypto.PublicKey = ... // The verifier will verify the Digest in addition to the HTTP signature return verifier.Verify(pubKey, algo) }

README 同时说明Verifier也不具备跨 goroutine 并发安全性,但由于验证器是按请求/按响应创建的,这一限制在实际中很少构成问题。

5.2 验证器构造细节:头探测、时间窗口与 Host 注入

NewVerifier(httpsig.go)与NewResponseVerifier(httpsig.go)内部共用newVerifier(verifying.go),关键行为包括:

  • 头位置探测:getSignatureScheme(verifying.go)检查Signature与Authorization两个头中哪一个携带签名参数(以是否含keyId/headers/signature字段判断)。两者都携带会报 “both ... have signature parameters”,都不携带报 “neither ...”;Authorization头会先剥掉Signature认证方案前缀。
  • 参数解析:getSignatureComponents(verifying.go)把参数串按,切分并解析keyId、created、expires、headers、signature;keyId或signature缺失即报错;headers缺省回落到Date;不认识的参数一律忽略;草案中的algorithm参数已废弃,解析时直接忽略(与签名端写死hs2019相呼应)。
  • 10 秒时钟偏差容忍:构造验证器时就做时间校验——created领先当前时间超过 10 秒报 “created is in the future”,expires落后当前时间超过 10 秒报 “signature expired”(verifying.go)。因此签名方设置expiresIn时,窗口必须明显大于 10 秒才可靠。
  • Host 头补齐:Go 服务端解析请求时通常不保留Host到 header map,NewVerifier会检测并用r.Host补写Host头(httpsig.go),使得把Host列入签名头列表在验证端也能取到值。
  • 验签:Verify(pKey, algo)(verifying.go)先按算法前缀判断走非对称(asymmVerify:重算签名串 → base64 解码 signature → 调用signer.Verify)还是 MAC 路径(macVerify:以[]byte密钥重算 MAC 并比较)。键类型约束与签名侧一致:MAC 用[]byte,RSA 用*rsa.PublicKey/*rsa.PrivateKey,ECDSA/ED25519 同理。

六、仓库内真实用例:Gitea SDK 用 SSH 密钥签名 API 请求

vendored 的 Gitea SDK httpsign.go 的Client.SignRequest是该库在本仓库中最直接的调用现场,完整展示了 README 所述“签名请求”流程的实战形态:

headersToSign := []string{httpsig.RequestTarget, "(created)", "(expires)"} // 若持有 SSH 证书,把证书放入 x-ssh-certificate 头并纳入签名 if c.httpsigner.cert { // ... r.Header.Add("x-ssh-certificate", certString) headersToSign = append(headersToSign, "x-ssh-certificate") } // 若有 body,则让库自动添加 Digest 头并把 Digest 纳入签名 if r.Body != nil { contents, _ = io.ReadAll(body) headersToSign = append(headersToSign, "Digest") } // 旧版 Gitea(< 1.23)使用 legacy 版 httpsig(go-fed/httpsig), // 签名有效期 10 秒,写入 Signature 头 legacySigner, _, err := legacyhttpsig.NewSSHSigner( c.httpsigner, httpsig.DigestSha512, headersToSign, legacyhttpsig.Signature, 10) // keyID 取公钥的 SHA-256 指纹(证书模式下用 "gitea") return legacySigner.SignRequest(keyID, r, contents)

这段代码印证了前文各节的多个要点:签名头列表包含(request-target)、(created)、(expires),与expiresIn=10秒配合形成 10 秒有效窗口;有 body 时把Digest加入签名列表,从而把“摘要 + 签名”双保险地绑定到消息体;keyID采用ssh.FingerprintSHA256(公钥),验证端(Gitea 服务端)即可据此定位用户。同时它揭示了 go-fed/httpsig 与 fork 版 42wim/httpsig 的分工:checkServerVersionGreaterThanOrEqual(version1_23_0)判定服务器版本后,老版本 Gitea 走legacyhttpsig(即本 README 对应的库),新版本走 fork——这是“legacy” 命名的由来,也解释了该库在 go.mod 中以 indirect 身份存在的原因。

七、使用要点与限制小结

综合 README 与 v1.1.0 源码,使用(阅读)该库时应注意:

  • 算法偏好列表要给出可回退项:NewSigner会跳过不可用/不安全的算法并在全部失败时回退RSA_SHA256;想强制失败时不要依赖它,而应自己检查返回的chosenAlgo;
  • 键类型必须匹配:MAC 算法要求crypto.PrivateKey/crypto.PublicKey底层是[]byte;RSA 要求*rsa.PrivateKey/*rsa.PublicKey,不匹配会得到显式错误(signing.go、verifying.go);
  • Digest头只能由库设置一次:addDigest在头已存在时报错,调用方不要提前手写Digest;
  • 时间窗口以 10 秒为容忍度:设置expiresIn时应显著大于 10 秒;验证端对created未来时间、expires过期时间都按 ±10 秒偏差判定;
  • 签名头二选一:Signature与Authorization(带Signature认证方案前缀)不能同时携带签名参数,验证端会直接拒绝;
  • 并发模型:Signer与Verifier均非 goroutine 安全,长期复用的签名器需加锁(README 给出的sync.Mutex模式),验证器则建议按请求新建;
  • README 示例与 vendored 版本的偏差:README 中NewSigner为四参调用,而 vendored v1.1.0 为五参(新增expiresIn),且新增了NewSSHSigner;以本仓库 httpsig.go 的实际签名为准。

本文所有结论均来自本仓库内 README 与 vendored 源码(httpsig.go、signing.go、verifying.go、algorithms.go、digest.go、httpsign.go),可直接按文中路径深入阅读原始实现。

  • 云原生
  • CI/CD
  • DevOps
  • 后端

【免费下载链接】pipeline

A cloud-native Pipeline resource.

项目地址:https://gitcode.com/gh_mirrors/pipelin/pipeline
点击查看免费下载
上一篇:Prisma 文档指南
下一篇:Simplefolio模板定制完全教程:从颜色主题到内容替换

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 8:52:40

Atlas:面向 macOS/Rust 开发者的运行时快照与协作基础设施

1. 项目概述&#xff1a;Atlas 不是“地图集”&#xff0c;而是一套面向现代开发者的开源协作基础设施最近在 Rust 社区和 macOS 开发者圈子里&#xff0c;“atlas”这个词频繁出现在技术讨论、CI/CD 配置片段、本地开发环境脚本甚至团队内部文档里。它既不是地理信息系统里的传…

作者头像 李华
网站建设 2026/9/25 8:51:52

GD32高级定时器互补PWM输出与死区控制实战

写GD32的高级定时器&#xff0c;绕不开三相电机控制、全桥逆变、UPS这类场景。做这类项目的人&#xff0c;百分之九十九都躲不过一个需求&#xff1a;要输出两路相位相反、中间还夹着一小段“空白”的PWM&#xff0c;而且这段空白还得精确可控。这段空白就是死区&#xff0c;控…

作者头像 李华
网站建设 2026/9/25 8:45:12

脉冲神经网络SNN:从事件驱动到神经形态芯片的第三代AI技术解析

1. 为什么说SNN是"第三代神经网络"&#xff1a;一场关于信息处理方式的代际演进提到神经网络&#xff0c;大多数人第一反应是深度学习、GPU集群、大模型这些概念。无论是卷积神经网络处理图像&#xff0c;还是Transformer处理语言&#xff0c;本质上做的都是同一件事…

作者头像 李华
网站建设 2026/9/25 8:45:06

treg CLI Agent 实战:OpenRouter 与 MCP 协议构建终端智能体

1. 从"treg"这个标题说起&#xff1a;一个被低估的CLI Agent入口第一次看到"treg"这个词&#xff0c;很多人会以为是某个拼写错误&#xff0c;或者某个小众库的缩写。但如果你最近在折腾 CLI Agent、MCP 协议、OpenRouter 这些关键词&#xff0c;就会意识到…

作者头像 李华
网站建设 2026/9/25 8:42:37

Atlas 300V 部署 YOLO 实战:从模型转换到推理调优全指南

如果你也在纠结“atlas部署yolo”到底怎么搞&#xff0c;以及“Atlas 300V 24G是不是运算加速卡”这类问题&#xff0c;那这篇应该能省你不少时间。我手头有一台装了Atlas 300V 24G的推理服务器&#xff0c;近一年里一直在上面跑目标检测项目&#xff0c;从模型转换到推理服务、…

作者头像 李华
网站建设 2026/9/25 8:40:51

CSP-S 2026初赛模拟卷2:选择题、阅读程序与完善程序解题策略

1. 这套模拟卷到底在练什么CSP-S 初赛的备考&#xff0c;很多人一上来就抱着历年真题猛刷&#xff0c;刷完对个答案就过去了。我见过太多这样的选手&#xff0c;真题正确率看着还行&#xff0c;一到考场上遇到稍微变形的题目就懵。问题出在哪儿&#xff1f;初赛考的不是你记住了…

作者头像 李华