Rekor 可插拔类型(Pluggable Types)机制深度解析:透明日志条目 Schema 插件体系与 BuildKit 中的落地实践
【免费下载链接】buildkitconcurrent, cache-efficient, and Dockerfile-agnostic builder toolkit项目地址: https://gitcode.com/GitHub_Trending/bu/buildkit
Rekor 是 Sigstore 软件供应链安全体系中的透明日志(transparency log)组件,而"可插拔类型"(Pluggable Types)是它用来支撑多种日志条目格式的核心架构。本文以本仓库 vendor 中 pkg/types/README.md 为骨架,结合其 types.go、entries.go、versionmap.go 等源码实现,系统讲解 Rekor 如何通过 Schema + 版本化的方式让同一条日志承载签名记录、DSSE 信封、in-toto 证言等多种数据类型,并说明 BuildKit 为什么只需要 vendor 其中三种类型。读完本文,你将掌握 Rekor 类型插件的注册、解析、版本路由与落库全流程,并理解它与 BuildKit attestation 产物之间的对应关系。
一、什么是 Rekor 的可插拔类型
Rekor 是一个面向软件供应链的透明日志服务:它把"某个签名、某个证书、某个软件工件"的加密学证据固化到不可篡改的日志中,供事后审计与验证。然而,不同类型的证据结构差异巨大——一条简单的 GPG 签名记录与一份完整的 in-toto 证言(attestation)在字段组织上完全不同。如果日志只接受一种固定格式,就无法覆盖多样化的供应链安全场景。
为此,Rekor 引入了可插拔类型(Pluggable Types):日志中的每一条条目(entry)都归属于一个具体的"类型"(kind),而每种类型又对应一套 JSON Schema,并可以拥有多个版本(version)。正如 pkg/types/README.md 所描述的:"Rekor supports pluggable types (aka different schemas) for entries stored in the transparency log"——可插拔类型的本质,就是为存储进透明日志的条目提供不同的 Schema。
这种设计带来的直接收益是:
- 格式解耦:类型与核心日志逻辑解耦,新增一种证据格式无需改动日志写入、索引、验证的通用链路;
- 向后兼容:同一类型可以并行保留多个版本,老版本条目在日志中仍然可读;
- 可扩展性:社区可以按既定约定为 Rekor 增加新的类型支持(README 末尾也给出了官方扩展指引)。
二、当前支持的类型全景
README 开列了 Rekor 当前支持的全部类型与对应 Schema、版本号。这里完整继承并整理如下:
| 类型(Kind) | 用途 | 版本 |
|---|---|---|
| Alpine Packages | Alpine 软件包签名记录 | 0.0.1 |
| COSE Envelopes | COSE 签名信封 | 0.0.1 |
| DSSE Envelopes | DSSE(Dead Simple Signing Envelope)签名信封 | 0.0.1 |
| HashedRekord | 仅存储工件哈希的签名记录 | 0.0.1 |
| Helm Provenance Files | Helm Chart 来源证明文件 | 0.0.1 |
| In-Toto Attestations | in-toto 证言(attestation) | 0.0.1、0.0.2 |
| Java Archives (JAR Files) | Java JAR 包签名记录 | 0.0.1 |
| Rekord(默认类型) | 通用签名记录(含工件内容) | 0.0.1 |
| RFC3161 Timestamps | RFC3161 时间戳令牌 | 0.0.1 |
| RPM Packages | RPM 软件包签名记录 | 0.0.1 |
| TUF Metadata | TUF 元数据签名记录 | 0.0.1 |
其中Rekord被标记为默认类型(default type),即未显式指定类型时使用的兜底条目格式。
需要特别指出的是:本文所在仓库(BuildKit)是 Rekor 的消费方,vendor 目录中只保留了与构建产物证言(attestation)最相关的三个类型的完整实现与 Schema,即dsse、hashedrekord、intoto(见 vendor/github.com/sigstore/rekor/pkg/types 目录结构)。表中其余类型属于完整 Rekor 服务端支持范围,本仓库未携带其源码文件,下文源码分析将以这三个 vendored 类型为主。
三、插件架构源码级剖析
可插拔类型的实现由三部分构成:类型注册表(TypeMap)、类型基类与接口(RekorType / TypeImpl)、版本工厂映射(VersionMap)。三者共同完成"按 kind 找到类型、按 version 找到实现"的两级路由。
3.1 类型注册表 TypeMap
在 types.go 中,Rekor 用一个sync.Map保存"类型字符串 → 类型构造器"的映射:
// TypeMap stores mapping between type strings and entry constructors // entries are written once at process initialization and read for each transaction, so we use // sync.Map which is optimized for this case var TypeMap sync.Map从注释可以读出设计意图:映射在进程初始化阶段一次性写入(各类型通过各自的init()完成注册),随后每次日志事务都会读取。写少读多、且并发读频繁,因此选用专为这种场景优化的sync.Map。
每种具体类型在自己的包中完成注册。以 dsse.go 为例:
func init() { types.TypeMap.Store(KIND, New) } func New() types.TypeImpl { bit := BaseDSSEType{} bit.Kind = KIND bit.VersionMap = VersionMap return &bit }hashedrekord(hashedrekord.go)与intoto(intoto.go)采用完全相同的注册模式:包内定义KIND常量,init()阶段把New构造器存入TypeMap。这意味着新增一种类型只需新增一个包并实现接口,无需改动任何既有代码——这正是"可插拔"的落点。
TypeMap 还提供了两个查询入口(types.go):
ListSupportedKinds():返回所有已加载的条目 kind;ListImplementedTypes():返回所有"kind:version"组合的列表(形如intoto:0.0.2),供 API 对外暴露能力清单。
3.2 类型基类 RekorType 与接口 TypeImpl
每种类型的基类都嵌入统一的RekorType结构(types.go):
type RekorType struct { Kind string // this is the unique string that identifies the type VersionMap VersionEntryFactoryMap // this maps the supported versions to implementation }它携带两个字段:类型标识Kind,以及负责"版本 → 实现工厂"分发的VersionMap。
所有类型都必须实现TypeImpl接口(types.go):
type TypeImpl interface { CreateProposedEntry(context.Context, string, ArtifactProperties) (models.ProposedEntry, error) DefaultVersion() string SupportedVersions() []string IsSupportedVersion(string) bool UnmarshalEntry(pe models.ProposedEntry) (EntryImpl, error) }接口中一个关键方法是VersionedUnmarshal(定义于 types.go),它演示了版本路由的完整过程:先从VersionMap中按版本取出工厂函数ef,调用ef()生成该版本的条目实例,若传入的 proposed entry 非空则调用其Unmarshal方法填充数据:
func (rt *RekorType) VersionedUnmarshal(pe models.ProposedEntry, version string) (EntryImpl, error) { ef, err := rt.VersionMap.GetEntryFactory(version) if err != nil { return nil, fmt.Errorf("%s implementation for version '%v' not found: %w", rt.Kind, version, err) } entry := ef() ... return entry, entry.Unmarshal(pe) }DefaultVersion()、SupportedVersions()、IsSupportedVersion()则分别回答"默认用哪个版本、支持哪些版本、某个版本能否写入日志"。
3.3 版本工厂映射 VersionEntryFactoryMap 与语义化版本路由
versionmap.go 定义了版本映射接口:
type VersionEntryFactoryMap interface { GetEntryFactory(string) (EntryFactory, error) // return the entry factory for the specified version SetEntryFactory(string, EntryFactory) error // set the entry factory for the specified version Count() int // return the count of entry factories currently in the map SupportedVersions() []string // return a list of versions currently stored in the map }其默认实现SemVerEntryFactoryMap是这篇文章最值得展开的细节:它并不做字符串等值匹配,而是用 blang/semver 库做语义化版本区间匹配(versionmap.go):
SetEntryFactory(constraint, ef)把形如0.0.1、>=0.0.1的版本约束作为 key 存入factoryMap,写入前先用semver.ParseRange校验约束合法性;GetEntryFactory(version)先把请求版本解析为semver.Version,再遍历 map,用每个约束的 range 去匹配,返回第一个匹配的工厂函数;- 内部用
sync.RWMutex保护并发读写。
这一设计让"支持版本"的表达能力远超普通 map:未来可以轻松表达">=0.0.2, <1.0.0都归到某个实现"这类区间路由。每个类型在包的init()中向自己的VersionMap注册版本实现,例如 DSSE v0.0.1 在 dsse/v0.0.1/entry.go 中:
func init() { if err := dsseType.VersionMap.SetEntryFactory(APIVERSION, NewEntry); err != nil { log.Logger.Panic(err) } }3.4 版本化条目的行为契约 EntryImpl
版本路由最终得到的对象是EntryImpl——它是"某个具体版本条目"的行为契约(entries.go):
type EntryImpl interface { APIVersion() string // the supported versions for this implementation IndexKeys() ([]string, error) // the keys that should be added to the external index for this entry Canonicalize(ctx context.Context) ([]byte, error) // marshal the canonical entry to be put into the tlog Unmarshal(e models.ProposedEntry) error // unmarshal the abstract entry into the specific struct for this versioned type CreateFromArtifactProperties(context.Context, ArtifactProperties) (models.ProposedEntry, error) Verifiers() ([]pkitypes.PublicKey, error) // list of keys or certificates that can verify an entry's signature ArtifactHash() (string, error) // hex-encoded artifact hash prefixed with hash name, e.g. sha256:abcdef Insertable() (bool, error) // denotes whether the entry that was unmarshalled has the writeOnly fields required to validate and insert into the log }各方法分工清晰:Unmarshal把抽象 proposed entry 落到具体结构体;Canonicalize产出写入透明日志的规范化字节;IndexKeys决定该条目在外部索引(供检索)中暴露的键;Verifiers返回能验证条目签名的公钥/证书;ArtifactHash返回形如sha256:abcdef的工件摘要;Insertable报告反序列化后的条目是否携带写入所需的 writeOnly 字段。对于还承载证言(attestation)的类型,另有扩展接口EntryWithAttestationImpl(entries.go),追加AttestationKey与AttestationKeyValue两个方法用于证言的存取。
四、三种 vendored 类型与 Schema 详解
本仓库实际携带了dsse、hashedrekord、intoto三套完整实现。下面结合各自的 JSON Schema 与实现逐一拆解。
4.1 HashedRekord v0.0.1:只存哈希的签名记录
HashedRekord 与默认类型 Rekord 的最大区别是:它不存工件原始内容,只存工件的哈希,从而避免把大型工件写入日志。其 Schema 位于 hashedrekord/v0.0.1/hashedrekord_v0_0_1_schema.json,顶层结构为:
{ "signature": { "content": { "type": "string", "format": "byte" }, "publicKey": { "content": { "type": "string", "format": "byte" } } }, "data": { "hash": { "algorithm": { "enum": ["sha256", "sha384", "sha512"] }, "value": { "type": "string" } } }, "required": ["signature", "data"] }要点:
signature.content:内联的签名内容(base64 编码的字节串);signature.publicKey.content:可验证该签名的公钥,也允许直接放 X.509 代码签名证书(Schema 描述明确写明 "this can also be an X509 code signing certificate"),证书中即含公钥信息;data.hash.algorithm:枚举限定为sha256/sha384/sha512三种哈希算法;data.hash.value:工件哈希值,必须是小写十六进制字符串(Schema 描述:"as represented by a lower case hexadecimal string")。
该类型的实现入口见 hashedrekord.go:UnmarshalEntry先做类型断言(pe.(*models.Hashedrekord)),再校验APIVersion非空,最后交给VersionedUnmarshal按版本路由到 v0.0.1 实现;DefaultVersion()返回0.0.1。
4.2 DSSE v0.0.1:签名信封
DSSE(Dead Simple Signing Envelope)是 Sigstore 推荐的签名封装格式,被 in-toto 证言广泛采用。其 Schema 位于 dsse/dsse_schema.json,通过oneOf引用 dsse/v0.0.1/dsse_v0_0_1_schema.json。后者字段分为三组,读写权限刻意区分:
proposedContent(writeOnly,提交时使用):内含envelope(整个 DSSE 信封的字符串化 JSON)与verifiers(验证材料,base64 编码的公钥/证书数组,至少 1 项),二者均为必填;signatures(readOnly,读取时返回):服务端从信封中提取出的签名集合,按 base64 签名串的字典序排序;每项含signature(带 base64 正则校验)与verifier;envelopeHash/payloadHash(均readOnly):前者是整个信封(含签名)的摘要,后者是信封内payload的摘要,算法均枚举为sha256。
vendor/github.com/sigstore/rekor/pkg/types/dsse/README.md 对存储语义做了关键澄清,值得原文继承:
- 如何识别:条目的
Body字段中包含dsseObj字段即为 DSSE 对象; - 可识别的内容类型:in-toto statement 会被识别并解析,其中 subject 的哈希会被加入索引以便检索;
- Rekor 实际存储什么:只存 payload 的哈希、整个 DSSE 信封(含签名)的哈希、签名以及对应的验证材料(公钥/证书);即使配置了 attestation 存储,也不会存储完整的 DSSE 信封。
这种"只存哈希、不存全文"的取舍,既保证了证据可校验,又控制了日志体积。
4.3 In-Toto v0.0.1 / v0.0.2:软件供应链证言
in-toto 类型承载的是证言(attestation)——关于软件工件的一种可认证、机器可读的元数据。本仓库中 intoto/intoto_schema.json 通过oneOf同时引用 v0.0.1 与 v0.0.2 两个版本的 Schema,这与 README 中"Versions: 0.0.1, 0.0.2"一致,也是 11 个类型中唯一支持双版本的类型。
vendor/github.com/sigstore/rekor/pkg/types/intoto/README.md 给出了语义约定:
- Attestation:关于一个或多个软件工件的可认证、机器可读的元数据(采用 SLSA 定义),其值应为Base64 编码的 JSON 对象;
- AttestationType:标识证言类型(如 provenance 构建来源证言、漏洞扫描证言),即使其值带有
http前缀,也不一定是一个可访问的 URL; - 如何识别:条目的
Body字段中包含IntotoObj字段即为 in-toto 对象。
v0.0.2 的 Schema(intoto/v0.0.2/intoto_v0_0_2_schema.json)结构为:
{ "content": { "envelope": { "payload": { "type": "string", "format": "byte", "writeOnly": true }, "payloadType": { "type": "string" }, "signatures": { "type": "array", "minItems": 1, "items": { "keyid": { "type": "string" }, "sig": { "type": "string", "format": "byte" }, "publicKey": { "type": "string", "format": "byte" } }, "required": ["sig", "publicKey"] } }, "hash": { "algorithm": { "enum": ["sha256"] }, "value": { "type": "string" } }, "payloadHash": { "algorithm": { "enum": ["sha256"] }, "value": { "type": "string" } } }, "required": ["content"] }可见 v0.0.2 直接在条目内嵌DSSE 信封(content.envelope):payload为 writeOnly 的字节串,signatures中每项含可选的keyid、必填的sig与publicKey;hash覆盖整个信封,payloadHash覆盖 payload,二者均为sha256且 readOnly。
实现层面,intoto.go 有两点值得注意:
DefaultVersion()返回0.0.2,而SupportedVersions()返回["0.0.2", "0.0.1"](按偏好顺序),与 README 的版本列表对应;CreateProposedEntry在未指定版本时,会构建一个ProposedIntotoEntryIterator链表,把支持的所有版本都生成一遍(intoto.go),由上层逐项尝试提交——这样即使某个版本在当前服务端被禁用,其他版本仍可兜底。
v0.0.2 实现 intoto/v0.0.2/entry.go 还展示了证言类型的索引逻辑(IndexKeys):对每个签名,计算其规范化公钥的sha256:hex摘要并追加 key 的Subjects();随后追加payloadHash的algorithm:value键;当payloadType为 in-toto 时还会解析 statement 的 subject。此外该文件定义了maxAttestationSize = 100 * 1024(100 KiB)的证言大小上限,并提供SetMaxAttestationSize供外部调整,防止超大证言拖垮日志服务。
五、条目的完整生命周期:从提交到落库
理解插件机制后,再看一条日志条目的生命周期,源码路径为 entries.go。
5.1 统一参数载体 ArtifactProperties
CLI 传入的工件、签名、公钥等杂项信息,统一收敛到ArtifactProperties结构(entries.go):
type ArtifactProperties struct { AdditionalAuthenticatedData []byte ArtifactPath *url.URL ArtifactHash string ArtifactBytes []byte SignaturePath *url.URL SignatureBytes []byte PublicKeyPaths []*url.URL PublicKeyBytes [][]byte PKIFormat string }它的注释点明了职责:"provide a consistent struct for passing values from CLI flags to the type+version specific CreateProposeEntry() methods"——即把 CLI 层与类型实现层解耦,各类型只需按需取用。
5.2 创建 Proposed Entry
NewProposedEntry(ctx, kind, version, props)(entries.go)从TypeMap中取出 kind 对应的构造器,实例化TypeImpl后调用CreateProposedEntry(ctx, version, props)。类型实现内部会先补默认版本(如if version == "" { version = it.DefaultVersion() },见 dsse.go),再通过VersionedUnmarshal(nil, version)拿到版本实现,最终调用其CreateFromArtifactProperties生成具体的 proposed entry。
5.3 校验并落库:CreateVersionedEntry
CreateVersionedEntry(pe)(entries.go)是插入路径(insertion flow)的入口,它依次执行三道检查:
- kind 白名单:
isKindAllowedForSubmission检查该 kind 是否在服务端启用。值得注意的是,这个白名单通过SetAllowedKindsForSubmission配置(entries.go),且用atomic.Pointer存储以支持运行时热更新;注释明确说明该限制只作用于插入路径,读取路径不受影响——这样在旧配置下写入的条目永远可读; - 版本支持:
TypeMap中能找到该 kind,且其IsSupportedVersion(ei.APIVersion())为真; - 可插入性:
ei.Insertable()返回真,即反序列化出的条目携带了写入所需的 writeOnly 字段。
三道检查全部通过后,条目才能进入签名、写入透明日志的后续阶段。与之相对的UnmarshalEntry(pe)(entries.go)是读取路径:不校验版本是否仍可写入,只做类型断言与版本路由,适用于回读历史条目。
5.4 解码与规范化:DecodeEntry / CanonicalizeEntry
两个辅助函数保证了条目数据的正确性:
DecodeEntry(input, output)(entries.go)基于mapstructure实现字段类型转换:当目标字段是[]byte时自动对源字符串做 base64 解码;当目标字段是strfmt.DateTime时自动解析时间串;CanonicalizeEntry(ctx, entry)(entries.go)先把条目 marshal 出来,再经jsoncanonicalizer.Transform按RFC 8785(JCS,JSON Canonicalization Scheme)做规范化,以"防止 Go 的 JSON marshal 因实现差异重排字段顺序"——这是保证透明日志条目字节级确定性、进而保证可校验性的关键。
六、与 BuildKit 的关联:证言产物如何对接 in-toto 类型
理解了 Rekor 的 in-toto 类型后,就能明白 BuildKit 为什么 vendor 上述代码。BuildKit 在构建镜像时支持生成SBOM、provenance 等 in-toto 证言(attestation),这些证言最终可被推送到 Rekor 之类的透明日志中存档验证。
本仓库中,exporter/attestation/make.go 的MakeInTotoStatements(L105-L120)逐条读取构建结果中的 attestation 内容,将其组装为 in-totoStatement:makeInTotoStatement(L137-L179)把每个 subject 按其类型映射为 in-toto 的Subject{Name, Digest}(通过result.ToDigestMap把 digest 列表转为{algorithm: value}映射),并生成以intoto.StatementInTotoV1为类型的 statement 头。证言内容读取受maxAttestationBytes = 80 << 20(80 MiB)上限保护(make.go),读取逻辑(make.go)用io.LimitedReader限制大小。
这段代码与 Rekor 侧正好形成闭环:
- BuildKit 侧:把构建产物包装成 in-toto Statement(其 subject digest 结构就是 intoto/v0.0.2/intoto_v0_0_2_schema.json 中
payloadHash/hash所覆盖的 DSSE 信封内容); - Rekor 侧:
intoto类型按 Schema 解析这些信封,提取签名、计算哈希、生成IndexKeys供检索——两者通过同一套 in-toto/DSSE 数据模型对齐。
这也解释了为何本仓库只需 vendordsse、hashedrekord、intoto三个类型:它们是"签名 + 哈希 + 证言"这条最核心证据链的载体,与 BuildKit 的镜像签名与 attestation 场景直接相关,而 Alpine/RPM/Helm 等包管理器类型对构建工具链而言并非必需。
七、如何扩展新类型
README 指出,新增类型支持请参阅 Rekor 官方文档(pluggable-types 章节)。从本文已分析的源码模式出发,可以梳理出完整的扩展路径:
- 新建类型包:在
pkg/types/<kind>/下创建包,定义KIND常量; - 定义 Schema:编写
*_schema.json(含版本子目录,如v0.0.1/),用 JSON Schema 描述条目结构,并区分 writeOnly(提交字段)与 readOnly(返回字段); - 实现 TypeImpl:定义嵌入
types.RekorType的基类,在init()中调用types.TypeMap.Store(KIND, New)注册;New()返回携带VersionMap的类型实例; - 注册版本实现:在
v0.0.1/entry.go等版本子包中实现EntryImpl(含APIVersion、Canonicalize、IndexKeys、Unmarshal、CreateFromArtifactProperties、Verifiers、ArtifactHash、Insertable),并在init()中调用VersionMap.SetEntryFactory(version, NewEntry); - 接入服务端:通过
SetAllowedKindsForSubmission将新 kind 加入插入白名单。
其中intoto类型的ProposedIntotoEntryIterator模式(intoto.go)提供了一个多版本并存的参考实现:在未指定版本时生成一个 proposed entry 迭代器,让上层逐个版本尝试,提升提交成功率。
八、总结
Rekor 的可插拔类型机制,本质上是一套"kind 注册表 + semver 版本路由 + JSON Schema 描述 + 版本化 EntryImpl 实现"的四层插件体系:
TypeMap(sync.Map)在初始化期完成 kind → 类型构造器的注册,写少读多的访问模式选型精准;SemVerEntryFactoryMap用语义化版本区间做版本路由,让"支持版本"的表达能力远超普通字符串 map;- 每个类型通过 writeOnly/readOnly 字段分离"提交载荷"与"返回视图",并通过 RFC 8785 规范化保证透明日志的字节级确定性;
- 插入路径的 kind 白名单与版本支持校验,配合只作用于写入的
allowedKindsForSubmission,兼顾了服务端管控与历史条目可读性。
对本仓库(BuildKit)而言,vendor 的dsse、hashedrekord、intoto三个类型与 exporter/attestation 的证言生成逻辑共同构成了完整的供应链证据闭环:BuildKit 产出 in-toto Statement,Rekor 侧以 in-toto/DSSE 类型将其解析、索引并固化为透明日志条目,从而让镜像的 provenance 与 SBOM 可被独立验证与审计。
【免费下载链接】buildkitconcurrent, cache-efficient, and Dockerfile-agnostic builder toolkit项目地址: https://gitcode.com/GitHub_Trending/bu/buildkit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考