Skip to content

Commit 9652ebf

Browse files
authored
feat(verification): automatically verify on describe command (#1808)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent 54748d2 commit 9652ebf

File tree

4 files changed

+148
-18
lines changed

4 files changed

+148
-18
lines changed

app/cli/internal/action/workflow_run_describe.go

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,21 @@ import (
2222
"errors"
2323
"fmt"
2424
"sort"
25+
"strings"
2526

2627
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
2728
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
29+
"github.com/chainloop-dev/chainloop/pkg/attestation/verifier"
30+
intoto "github.com/in-toto/attestation/go/v1"
31+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
2832
"github.com/sigstore/cosign/v2/pkg/blob"
2933
"github.com/sigstore/cosign/v2/pkg/cosign"
3034
sigs "github.com/sigstore/cosign/v2/pkg/signature"
3135
"github.com/sigstore/sigstore/pkg/cryptoutils"
3236
"github.com/sigstore/sigstore/pkg/signature"
33-
34-
intoto "github.com/in-toto/attestation/go/v1"
35-
"github.com/secure-systems-lab/go-securesystemslib/dsse"
3637
sigdsee "github.com/sigstore/sigstore/pkg/signature/dsse"
38+
"google.golang.org/grpc/codes"
39+
"google.golang.org/grpc/status"
3740
)
3841

3942
type WorkflowRunDescribe struct {
@@ -157,17 +160,43 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes
157160
item.WorkflowRun.FinishedAt = toTimePtr(wr.FinishedAt.AsTime())
158161
}
159162

160-
attestation := resp.GetResult().GetAttestation()
163+
att := resp.GetResult().GetAttestation()
161164
// The item does not have associated attestation
162-
if attestation == nil {
165+
if att == nil {
163166
return item, nil
164167
}
165168

166-
envelope, err := decodeEnvelope(attestation.Envelope)
169+
envelope, err := decodeEnvelope(att.Envelope)
167170
if err != nil {
168171
return nil, err
169172
}
170173

174+
if att.Bundle != nil {
175+
sc := pb.NewSigningServiceClient(action.cfg.CPConnection)
176+
trResp, err := sc.GetTrustedRoot(ctx, &pb.GetTrustedRootRequest{})
177+
if err != nil {
178+
// if trusted root is not implemented, skip verification
179+
if status.Code(err) != codes.Unimplemented {
180+
return nil, fmt.Errorf("failed getting trusted root: %w", err)
181+
}
182+
}
183+
184+
if trResp != nil {
185+
tr, err := trustedRootPbToVerifier(trResp)
186+
if err != nil {
187+
return nil, fmt.Errorf("getting roots: %w", err)
188+
}
189+
if err = verifier.VerifyBundle(ctx, att.Bundle, tr); err != nil {
190+
if !errors.Is(err, verifier.ErrMissingVerificationMaterial) {
191+
action.cfg.Logger.Debug().Err(err).Msg("bundle verification failed")
192+
return nil, errors.New("bundle verification failed")
193+
}
194+
} else {
195+
item.Verified = true
196+
}
197+
}
198+
}
199+
171200
if opts.Verify {
172201
if err := verifyEnvelope(ctx, envelope, opts); err != nil {
173202
action.cfg.Logger.Debug().Err(err).Msg("verifying the envelope")
@@ -182,48 +211,48 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes
182211
return nil, fmt.Errorf("extracting statement: %w", err)
183212
}
184213

185-
envVars := make([]*EnvVar, 0, len(attestation.GetEnvVars()))
186-
for _, v := range attestation.GetEnvVars() {
214+
envVars := make([]*EnvVar, 0, len(att.GetEnvVars()))
215+
for _, v := range att.GetEnvVars() {
187216
envVars = append(envVars, &EnvVar{Name: v.Name, Value: v.Value})
188217
}
189218

190-
materials := make([]*Material, 0, len(attestation.GetMaterials()))
191-
for _, v := range attestation.GetMaterials() {
219+
materials := make([]*Material, 0, len(att.GetMaterials()))
220+
for _, v := range att.GetMaterials() {
192221
materials = append(materials, materialPBToAction(v))
193222
}
194223

195-
keys := make([]string, 0, len(attestation.GetAnnotations()))
196-
for k := range attestation.GetAnnotations() {
224+
keys := make([]string, 0, len(att.GetAnnotations()))
225+
for k := range att.GetAnnotations() {
197226
keys = append(keys, k)
198227
}
199228
sort.Strings(keys)
200229

201-
annotations := make([]*Annotation, 0, len(attestation.GetAnnotations()))
230+
annotations := make([]*Annotation, 0, len(att.GetAnnotations()))
202231
for _, k := range keys {
203232
annotations = append(annotations, &Annotation{
204-
Name: k, Value: attestation.GetAnnotations()[k],
233+
Name: k, Value: att.GetAnnotations()[k],
205234
})
206235
}
207236

208237
evaluations := make(map[string][]*PolicyEvaluation)
209-
for k, v := range attestation.GetPolicyEvaluations() {
238+
for k, v := range att.GetPolicyEvaluations() {
210239
evs := make([]*PolicyEvaluation, 0)
211240
for _, ev := range v.Evaluations {
212241
evs = append(evs, policyEvaluationPBToAction(ev))
213242
}
214243
evaluations[k] = evs
215244
}
216245

217-
policyEvaluationStatus := attestation.GetPolicyEvaluationStatus()
246+
policyEvaluationStatus := att.GetPolicyEvaluationStatus()
218247

219248
item.Attestation = &WorkflowRunAttestationItem{
220249
Envelope: envelope,
221-
Bundle: attestation.GetBundle(),
250+
Bundle: att.GetBundle(),
222251
statement: statement,
223252
EnvVars: envVars,
224253
Materials: materials,
225254
Annotations: annotations,
226-
Digest: attestation.DigestInCasBackend,
255+
Digest: att.DigestInCasBackend,
227256
PolicyEvaluations: evaluations,
228257
PolicyEvaluationStatus: &PolicyEvaluationStatus{
229258
Strategy: policyEvaluationStatus.Strategy,
@@ -236,6 +265,20 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes
236265
return item, nil
237266
}
238267

268+
func trustedRootPbToVerifier(resp *pb.GetTrustedRootResponse) (*verifier.TrustedRoot, error) {
269+
tr := &verifier.TrustedRoot{Keys: make(map[string][]*x509.Certificate)}
270+
for k, v := range resp.GetKeys() {
271+
for _, c := range v.Certificates {
272+
cert, err := cryptoutils.LoadCertificatesFromPEM(strings.NewReader(c))
273+
if err != nil {
274+
return nil, fmt.Errorf("loading certificate from PEM: %w", err)
275+
}
276+
tr.Keys[k] = append(tr.Keys[k], cert[0])
277+
}
278+
}
279+
return tr, nil
280+
}
281+
239282
func policyEvaluationPBToAction(in *pb.PolicyEvaluation) *PolicyEvaluation {
240283
var pr *PolicyReference
241284
if in.PolicyReference != nil {

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ require (
8585
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
8686
github.com/sigstore/fulcio v1.6.3
8787
github.com/sigstore/protobuf-specs v0.3.2
88+
github.com/sigstore/sigstore-go v0.6.1
8889
github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8
8990
github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8
9091
github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8
@@ -185,6 +186,7 @@ require (
185186
github.com/spiffe/go-spiffe/v2 v2.3.0 // indirect
186187
github.com/stoewer/go-strcase v1.3.0 // indirect
187188
github.com/tchap/go-patricia/v2 v2.3.1 // indirect
189+
github.com/theupdateframework/go-tuf/v2 v2.0.1 // indirect
188190
github.com/tklauser/go-sysconf v0.3.14 // indirect
189191
github.com/tklauser/numcpus v0.9.0 // indirect
190192
github.com/xanzy/ssh-agent v0.3.3 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
800800
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
801801
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
802802
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
803+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
804+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
803805
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
804806
github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM=
805807
github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
//
2+
// Copyright 2025 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package verifier
17+
18+
import (
19+
"context"
20+
"crypto/sha256"
21+
"crypto/x509"
22+
"errors"
23+
"fmt"
24+
25+
"github.com/chainloop-dev/chainloop/pkg/attestation"
26+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
27+
"github.com/sigstore/cosign/v2/pkg/cosign"
28+
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
29+
sigstorebundle "github.com/sigstore/sigstore-go/pkg/bundle"
30+
sigdsee "github.com/sigstore/sigstore/pkg/signature/dsse"
31+
"google.golang.org/protobuf/encoding/protojson"
32+
)
33+
34+
type TrustedRoot struct {
35+
// map key identifiers to a chain of certificates
36+
Keys map[string][]*x509.Certificate
37+
}
38+
39+
var ErrMissingVerificationMaterial = errors.New("missing material")
40+
41+
func VerifyBundle(ctx context.Context, bundleBytes []byte, tr *TrustedRoot) error {
42+
bundle := new(protobundle.Bundle)
43+
// unmarshal and validate
44+
if err := protojson.Unmarshal(bundleBytes, bundle); err != nil {
45+
return fmt.Errorf("invalid bundle: %w", err)
46+
}
47+
48+
if bundle.GetVerificationMaterial() == nil || bundle.GetVerificationMaterial().GetCertificate() == nil {
49+
// nothing to verify
50+
return ErrMissingVerificationMaterial
51+
}
52+
53+
// Use sigstore helpers
54+
var sb sigstorebundle.Bundle
55+
if err := sb.UnmarshalJSON(bundleBytes); err != nil {
56+
return fmt.Errorf("invalid bundle: %w", err)
57+
}
58+
59+
vc, err := sb.VerificationContent()
60+
if err != nil {
61+
return fmt.Errorf("could not get verification material: %w", err)
62+
}
63+
signingCert := vc.GetCertificate()
64+
65+
aki := fmt.Sprintf("%x", sha256.Sum256(signingCert.AuthorityKeyId))
66+
chain, ok := tr.Keys[aki]
67+
if !ok {
68+
return fmt.Errorf("trusted root not found for signing key with AKI %s", aki)
69+
}
70+
71+
verifier, err := cosign.ValidateAndUnpackCertWithChain(signingCert, chain, &cosign.CheckOpts{IgnoreSCT: true})
72+
if err != nil {
73+
return fmt.Errorf("validating the certificate: %w", err)
74+
}
75+
76+
dsseVerifier, err := dsse.NewEnvelopeVerifier(&sigdsee.VerifierAdapter{SignatureVerifier: verifier})
77+
if err != nil {
78+
return fmt.Errorf("creating DSSE verifier: %w", err)
79+
}
80+
81+
_, err = dsseVerifier.Verify(ctx, attestation.DSSEEnvelopeFromBundle(bundle))
82+
return err
83+
}

0 commit comments

Comments
 (0)