Skip to content

Commit f9ea5de

Browse files
committed
feat: add SLSA provenance generation for package builds
Signed-off-by: egibs <[email protected]>
1 parent ece051e commit f9ea5de

File tree

7 files changed

+299
-15
lines changed

7 files changed

+299
-15
lines changed

go.mod

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ require (
1818
github.com/google/go-github/v54 v54.0.0
1919
github.com/google/licenseclassifier/v2 v2.0.0
2020
github.com/ijt/goparsify v0.0.0-20221203142333-3a5276334b8d
21+
github.com/in-toto/attestation v1.1.2
2122
github.com/invopop/jsonschema v0.13.0
2223
github.com/joho/godotenv v1.5.1
2324
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
@@ -44,7 +45,7 @@ require (
4445
golang.org/x/sys v0.33.0
4546
golang.org/x/term v0.32.0
4647
golang.org/x/text v0.25.0
47-
golang.org/x/time v0.11.0
48+
golang.org/x/time v0.12.0
4849
gopkg.in/ini.v1 v1.67.0
4950
gopkg.in/yaml.v3 v3.0.1
5051
mvdan.cc/sh/v3 v3.11.0
@@ -53,6 +54,8 @@ require (
5354
)
5455

5556
require (
57+
github.com/fatih/color v1.18.0 // indirect
58+
github.com/google/martian/v3 v3.3.3 // indirect
5659
go.opencensus.io v0.24.0 // indirect
5760
golang.org/x/tools v0.33.0 // indirect
5861
k8s.io/klog/v2 v2.130.1 // indirect
@@ -167,8 +170,8 @@ require (
167170
google.golang.org/api v0.231.0 // indirect
168171
google.golang.org/genproto/googleapis/api v0.0.0-20250414145226-207652e42e2e // indirect
169172
google.golang.org/genproto/googleapis/rpc v0.0.0-20250425173222-7b384671a197 // indirect
170-
google.golang.org/grpc v1.72.0 // indirect
171-
google.golang.org/protobuf v1.36.6 // indirect
173+
google.golang.org/grpc v1.72.1 // indirect
174+
google.golang.org/protobuf v1.36.6
172175
gopkg.in/warnings.v0 v0.1.2 // indirect
173176
k8s.io/apimachinery v0.32.3 // indirect
174177
k8s.io/utils v0.0.0-20241210054802-24370beab758 // indirect

go.sum

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,8 +114,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF
114114
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
115115
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
116116
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
117-
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
118-
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
117+
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
118+
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
119119
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
120120
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
121121
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
@@ -189,8 +189,8 @@ github.com/google/go-replayers/httpreplay v1.2.0 h1:VM1wEyyjaoU53BwrOnaf9VhAyQQE
189189
github.com/google/go-replayers/httpreplay v1.2.0/go.mod h1:WahEFFZZ7a1P4VM1qEeHy+tME4bwyqPcwWbNlUI1Mcg=
190190
github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA=
191191
github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM=
192-
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
193-
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
192+
github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=
193+
github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=
194194
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
195195
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
196196
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
@@ -216,6 +216,8 @@ github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISH
216216
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
217217
github.com/ijt/goparsify v0.0.0-20221203142333-3a5276334b8d h1:LFOmpWrSbtolg0YqYC9hQjj5WSLtRGb6aZ3JAugLfgg=
218218
github.com/ijt/goparsify v0.0.0-20221203142333-3a5276334b8d/go.mod h1:112TOyA+aruNSUBlyBWlKBdLVYTdhjiO2CKD0j/URSU=
219+
github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E=
220+
github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM=
219221
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
220222
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
221223
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
@@ -502,8 +504,8 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
502504
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
503505
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
504506
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
505-
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
506-
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
507+
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
508+
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
507509
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
508510
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
509511
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -541,8 +543,8 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ
541543
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
542544
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
543545
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
544-
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
545-
google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
546+
google.golang.org/grpc v1.72.1 h1:HR03wO6eyZ7lknl75XlxABNVLLFc2PAb6mHlYh756mA=
547+
google.golang.org/grpc v1.72.1/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
546548
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
547549
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
548550
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=

pkg/build/build.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ import (
3737
"chainguard.dev/apko/pkg/apk/apk"
3838
apkofs "chainguard.dev/apko/pkg/apk/fs"
3939
apko_build "chainguard.dev/apko/pkg/build"
40-
"chainguard.dev/apko/pkg/tarfs"
4140
apko_types "chainguard.dev/apko/pkg/build/types"
4241
"chainguard.dev/apko/pkg/options"
4342
"chainguard.dev/apko/pkg/sbom/generator/spdx"
43+
"chainguard.dev/apko/pkg/tarfs"
4444
"github.com/chainguard-dev/clog"
4545
purl "github.com/package-url/packageurl-go"
4646
"github.com/yookoala/realpath"
@@ -103,7 +103,7 @@ type Build struct {
103103
WorkspaceDir string
104104
WorkspaceDirFS apkofs.FullFS
105105
WorkspaceIgnore string
106-
GuestFS apkofs.FullFS
106+
GuestFS apkofs.FullFS
107107
// Ordered directories where to find 'uses' pipelines.
108108
PipelineDirs []string
109109
SourceDir string
@@ -148,6 +148,9 @@ type Build struct {
148148
// visibility into our packages' (including subpackages') composition. This is
149149
// how we get "build-time" SBOMs!
150150
SBOMGroup *SBOMGroup
151+
152+
Start time.Time
153+
End time.Time
151154
}
152155

153156
func New(ctx context.Context, opts ...Option) (*Build, error) {
@@ -158,6 +161,7 @@ func New(ctx context.Context, opts ...Option) (*Build, error) {
158161
CacheDir: "./melange-cache/",
159162
Arch: apko_types.ParseArchitecture(runtime.GOARCH),
160163
GuestFS: tarfs.New(),
164+
Start: time.Now(),
161165
}
162166

163167
for _, opt := range opts {

pkg/build/package.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,12 @@ import (
2929
"slices"
3030
"strings"
3131
"text/template"
32+
"time"
3233

3334
apkofs "chainguard.dev/apko/pkg/apk/fs"
3435
apko_types "chainguard.dev/apko/pkg/build/types"
3536

37+
"github.com/charmbracelet/log"
3638
"github.com/klauspost/compress/gzip"
3739
"github.com/klauspost/pgzip"
3840

@@ -80,6 +82,8 @@ type PackageBuild struct {
8082
Description string
8183
URL string
8284
Commit string
85+
Start time.Time
86+
End time.Time
8387
}
8488

8589
func pkgFromSub(sub *config.Subpackage) *config.Package {
@@ -95,6 +99,7 @@ func pkgFromSub(sub *config.Subpackage) *config.Package {
9599
}
96100

97101
func (b *Build) Emit(ctx context.Context, pkg *config.Package) error {
102+
b.End = time.Now()
98103
pc := PackageBuild{
99104
Build: b,
100105
Origin: &b.Configuration.Package,
@@ -108,6 +113,8 @@ func (b *Build) Emit(ctx context.Context, pkg *config.Package) error {
108113
Description: pkg.Description,
109114
URL: pkg.URL,
110115
Commit: pkg.Commit,
116+
Start: b.Start,
117+
End: b.End,
111118
}
112119

113120
if !b.StripOriginName {
@@ -216,6 +223,15 @@ func (pc *PackageBuild) generateControlSection(ctx context.Context) ([]byte, err
216223
return nil, fmt.Errorf("unable to build control FS: %w", err)
217224
}
218225

226+
slsaData, err := pc.generateSLSA()
227+
if err != nil {
228+
return nil, fmt.Errorf("unable to generate SLSA provenance: %s", err)
229+
}
230+
231+
if err := fsys.WriteFile(".PROVENANCE", slsaData, 0644); err != nil {
232+
return nil, fmt.Errorf("failed to write SLSA provenance: %w", err)
233+
}
234+
219235
var melangeBuf bytes.Buffer
220236
enc := yaml.NewEncoder(&melangeBuf)
221237
enc.SetIndent(2) // To align with `yam` a little better.
@@ -394,7 +410,6 @@ func (pc *PackageBuild) calculateInstalledSize(fsys apkofs.FullFS) error {
394410
}
395411

396412
func (pc *PackageBuild) emitDataSection(ctx context.Context, fsys apkofs.FullFS, userinfofs apkofs.FullFS, remapUIDs map[int]int, remapGIDs map[int]int, w io.WriteSeeker) error {
397-
log := clog.FromContext(ctx)
398413
tarctx, err := tarball.NewContext(
399414
tarball.WithSourceDateEpoch(pc.Build.SourceDateEpoch),
400415
tarball.WithRemapUIDs(remapUIDs),
@@ -511,6 +526,8 @@ func (pc *PackageBuild) EmitPackage(ctx context.Context) error {
511526
remapUIDs[int(buildUser.UID)] = 0
512527
remapGIDs[int(buildGroup.GID)] = 0
513528

529+
log.Infof(" data.tar.gz digest: %s", pc.DataHash)
530+
514531
if err := pc.emitDataSection(ctx, fsys, userinfofs, remapUIDs, remapGIDs, dataTarGz); err != nil {
515532
return err
516533
}

pkg/build/slsa.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright 2025 Chainguard, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package build
16+
17+
import (
18+
"encoding/json"
19+
"fmt"
20+
21+
provenancev1 "github.com/in-toto/attestation/go/predicates/provenance/v1"
22+
intoto "github.com/in-toto/attestation/go/v1"
23+
"google.golang.org/protobuf/encoding/protojson"
24+
"google.golang.org/protobuf/types/known/structpb"
25+
"google.golang.org/protobuf/types/known/timestamppb"
26+
"sigs.k8s.io/release-utils/version"
27+
)
28+
29+
const (
30+
intotoStatementType = "https://in-toto.io/Statement/v1"
31+
slsaProvenanceStatementType = "https://slsa.dev/provenance/v1"
32+
melangeBuilder = "https://chainguard.dev/prod/builders/melange/v1"
33+
melangeBuildType = "https://chainguard.dev/buildtypes/melange/v1"
34+
)
35+
36+
func (pc *PackageBuild) generateSLSA() ([]byte, error) {
37+
slsaBuilder := &provenancev1.Builder{
38+
Id: melangeBuilder,
39+
Version: map[string]string{
40+
"melange": version.GetVersionInfo().GitVersion,
41+
},
42+
}
43+
44+
cfg, err := structToMap(pc.Build.Configuration)
45+
if err != nil {
46+
return nil, fmt.Errorf("converting contents to generic map: %w", err)
47+
}
48+
externalParameters, err := structpb.NewStruct(map[string]any{
49+
"package-configuration": cfg,
50+
})
51+
52+
if err != nil {
53+
return nil, err
54+
}
55+
56+
predicate := &provenancev1.Provenance{
57+
BuildDefinition: &provenancev1.BuildDefinition{
58+
BuildType: melangeBuildType,
59+
ExternalParameters: externalParameters,
60+
},
61+
RunDetails: &provenancev1.RunDetails{
62+
Builder: slsaBuilder,
63+
Metadata: &provenancev1.BuildMetadata{
64+
StartedOn: timestamppb.New(pc.Start),
65+
FinishedOn: timestamppb.New(pc.End),
66+
},
67+
},
68+
}
69+
70+
pbJson, err := protojson.Marshal(predicate)
71+
if err != nil {
72+
return nil, err
73+
}
74+
75+
var pbMap map[string]any
76+
if err := json.Unmarshal(pbJson, &pbMap); err != nil {
77+
return nil, err
78+
}
79+
80+
pbStruct, err := structpb.NewStruct(pbMap)
81+
if err != nil {
82+
return nil, err
83+
}
84+
85+
subject := []*intoto.ResourceDescriptor{
86+
{
87+
Name: pc.Identity() + ".apk",
88+
Digest: map[string]string{
89+
"sha256": pc.DataHash,
90+
},
91+
},
92+
}
93+
94+
statement := &intoto.Statement{
95+
Type: intotoStatementType,
96+
PredicateType: slsaProvenanceStatementType,
97+
Subject: subject,
98+
Predicate: pbStruct,
99+
}
100+
101+
slsa, err := json.MarshalIndent(statement, "", " ")
102+
if err != nil {
103+
return nil, fmt.Errorf("marshaling provenance: %w", err)
104+
}
105+
106+
return slsa, nil
107+
}
108+
109+
// structToMap converts a struct to a map[string]any. It assumes that the struct
110+
// can be marshaled to JSON and unmarshaled back to a map.
111+
func structToMap(val any) (map[string]any, error) {
112+
contents, err := json.Marshal(val)
113+
if err != nil {
114+
return nil, fmt.Errorf("marshaling struct: %w", err)
115+
}
116+
var genericValue map[string]any
117+
if err := json.Unmarshal(contents, &genericValue); err != nil {
118+
return nil, fmt.Errorf("unmarshaling struct: %w", err)
119+
}
120+
return genericValue, nil
121+
}

0 commit comments

Comments
 (0)