Merge branch 'main' into fix-plugin-oci-image-index

pull/32548/head
Robert Sirchia 2 weeks ago committed by GitHub
commit 80fe11737b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -48,7 +48,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # pinv4.37.8
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@ -59,7 +59,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6
uses: github/codeql-action/autobuild@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # pinv4.37.8
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@ -73,4 +73,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # pinv4.37.6
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # pinv4.37.8

@ -64,6 +64,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard (optional).
# Commenting out will disable upload of results to your repo's Code Scanning dashboard
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
sarif_file: results.sarif

@ -32,21 +32,21 @@ require (
github.com/santhosh-tekuri/jsonschema/v6 v6.0.3
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/stretchr/testify v1.12.1
github.com/tetratelabs/wazero v1.12.0
go.yaml.in/yaml/v3 v3.0.5
golang.org/x/crypto v0.54.0
golang.org/x/crypto v0.55.0
golang.org/x/term v0.45.0
golang.org/x/text v0.40.0
golang.org/x/text v0.41.0
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.36.3
k8s.io/apiextensions-apiserver v0.36.3
k8s.io/apimachinery v0.36.3
k8s.io/apiserver v0.36.3
k8s.io/cli-runtime v0.36.3
k8s.io/client-go v0.36.3
k8s.io/api v0.36.4
k8s.io/apiextensions-apiserver v0.36.4
k8s.io/apimachinery v0.36.4
k8s.io/apiserver v0.36.4
k8s.io/cli-runtime v0.36.4
k8s.io/client-go v0.36.4
k8s.io/klog/v2 v2.140.0
k8s.io/kubectl v0.36.3
k8s.io/kubectl v0.36.4
oras.land/oras-go/v2 v2.6.2
sigs.k8s.io/controller-runtime v0.24.1
sigs.k8s.io/kustomize/kyaml v0.21.1
@ -158,13 +158,13 @@ require (
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/mod v0.38.0 // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
golang.org/x/sync v0.22.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.47.0 // indirect
golang.org/x/tools v0.48.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
google.golang.org/grpc v1.82.1 // indirect
@ -172,7 +172,7 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
k8s.io/component-base v0.36.3 // indirect
k8s.io/component-base v0.36.4 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect

@ -301,14 +301,14 @@ github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
@ -387,14 +387,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -405,8 +405,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -453,8 +453,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@ -463,8 +463,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk=
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE=
golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
@ -490,26 +490,26 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.36.3 h1:NxB+05W2UGqXWFXcLO0RB5cnqnUPP5v5sVlaOH0Iz4w=
k8s.io/api v0.36.3/go.mod h1:JzLQKqRHC5+I8RVj/lS3lCg0mg6nWI9Fo/Sk3ElxHzg=
k8s.io/apiextensions-apiserver v0.36.3 h1:dPmOAPhwTtqb1bTxbFPsy18KHPhktQeO3WUPXunZIB0=
k8s.io/apiextensions-apiserver v0.36.3/go.mod h1:KTXFqgXiuw2pRoL+Wpmttqc+up9Xt/GohadPWeLLOa4=
k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
k8s.io/apimachinery v0.36.3/go.mod h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
k8s.io/apiserver v0.36.3 h1:MGSg2SkdfuytiDEcRylT5mQFmmSsbx90XFUO67Y4bsQ=
k8s.io/apiserver v0.36.3/go.mod h1:fVH7zv9EUNUA7Fl7LtDKh8aB9W7u1VQPSGtWV5SjUxg=
k8s.io/cli-runtime v0.36.3 h1:g+eJ+M1sYpnNYp/q5fzaw2KejIL0Q7DH+xFl6YVoL4U=
k8s.io/cli-runtime v0.36.3/go.mod h1:hZpAqK8nSFXvvLaVCbzUPVp8e9TRLSTCfpNzMt7s3tE=
k8s.io/client-go v0.36.3 h1:M4JdVzXxYcZk4fGpfDdYnxSwhLKWCFoQsHW6t+z8Hfg=
k8s.io/client-go v0.36.3/go.mod h1:gcPwr0c87vjjG6HB6pWEqOeuYVoXSsREjzux2j6GF30=
k8s.io/component-base v0.36.3 h1:vc/UFvPCkW0irPz84LAodAL1j3f4xktPM6dDJIEheAY=
k8s.io/component-base v0.36.3/go.mod h1:hZbNFG+gCMl9EbykDGEu73feKP9/Cq6JsV4pTo9GTO8=
k8s.io/api v0.36.4 h1:RxrvqCL6vgH5/+UnTeu1IIFqYmGfy0hnyrod1rn35Oo=
k8s.io/api v0.36.4/go.mod h1:S2B3orCFBDhrgyWbLeuKcT2QdHIpQesBkCYSlWtwUOw=
k8s.io/apiextensions-apiserver v0.36.4 h1:SfvCVt+4CqKWvzuVytYDT5g9hyb9MztoiYELIkPVrFc=
k8s.io/apiextensions-apiserver v0.36.4/go.mod h1:JT9V2Ju7ys1FY4zbSpmX9XOvKB3/BwsODc4hFQEa+Xo=
k8s.io/apimachinery v0.36.4 h1:PT2UzkupGuAx/+xT5XjiMJ1WGpY3fn9/hdAvjweRet4=
k8s.io/apimachinery v0.36.4/go.mod h1:p2I2dipt7JHG+quVwQ1d02d28O4GdDi77RByQ13MTpk=
k8s.io/apiserver v0.36.4 h1:AtKjaf2eUiX5G6TfF2IOlhuUuvMsHh49Ivr1+4fZ2gA=
k8s.io/apiserver v0.36.4/go.mod h1:RyiGghXP67hb0Ll+7iLJ6GGv2JpEzCn7ljbiA+L3cJ0=
k8s.io/cli-runtime v0.36.4 h1:OHvManCwP1k9GiC5tXRFxHhzZIQQFCsrHlt7OspKo3w=
k8s.io/cli-runtime v0.36.4/go.mod h1:qQSj2FJgQos6GHpS/ge7wTdQMZm9XFWlesWgV6h7qZY=
k8s.io/client-go v0.36.4 h1:MDvfDNvMSt0Br94SK8neviVlwL9qifw9B26hJCpD1K0=
k8s.io/client-go v0.36.4/go.mod h1:pNK4WKELbwlEDvtbE8l22lEZL5THYF61H5EealokZmA=
k8s.io/component-base v0.36.4 h1:tz75yC2xgq3kd7vPdBtR8do5iMx0OHf6Zd1kuaxDB84=
k8s.io/component-base v0.36.4/go.mod h1:DCwb306U8ou89NNAp45Csuy8ok+1rp1ELDVPhzN5AWc=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/kubectl v0.36.3 h1:TesKp+XYQEjPYoFvuobcVnuvira2+/xAVlq//+kksaI=
k8s.io/kubectl v0.36.3/go.mod h1:W+NEb1CzBGmoaI1Nrpn2ETo9omNBl0AsyxnnMT40N6E=
k8s.io/kubectl v0.36.4 h1:xZd9g1bFBd7hpb1oKjK8lT9jRL18dtgr4DAPQG1Oksk=
k8s.io/kubectl v0.36.4/go.mod h1:STWlr78cdEa1hHpr55wpcboaqchvfDueKRNDa1zOd1w=
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc=
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
oras.land/oras-go/v2 v2.6.2 h1:N04RXngAp1LJKTG6ifz3xHPipasEkWr+hFmInja5YKo=

@ -183,8 +183,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]any, error) {
// Read fully first. YAMLReader/LineReader can drop a final unterminated
// line when its length is an exact multiple of bufio.Reader's default
// buffer (4096). Appending a trailing newline avoids that case.
// See https://github.com/helm/helm/issues/32506
b, err := io.ReadAll(data)
if err != nil {
return nil, err
}
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
values := map[string]any{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b)))
for {
currentMap := map[string]any{}
raw, err := reader.Read()

@ -21,6 +21,7 @@ import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"os"
@ -419,6 +420,33 @@ foo:
}
}
func TestLoadValuesEOFBoundary(t *testing.T) {
// Reproduces #32506: a single logical line whose length is a multiple of
// bufio's default buffer (4096) and has no trailing newline used to be
// dropped entirely by YAMLReader, yielding empty values.
// Also cover 8192 (2x buffer) so we do not only hit the single-buffer case.
for _, size := range []int{4096, 8192} {
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
prefix := []byte(`{"foo":"`)
suffix := []byte(`"}`)
pad := size - len(prefix) - len(suffix)
data := make([]byte, 0, size)
data = append(data, prefix...)
data = append(data, bytes.Repeat([]byte("x"), pad)...)
data = append(data, suffix...)
if len(data) != size {
t.Fatalf("test setup: want data length %d, got %d", size, len(data))
}
values, err := LoadValues(bytes.NewReader(data))
require.NoError(t, err)
assert.Equal(t, map[string]any{
"foo": string(bytes.Repeat([]byte("x"), pad)),
}, values)
})
}
}
func TestMergeValuesV3(t *testing.T) {
nestedMap := map[string]any{
"foo": "bar",

@ -16,7 +16,7 @@ limitations under the License.
/*
This file contains a "registry" of supported plugin types.
It enables "dyanmic" operations on the go type associated with a given plugin type (see: `helm.sh/helm/v4/internal/plugin/schema` package)
It enables "dynamic" operations on the go type associated with a given plugin type (see: `helm.sh/helm/v4/internal/plugin/schema` package)
Examples:

@ -17,9 +17,14 @@ limitations under the License.
package action
import (
"archive/tar"
"compress/gzip"
"errors"
"io"
"os"
"path"
"testing"
"time"
"github.com/Masterminds/semver/v3"
"github.com/stretchr/testify/assert"
@ -144,3 +149,60 @@ func TestRun(t *testing.T) {
require.Equal(t, "empty-0.1.0.tgz", filename)
require.NoError(t, os.Remove(filename))
}
// TestRunWithSourceDateEpochStampsLockGenerated verifies that packaging a chart
// that has a Chart.lock stamps both the tar entry modtime and the marshaled
// generated: field in Chart.lock to the given epoch.
//
// This guards against the normalization regression where a caller supplying a
// local-timezone or sub-second time.Time would produce a non-reproducible
// generated: value even when the same SOURCE_DATE_EPOCH is used on different
// machines.
func TestRunWithSourceDateEpochStampsLockGenerated(t *testing.T) {
// Use a non-local, non-UTC timezone and sub-second precision to confirm
// normalization: without UTC().Truncate(time.Second) the generated: field
// would contain a timezone offset or fractional seconds.
loc := time.FixedZone("UTC+3", 3*60*60)
rawEpoch := time.Unix(1700000000, 123456789).In(loc)
epoch := rawEpoch.UTC().Truncate(time.Second)
client := NewPackage()
client.SourceDateEpoch = &rawEpoch
filename, err := client.Run("testdata/charts/chart-with-lock", nil)
require.NoError(t, err)
t.Cleanup(func() { os.Remove(filename) })
f, err := os.Open(filename)
require.NoError(t, err)
defer f.Close()
gr, err := gzip.NewReader(f)
require.NoError(t, err)
defer gr.Close()
const wantPath = "chart-with-lock/Chart.lock"
found := false
tr := tar.NewReader(gr)
for {
hdr, err := tr.Next()
if errors.Is(err, io.EOF) {
break
}
require.NoError(t, err)
if hdr.Name != wantPath {
continue
}
found = true
require.True(t, epoch.Equal(hdr.ModTime),
"Chart.lock tar modtime: got %v, want %v", hdr.ModTime, epoch)
raw, err := io.ReadAll(tr)
require.NoError(t, err)
wantGenerated := epoch.Format(time.RFC3339)
require.Contains(t, string(raw), wantGenerated,
"Chart.lock generated: field should contain normalized UTC timestamp")
break
}
require.True(t, found, "expected archive to contain %q entry", wantPath)
}

@ -0,0 +1,3 @@
dependencies: []
digest: sha256:0000000000000000000000000000000000000000000000000000000000000
generated: "2016-01-01T00:00:00Z"

@ -0,0 +1,4 @@
apiVersion: v2
name: chart-with-lock
version: 0.1.0
description: Test chart with a Chart.lock, used to verify SourceDateEpoch stamping

@ -177,34 +177,41 @@ func (ch *Chart) CRDObjects() []CRD {
return crds
}
// StampModTimes sets timestamps on the chart (and dependencies) to epoch.
// This is used for reproducible builds via SOURCE_DATE_EPOCH.
func (ch *Chart) StampModTimes(epoch time.Time) {
ch.ModTime = epoch
// StampModTimes sets timestamps on the chart (and dependencies) to t,
// normalized to UTC and truncated to whole seconds.
//
// Normalization is required because Chart.lock's generated: field is written
// by yaml.Marshal from Lock.Generated. Without UTC/truncate, a caller
// supplying a local-zone or sub-second time.Time produces a generated: value
// with a timezone offset or fractional seconds, making the lock file content
// non-reproducible across machines even when the same SOURCE_DATE_EPOCH is used.
func (ch *Chart) StampModTimes(t time.Time) {
t = t.UTC().Truncate(time.Second)
ch.ModTime = t
if len(ch.Schema) > 0 {
ch.SchemaModTime = epoch
ch.SchemaModTime = t
}
if ch.Lock != nil {
ch.Lock.Generated = epoch
ch.Lock.Generated = t
}
for _, f := range ch.Raw {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, f := range ch.Templates {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, f := range ch.Files {
if f != nil {
f.ModTime = epoch
f.ModTime = t
}
}
for _, dep := range ch.Dependencies() {
dep.StampModTimes(epoch)
dep.StampModTimes(t)
}
}

@ -210,8 +210,20 @@ func LoadFiles(files []*archive.BufferedFile) (*chart.Chart, error) {
// The reader is expected to contain one or more YAML documents, the values of which are merged.
// And the values can be either a chart's default values or user-supplied values.
func LoadValues(data io.Reader) (map[string]any, error) {
// Read fully first. YAMLReader/LineReader can drop a final unterminated
// line when its length is an exact multiple of bufio.Reader's default
// buffer (4096). Appending a trailing newline avoids that case.
// See https://github.com/helm/helm/issues/32506
b, err := io.ReadAll(data)
if err != nil {
return nil, err
}
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
values := map[string]any{}
reader := utilyaml.NewYAMLReader(bufio.NewReader(data))
reader := utilyaml.NewYAMLReader(bufio.NewReader(bytes.NewReader(b)))
for {
currentMap := map[string]any{}
raw, err := reader.Read()

@ -21,6 +21,7 @@ import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"os"
@ -463,6 +464,33 @@ foo:
}
}
func TestLoadValuesEOFBoundary(t *testing.T) {
// Reproduces #32506: a single logical line whose length is a multiple of
// bufio's default buffer (4096) and has no trailing newline used to be
// dropped entirely by YAMLReader, yielding empty values.
// Also cover 8192 (2x buffer) so we do not only hit the single-buffer case.
for _, size := range []int{4096, 8192} {
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
prefix := []byte(`{"foo":"`)
suffix := []byte(`"}`)
pad := size - len(prefix) - len(suffix)
data := make([]byte, 0, size)
data = append(data, prefix...)
data = append(data, bytes.Repeat([]byte("x"), pad)...)
data = append(data, suffix...)
if len(data) != size {
t.Fatalf("test setup: want data length %d, got %d", size, len(data))
}
values, err := LoadValues(bytes.NewReader(data))
require.NoError(t, err)
assert.Equal(t, map[string]any{
"foo": string(bytes.Repeat([]byte("x"), pad)),
}, values)
})
}
}
func TestMergeValuesV2(t *testing.T) {
nestedMap := map[string]any{
"foo": "bar",

@ -19,6 +19,7 @@ import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
@ -97,9 +98,25 @@ func newDependencyBuildCmd(out io.Writer) *cobra.Command {
}
// defaultKeyring returns the expanded path to the default keyring.
//
// The legacy pubring.gpg file is preferred and treated as absent only when
// stat fails with "not exist" — any other stat error (e.g. permissions)
// keeps the legacy path so the real error surfaces when the file is opened.
// If the legacy file is absent, the file-backed pubring.kbx path takes over
// under the same rule. When neither exists, the legacy path is returned so
// error messages keep pointing at the traditional default.
func defaultKeyring() string {
gnupgHome := filepath.Join(homedir.HomeDir(), ".gnupg")
if v, ok := os.LookupEnv("GNUPGHOME"); ok {
return filepath.Join(v, "pubring.gpg")
gnupgHome = v
}
return filepath.Join(homedir.HomeDir(), ".gnupg", "pubring.gpg")
legacy := filepath.Join(gnupgHome, "pubring.gpg")
if _, err := os.Stat(legacy); !errors.Is(err, fs.ErrNotExist) {
return legacy
}
keybox := filepath.Join(gnupgHome, "pubring.kbx")
if _, err := os.Stat(keybox); !errors.Is(err, fs.ErrNotExist) {
return keybox
}
return legacy
}

@ -19,6 +19,7 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
@ -140,3 +141,68 @@ func TestDependencyBuildCmdWithHelmV2Hash(t *testing.T) {
t.Fatal(err)
}
}
func TestDefaultKeyring(t *testing.T) {
touch := func(t *testing.T, path string) {
t.Helper()
require.NoError(t, os.WriteFile(path, []byte("test"), 0o644))
}
tests := []struct {
name string
files []string
want string
}{
{"legacy keyring only", []string{"pubring.gpg"}, "pubring.gpg"},
{"keybox only", []string{"pubring.kbx"}, "pubring.kbx"},
{"legacy keyring preferred over keybox", []string{"pubring.gpg", "pubring.kbx"}, "pubring.gpg"},
{"neither present falls back to legacy path", nil, "pubring.gpg"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
dir := t.TempDir()
t.Setenv("GNUPGHOME", dir)
for _, f := range tt.files {
touch(t, filepath.Join(dir, f))
}
assert.Equal(t, filepath.Join(dir, tt.want), defaultKeyring())
})
}
t.Run("stat error other than not-exist keeps the legacy path", func(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("directory permissions are not enforced on Windows")
}
if os.Geteuid() == 0 {
t.Skip("root bypasses directory permissions")
}
parent := t.TempDir()
dir := filepath.Join(parent, ".gnupg")
require.NoError(t, os.MkdirAll(dir, 0o700))
touch(t, filepath.Join(dir, "pubring.kbx"))
t.Setenv("GNUPGHOME", dir)
// Make the directory unsearchable so stat on both keyrings fails
// with a permission error rather than "not exist".
require.NoError(t, os.Chmod(dir, 0o000))
t.Cleanup(func() { require.NoError(t, os.Chmod(dir, 0o700)) })
assert.Equal(t, filepath.Join(dir, "pubring.gpg"), defaultKeyring())
})
t.Run("no GNUPGHOME falls back to the home directory", func(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("GNUPGHOME", home) // register restoration before unsetting
require.NoError(t, os.Unsetenv("GNUPGHOME"))
gnupgDir := filepath.Join(home, ".gnupg")
require.NoError(t, os.MkdirAll(gnupgDir, 0o700))
assert.Equal(t, filepath.Join(gnupgDir, "pubring.gpg"), defaultKeyring())
touch(t, filepath.Join(gnupgDir, "pubring.kbx"))
assert.Equal(t, filepath.Join(gnupgDir, "pubring.kbx"), defaultKeyring())
})
}

@ -386,7 +386,7 @@ func (c *ReadyChecker) crdReady(crd apiextv1.CustomResourceDefinition) bool {
func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
// Verify the generation observed by the statefulSet controller matches the spec generation
if sts.Status.ObservedGeneration != sts.Generation {
slog.Debug("StatefulSet is not ready, observedGeneration doest not match spec generation", "namespace", sts.GetNamespace(), "name", sts.GetName(), "actualGeneration", sts.Status.ObservedGeneration, "expectedGeneration", sts.Generation)
slog.Debug("StatefulSet is not ready, observedGeneration does not match spec generation", "namespace", sts.GetNamespace(), "name", sts.GetName(), "actualGeneration", sts.Status.ObservedGeneration, "expectedGeneration", sts.Generation)
return false
}
@ -440,7 +440,7 @@ func (c *ReadyChecker) statefulSetReady(sts *appsv1.StatefulSet) bool {
func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationController) bool {
// Verify the generation observed by the replicationController controller matches the spec generation
if rc.Status.ObservedGeneration != rc.Generation {
slog.Debug("ReplicationController is not ready, observedGeneration doest not match spec generation", "namespace", rc.GetNamespace(), "name", rc.GetName(), "actualGeneration", rc.Status.ObservedGeneration, "expectedGeneration", rc.Generation)
slog.Debug("ReplicationController is not ready, observedGeneration does not match spec generation", "namespace", rc.GetNamespace(), "name", rc.GetName(), "actualGeneration", rc.Status.ObservedGeneration, "expectedGeneration", rc.Generation)
return false
}
return true
@ -449,7 +449,7 @@ func (c *ReadyChecker) replicationControllerReady(rc *corev1.ReplicationControll
func (c *ReadyChecker) replicaSetReady(rs *appsv1.ReplicaSet) bool {
// Verify the generation observed by the replicaSet controller matches the spec generation
if rs.Status.ObservedGeneration != rs.Generation {
slog.Debug("ReplicaSet is not ready, observedGeneration doest not match spec generation", "namespace", rs.GetNamespace(), "name", rs.GetName(), "actualGeneration", rs.Status.ObservedGeneration, "expectedGeneration", rs.Generation)
slog.Debug("ReplicaSet is not ready, observedGeneration does not match spec generation", "namespace", rs.GetNamespace(), "name", rs.GetName(), "actualGeneration", rs.Status.ObservedGeneration, "expectedGeneration", rs.Generation)
return false
}
return true

@ -0,0 +1,113 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provenance
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
)
// GnuPG 2.1+ can store file-backed public keys in a "keybox" (pubring.kbx),
// a container that interleaves OpenPGP keyblocks with GnuPG-specific
// metadata. It is not an OpenPGP packet stream, so it cannot be handed to
// openpgp.ReadKeyRing directly. A keybox is a sequence of blobs, each starting
// with:
//
// byte 0..3 u32 blob length, big endian, including this header
// byte 4 u8 blob type (0 empty, 1 header, 2 OpenPGP, 3 X.509)
// byte 5 u8 blob version
//
// The first blob is a header carrying the "KBXf" magic at offset 8. OpenPGP
// blobs (type 2) record where the raw keyblock lives inside the blob:
//
// byte 8..11 u32 keyblock offset, relative to the blob start
// byte 12..15 u32 keyblock length
//
// Reference: kbx/keybox-blob.c in the GnuPG source tree.
const (
kbxBlobTypeHeader = 1
kbxBlobTypeOpenPGP = 2
// kbxBlobFlagEphemeral marks a blob GnuPG considers not (yet) part of
// the keyring, e.g. written during an interrupted keyserver operation
// (KEYBOX_FLAG_BLOB_EPHEMERAL in kbx/keybox.h). GnuPG skips such blobs
// on every normal read (kbx/keybox-search.c), and so do we.
kbxBlobFlagEphemeral = 0x0002
// kbxMinBlobLen covers the length and type fields present in every blob.
kbxMinBlobLen = 5
// kbxOpenPGPHeaderLen is how much of an OpenPGP blob header must be
// present for the flags, keyblock offset and keyblock length fields to
// be readable.
kbxOpenPGPHeaderLen = 16
)
// isKeybox reports whether data looks like a GnuPG keybox (pubring.kbx)
// image, identified by the "KBXf" magic in the mandatory first header blob.
func isKeybox(data []byte) bool {
return len(data) >= 12 && data[4] == kbxBlobTypeHeader && string(data[8:12]) == "KBXf"
}
// isArmored reports whether data looks like an ASCII-armored keyring, as
// produced by `gpg --export --armor`.
func isArmored(data []byte) bool {
return bytes.HasPrefix(bytes.TrimSpace(data), []byte("-----BEGIN PGP"))
}
// keyboxPublicKeys extracts the OpenPGP keyblocks embedded in a keybox image
// and returns them concatenated, ready for openpgp.ReadKeyRing. Blobs of any
// other type (header, X.509, empty) are skipped, as are blobs flagged
// ephemeral, which GnuPG itself ignores when reading the keyring. Malformed
// input yields an error, never a panic.
func keyboxPublicKeys(data []byte) ([]byte, error) {
var keyblocks bytes.Buffer
for offset := 0; offset < len(data); {
rest := data[offset:]
if len(rest) < kbxMinBlobLen {
return nil, fmt.Errorf("truncated blob header at offset %d", offset)
}
blobLen := binary.BigEndian.Uint32(rest)
if blobLen < kbxMinBlobLen {
return nil, fmt.Errorf("invalid blob length %d at offset %d", blobLen, offset)
}
if uint64(blobLen) > uint64(len(rest)) {
return nil, fmt.Errorf("blob at offset %d has length %d exceeding the %d remaining bytes", offset, blobLen, len(rest))
}
blob := rest[:blobLen]
if blob[4] == kbxBlobTypeOpenPGP {
if len(blob) < kbxOpenPGPHeaderLen {
return nil, fmt.Errorf("OpenPGP blob at offset %d is too short", offset)
}
flags := binary.BigEndian.Uint16(blob[6:])
keyblockOffset := binary.BigEndian.Uint32(blob[8:])
keyblockLen := binary.BigEndian.Uint32(blob[12:])
if uint64(keyblockOffset)+uint64(keyblockLen) > uint64(len(blob)) {
return nil, fmt.Errorf("OpenPGP blob at offset %d has an out-of-range keyblock", offset)
}
if flags&kbxBlobFlagEphemeral == 0 {
keyblocks.Write(blob[keyblockOffset : keyblockOffset+keyblockLen])
}
}
offset += int(blobLen)
}
if keyblocks.Len() == 0 {
return nil, errors.New("keybox contains no OpenPGP keys")
}
return keyblocks.Bytes(), nil
}

@ -0,0 +1,188 @@
/*
Copyright The Helm Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package provenance
import (
"bytes"
"encoding/binary"
"os"
"testing"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
// testKeybox is a GnuPG keybox (pubring.kbx) containing the helm-test
// public key. Regenerate with testdata/regen-keyring-formats.sh.
testKeybox = "testdata/helm-test-key.kbx"
// testMixedKeybox is a keybox containing the RSA and Ed25519 test keys.
testMixedKeybox = "testdata/helm-mixed-keyring.kbx"
// testArmoredPubfile is the ASCII-armored export of the helm-test key.
testArmoredPubfile = "testdata/helm-test-key.asc"
// testMultiBlockArmored is two concatenated single-key armored exports
// (cat key1.asc key2.asc), covering the RSA and Ed25519 test keys.
testMultiBlockArmored = "testdata/helm-mixed-keyring.asc"
)
func TestIsKeybox(t *testing.T) {
tests := []struct {
name string
file string
want bool
}{
{"keybox", testKeybox, true},
{"mixed keybox", testMixedKeybox, true},
{"legacy binary keyring", testPubfile, false},
{"armored keyring", testArmoredPubfile, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := os.ReadFile(tt.file)
require.NoError(t, err)
assert.Equal(t, tt.want, isKeybox(data))
})
}
t.Run("degenerate inputs", func(t *testing.T) {
assert.False(t, isKeybox(nil))
assert.False(t, isKeybox([]byte{}))
assert.False(t, isKeybox([]byte("KBXf")))
assert.False(t, isKeybox([]byte("garbage that is longer than twelve bytes")))
})
}
func TestIsArmored(t *testing.T) {
tests := []struct {
name string
file string
want bool
}{
{"armored keyring", testArmoredPubfile, true},
{"legacy binary keyring", testPubfile, false},
{"keybox", testKeybox, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := os.ReadFile(tt.file)
require.NoError(t, err)
assert.Equal(t, tt.want, isArmored(data))
})
}
t.Run("degenerate inputs", func(t *testing.T) {
assert.False(t, isArmored(nil))
assert.False(t, isArmored([]byte("not a key")))
assert.True(t, isArmored([]byte("\n\t -----BEGIN PGP PUBLIC KEY BLOCK-----")))
})
}
func TestKeyboxPublicKeys(t *testing.T) {
data, err := os.ReadFile(testKeybox)
require.NoError(t, err)
keys, err := keyboxPublicKeys(data)
require.NoError(t, err)
ring, err := openpgp.ReadKeyRing(bytes.NewReader(keys))
require.NoError(t, err)
require.Len(t, ring, 1)
_, ok := ring[0].Identities[testKeyName]
assert.True(t, ok, "expected keybox to contain %q", testKeyName)
}
func TestKeyboxPublicKeysEphemeral(t *testing.T) {
// GnuPG flags in-progress key material as ephemeral (bit 0x0002 of the
// blob flags at blob offset 6) and hides it from every normal read; the
// parser must do the same.
setFlags := func(t *testing.T, data []byte, blobStart int, flags uint16) []byte {
t.Helper()
require.Equal(t, byte(kbxBlobTypeOpenPGP), data[blobStart+4])
mutated := bytes.Clone(data)
binary.BigEndian.PutUint16(mutated[blobStart+6:], flags)
return mutated
}
t.Run("all blobs ephemeral means no keys", func(t *testing.T) {
valid, err := os.ReadFile(testKeybox)
require.NoError(t, err)
_, err = keyboxPublicKeys(setFlags(t, valid, 32, kbxBlobFlagEphemeral))
assert.ErrorContains(t, err, "no OpenPGP keys")
})
t.Run("ephemeral blob is skipped, others kept", func(t *testing.T) {
valid, err := os.ReadFile(testMixedKeybox)
require.NoError(t, err)
// Flag only the first OpenPGP blob (the RSA helm-test key).
keys, err := keyboxPublicKeys(setFlags(t, valid, 32, kbxBlobFlagEphemeral))
require.NoError(t, err)
ring, err := openpgp.ReadKeyRing(bytes.NewReader(keys))
require.NoError(t, err)
require.Len(t, ring, 1)
_, ok := ring[0].Identities[testKeyName]
assert.False(t, ok, "expected the ephemeral-flagged %q blob to be skipped", testKeyName)
})
}
func TestKeyboxPublicKeysMalformed(t *testing.T) {
valid, err := os.ReadFile(testKeybox)
require.NoError(t, err)
// The mutations below rely on the fixture layout: a 32-byte header blob
// followed by an OpenPGP blob.
const blobStart = 32
require.Greater(t, len(valid), blobStart+16)
require.Equal(t, byte(kbxBlobTypeOpenPGP), valid[blobStart+4])
mutate := func(offset int, value uint32) []byte {
data := bytes.Clone(valid)
binary.BigEndian.PutUint32(data[offset:], value)
return data
}
tests := []struct {
name string
data []byte
}{
{"header only, no keys", valid[:blobStart]},
{"truncated inside blob header", valid[:blobStart+2]},
{"truncated inside blob body", valid[:blobStart+16]},
{"zero blob length", mutate(blobStart, 0)},
{"blob length below minimum", mutate(blobStart, 4)},
{"blob length past end of data", mutate(blobStart, uint32(len(valid))+1)},
{"keyblock offset out of range", mutate(blobStart+8, uint32(len(valid)))},
{"keyblock length out of range", mutate(blobStart+12, uint32(len(valid)))},
{"keyblock offset overflow", mutate(blobStart+8, ^uint32(0))},
{"keyblock length overflow", mutate(blobStart+12, ^uint32(0))},
{"openpgp blob shorter than its header", append(bytes.Clone(valid[:blobStart]), 0, 0, 0, 8, kbxBlobTypeOpenPGP, 1, 0, 0)},
{"empty input", nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := keyboxPublicKeys(tt.data)
assert.Error(t, err)
})
}
}

@ -26,6 +26,7 @@ import (
"strings"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/clearsign"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"sigs.k8s.io/yaml"
@ -357,13 +358,64 @@ func loadKey(keypath string) (*openpgp.Entity, error) {
return openpgp.ReadEntity(pr)
}
// loadKeyRing loads a keyring from ringpath, auto-detecting its storage
// format. Three formats are supported:
//
// - the legacy binary OpenPGP packet stream (GnuPG's pubring.gpg)
// - a GnuPG file-backed keybox (pubring.kbx), introduced in GnuPG 2.1
// - an ASCII-armored keyring, as produced by `gpg --export --armor`
func loadKeyRing(ringpath string) (openpgp.EntityList, error) {
f, err := os.Open(ringpath)
data, err := os.ReadFile(ringpath)
if err != nil {
return nil, err
}
defer f.Close()
return openpgp.ReadKeyRing(f)
switch {
case isKeybox(data):
keys, err := keyboxPublicKeys(data)
if err != nil {
return nil, fmt.Errorf("reading keybox %s: %w", ringpath, err)
}
return openpgp.ReadKeyRing(bytes.NewReader(keys))
case isArmored(data):
ring, err := loadArmoredKeyRing(data)
if err != nil {
return nil, fmt.Errorf("reading armored keyring %s: %w", ringpath, err)
}
return ring, nil
default:
return openpgp.ReadKeyRing(bytes.NewReader(data))
}
}
// loadArmoredKeyRing reads every armored block in data and merges the keys
// into a single keyring. Unlike openpgp.ReadArmoredKeyRing, which silently
// ignores everything after the first block, this handles keyrings assembled
// by concatenating exports (cat key1.asc key2.asc > keyring.asc), the same
// way GnuPG imports them.
func loadArmoredKeyRing(data []byte) (openpgp.EntityList, error) {
var ring openpgp.EntityList
r := bytes.NewReader(data)
for {
block, err := armor.Decode(r)
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
if block.Type != openpgp.PublicKeyType && block.Type != openpgp.PrivateKeyType {
return nil, fmt.Errorf("expected a public or private key block, got %q", block.Type)
}
entities, err := openpgp.ReadKeyRing(block.Body)
if err != nil {
return nil, err
}
ring = append(ring, entities...)
}
if len(ring) == 0 {
return nil, errors.New("no keys found")
}
return ring, nil
}
// DigestFile calculates a SHA256 hash (like Docker) for a given file.

@ -16,6 +16,7 @@ limitations under the License.
package provenance
import (
"bytes"
"crypto"
"errors"
"io"
@ -24,6 +25,7 @@ import (
"strings"
"testing"
"github.com/ProtonMail/go-crypto/openpgp/armor"
pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/packet"
"github.com/stretchr/testify/assert"
@ -143,6 +145,72 @@ func TestLoadKeyRing(t *testing.T) {
}
}
func TestLoadKeyRingKeybox(t *testing.T) {
k, err := loadKeyRing(testKeybox)
require.NoError(t, err)
require.Len(t, k, 1)
_, ok := k[0].Identities[testKeyName]
assert.True(t, ok, "expected %q in keybox keyring", testKeyName)
}
func TestLoadKeyRingMixedKeybox(t *testing.T) {
k, err := loadKeyRing(testMixedKeybox)
require.NoError(t, err)
require.Len(t, k, 2)
names := make([]string, 0, len(k))
hasEdDSA := false
for _, e := range k {
for n := range e.Identities {
names = append(names, n)
}
if e.PrimaryKey != nil && e.PrimaryKey.PubKeyAlgo == packet.PubKeyAlgoEdDSA {
hasEdDSA = true
}
}
assert.Contains(t, names, testKeyName)
assert.True(t, hasEdDSA, "expected an Ed25519 key in %s", testMixedKeybox)
}
func TestLoadKeyRingArmored(t *testing.T) {
k, err := loadKeyRing(testArmoredPubfile)
require.NoError(t, err)
require.Len(t, k, 1)
_, ok := k[0].Identities[testKeyName]
assert.True(t, ok, "expected %q in armored keyring", testKeyName)
}
func TestLoadKeyRingArmoredMultiBlock(t *testing.T) {
// A keyring assembled by concatenating exports (cat a.asc b.asc) must
// load every block, the same way gpg --import does.
k, err := loadKeyRing(testMultiBlockArmored)
require.NoError(t, err)
require.Len(t, k, 2)
names := make([]string, 0, len(k))
for _, e := range k {
for n := range e.Identities {
names = append(names, n)
}
}
assert.Contains(t, names, testKeyName)
}
func TestLoadArmoredKeyRingRejectsNonKeyBlocks(t *testing.T) {
var buf bytes.Buffer
w, err := armor.Encode(&buf, "PGP MESSAGE", nil)
require.NoError(t, err)
_, err = w.Write([]byte("not a key"))
require.NoError(t, err)
require.NoError(t, w.Close())
_, err = loadArmoredKeyRing(buf.Bytes())
assert.ErrorContains(t, err, "expected a public or private key block")
}
func TestDigest(t *testing.T) {
f, err := os.Open(testChartfile)
require.NoError(t, err)
@ -318,6 +386,31 @@ func TestVerify(t *testing.T) {
}
}
// TestVerifyKeyboxKeyring mirrors TestVerify with the keyring loaded from a
// GnuPG keybox instead of the legacy binary format.
func TestVerifyKeyboxKeyring(t *testing.T) {
signer, err := NewFromKeyring(testKeybox, "")
require.NoError(t, err)
archiveData, err := os.ReadFile(testChartfile)
require.NoError(t, err)
sigData, err := os.ReadFile(testSigBlock)
require.NoError(t, err)
ver, err := signer.Verify(archiveData, sigData, filepath.Base(testChartfile))
require.NoError(t, err)
require.NotNil(t, ver.SignedBy)
_, ok := ver.SignedBy.Identities[testKeyName]
assert.True(t, ok, "expected chart to verify as signed by %q", testKeyName)
tamperedSigData, err := os.ReadFile(testTamperedSigBlock)
require.NoError(t, err)
_, err = signer.Verify(archiveData, tamperedSigData, filepath.Base(testChartfile))
assert.Error(t, err, "expected tampered signature to fail against keybox keyring")
}
// readSumFile reads a file containing a sum generated by the UNIX shasum tool.
func readSumFile(sumfile string) (string, error) {
data, err := os.ReadFile(sumfile)

@ -0,0 +1,40 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFeWbZ4BCADIsgNRyVBSWJkbH6h3UWWXsA9ce1u+YKvaCYFSjIujKycTAqCC
P7qUV2Oj/4E2zUcOD1/8/meDwuTnNTLzdSw4ujtFlKRSw7zelQE6sxvID0KM0pQK
7AxDTXsm/7Afd/fg4WNW0/hcbeiNz4TVmSWAnbqeLg8o8eljR5QhTk47H6Glo4hV
raeLCKG77qm2qOQ/m38ec+L5n9iUpoZZu1S5RXPUIanV5pLlx2rQsooIQEdJbTRF
Iv3+2Nj/56PFVdrw7E5ARqCD4PpzzYV7uj8vKumOp+VivFj95Ze6DFENh1WWy336
jQEj0uTolgOYeQ6AdJobwSPUeYXGV6Sf2vwXABEBAAG0XUhlbG0gVGVzdGluZyAo
VGhpcyBrZXkgc2hvdWxkIG9ubHkgYmUgdXNlZCBmb3IgdGVzdGluZy4gRE8gTk9U
IFRSVVNULikgPGhlbG0tdGVzdGluZ0BoZWxtLnNoPokBNwQTAQoAIQUCV5ZtngIb
AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCEO7+YH8GHYolFCAC0+ejT5dIX
0juRl5AdG6XAlEf9IrehkVlo0s5bG1Ucea9jjNRafmmHyMravrxZVI9zGFhaNpxA
+McnnWAO8lx5yTgnGW269qvFBsj5n0ItPewILQZjTEgwLYf1oc2qCFpk+8ovBnxZ
Kaz3J5QKqU09zLO2yALuI5FTCCUupJ3OGIrKvLJ4H2jsRk/5jgqIgqJotW/4Baza
4zemFdQHj9FPvq9sVeW5lyxM48i638GtslIgc0y2Yd/bfiy0tfIkskWz/kS1U6c9
Utr3UPwpBqHjXnakmpK7GMGlp5eDXgr63BEdLKxJJWWxIys35fR19XZXcAELrwMh
zkTzB/dvI375uQENBFeWbZ4BCADB6a8oiF2royWbvwmujBxSmD68QWziNZbSCuRs
M4WM1iy69kspJHRwJEVIE5pkoVfV6tIcwQQ0VkJmq6Pf/pyKeMAXp0u8bp5K0mVE
Yio1/adiLM3kX1yleoXLNUb1gNvsk6Kp6BZCUNRmjKlbKxapJKHmL+opX8UoeLeE
KSgJNL8jvo3nRgg+wIPnVDoizz7y03F0k4SbgNGdoA/FtZs/vcSrEfzOLgI2RxQS
dj/ePkxg9TvQygUSYgAVbYlASw6XMmd/zTjkfD9qa0l3WSejTisn85b611WryDpA
iYkYC3GzK8g7S9VwkAbtr79cbG+1djlA4aHASpil+zR12yEhABEBAAGJAR8EGAEK
AAkFAleWbZ4CGwwACgkQhDu/mB/Bh2LbNQgAiOOMNzmKzY4A7/ra8ppaH9oL8XIa
WvInivOsx22K09PPuPVqi/ooBiRGyRqhdVS4ShPOoOTR5tOsdfPpEMTHnGQ1+jW+
Tw5MXv3oMckWw3YEYptnuvon3wT4bOXLr+eYlY4Z1ONs+pAXwMiQ2zXNuKHpA2fR
HsF8Wyw57rCVn7K5nQgZCbVxasYlzvFGnmt/itSC5w/AnIvICDOWcoGFKTieqhME
IkCyvu+DNrMAumnD2fqF2olsM4IzCEPSMEQqJIGzZTtQTseS8NSDioRGnG1AJQ94
BssQVmTh0/hlpoTqXY803lR5wb1fr3RRnkOu+lbEI6AAUj51j5TAAo/1dA==
=bvcV
-----END PGP PUBLIC KEY BLOCK-----
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEaMzP2xYJKwYBBAHaRw8BAQdAXkL0KKTOZoMaM+6HsrkWv93zmLmnMiIoQvR2
F+bjx7G0KEhlbG0gRWQyNTUxOSBUZXN0IDxoZWxtLWVkMjU1MTlAaGVsbS5zaD6I
mQQTFgoAQRYhBEy+dUwlWAxsh43nEfuFaBLOL8tkBQJozM/bAhsDBQkFo5qABQsJ
CAcCAiICBhUKCQgLAgQWAgMBAh4HAheAAAoJEPuFaBLOL8tktBYA/2cclE0N+BZV
Wymk6XfwFMjErM6olWAfwA8DfAGQ55+lAQCqSa+Tkjd4dmToVHcb0QAm+zwklAY4
e6qWgGiivn77AA==
=wJ7K
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

@ -0,0 +1,30 @@
-----BEGIN PGP PUBLIC KEY BLOCK-----
mQENBFeWbZ4BCADIsgNRyVBSWJkbH6h3UWWXsA9ce1u+YKvaCYFSjIujKycTAqCC
P7qUV2Oj/4E2zUcOD1/8/meDwuTnNTLzdSw4ujtFlKRSw7zelQE6sxvID0KM0pQK
7AxDTXsm/7Afd/fg4WNW0/hcbeiNz4TVmSWAnbqeLg8o8eljR5QhTk47H6Glo4hV
raeLCKG77qm2qOQ/m38ec+L5n9iUpoZZu1S5RXPUIanV5pLlx2rQsooIQEdJbTRF
Iv3+2Nj/56PFVdrw7E5ARqCD4PpzzYV7uj8vKumOp+VivFj95Ze6DFENh1WWy336
jQEj0uTolgOYeQ6AdJobwSPUeYXGV6Sf2vwXABEBAAG0XUhlbG0gVGVzdGluZyAo
VGhpcyBrZXkgc2hvdWxkIG9ubHkgYmUgdXNlZCBmb3IgdGVzdGluZy4gRE8gTk9U
IFRSVVNULikgPGhlbG0tdGVzdGluZ0BoZWxtLnNoPokBNwQTAQoAIQUCV5ZtngIb
AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRCEO7+YH8GHYolFCAC0+ejT5dIX
0juRl5AdG6XAlEf9IrehkVlo0s5bG1Ucea9jjNRafmmHyMravrxZVI9zGFhaNpxA
+McnnWAO8lx5yTgnGW269qvFBsj5n0ItPewILQZjTEgwLYf1oc2qCFpk+8ovBnxZ
Kaz3J5QKqU09zLO2yALuI5FTCCUupJ3OGIrKvLJ4H2jsRk/5jgqIgqJotW/4Baza
4zemFdQHj9FPvq9sVeW5lyxM48i638GtslIgc0y2Yd/bfiy0tfIkskWz/kS1U6c9
Utr3UPwpBqHjXnakmpK7GMGlp5eDXgr63BEdLKxJJWWxIys35fR19XZXcAELrwMh
zkTzB/dvI375uQENBFeWbZ4BCADB6a8oiF2royWbvwmujBxSmD68QWziNZbSCuRs
M4WM1iy69kspJHRwJEVIE5pkoVfV6tIcwQQ0VkJmq6Pf/pyKeMAXp0u8bp5K0mVE
Yio1/adiLM3kX1yleoXLNUb1gNvsk6Kp6BZCUNRmjKlbKxapJKHmL+opX8UoeLeE
KSgJNL8jvo3nRgg+wIPnVDoizz7y03F0k4SbgNGdoA/FtZs/vcSrEfzOLgI2RxQS
dj/ePkxg9TvQygUSYgAVbYlASw6XMmd/zTjkfD9qa0l3WSejTisn85b611WryDpA
iYkYC3GzK8g7S9VwkAbtr79cbG+1djlA4aHASpil+zR12yEhABEBAAGJAR8EGAEK
AAkFAleWbZ4CGwwACgkQhDu/mB/Bh2LbNQgAiOOMNzmKzY4A7/ra8ppaH9oL8XIa
WvInivOsx22K09PPuPVqi/ooBiRGyRqhdVS4ShPOoOTR5tOsdfPpEMTHnGQ1+jW+
Tw5MXv3oMckWw3YEYptnuvon3wT4bOXLr+eYlY4Z1ONs+pAXwMiQ2zXNuKHpA2fR
HsF8Wyw57rCVn7K5nQgZCbVxasYlzvFGnmt/itSC5w/AnIvICDOWcoGFKTieqhME
IkCyvu+DNrMAumnD2fqF2olsM4IzCEPSMEQqJIGzZTtQTseS8NSDioRGnG1AJQ94
BssQVmTh0/hlpoTqXY803lR5wb1fr3RRnkOu+lbEI6AAUj51j5TAAo/1dA==
=bvcV
-----END PGP PUBLIC KEY BLOCK-----

Binary file not shown.

@ -0,0 +1,26 @@
#!/bin/sh
# Regenerate the file-backed keyring fixtures from the committed binary
# keyrings. Requires GnuPG 2.1+ (keybox support).
#
# helm-test-key.kbx keybox (pubring.kbx) containing the helm-test key
# helm-mixed-keyring.kbx keybox containing the RSA and Ed25519 test keys
# helm-test-key.asc ASCII-armored export of the helm-test key
# helm-mixed-keyring.asc two concatenated single-key armored exports
set -e
GNUPGHOME=$(mktemp -d)
export GNUPGHOME
chmod 700 "$GNUPGHOME"
gpg --batch --no-tty --quiet --import helm-test-key.pub
cp "$GNUPGHOME/pubring.kbx" helm-test-key.kbx
gpg --batch --no-tty --export --armor helm-testing@helm.sh > helm-test-key.asc
rm -rf "$GNUPGHOME"
GNUPGHOME=$(mktemp -d)
export GNUPGHOME
chmod 700 "$GNUPGHOME"
gpg --batch --no-tty --quiet --import helm-mixed-keyring.pub
cp "$GNUPGHOME/pubring.kbx" helm-mixed-keyring.kbx
gpg --batch --no-tty --export --armor helm-testing@helm.sh > helm-mixed-keyring.asc
gpg --batch --no-tty --export --armor helm-ed25519@helm.sh >> helm-mixed-keyring.asc
rm -rf "$GNUPGHOME"
Loading…
Cancel
Save